Restructuration de pmoaudio avec ajout des messages de synchro

This commit is contained in:
2025-10-30 08:54:47 +01:00
parent 579109374d
commit 8fe110eeb7
27 changed files with 3183 additions and 400 deletions

View File

@@ -11,6 +11,7 @@ simd = []
tokio = { version = "1.42", features = ["full"] }
async-trait = "0.1"
pmoflac = { path = "../pmoflac" }
pmometadata = { path = "../pmometadata" }
paste = "1"
soxr = "0.6.0"
bytemuck = "1.24.0"

View File

@@ -0,0 +1,342 @@
# PMOAudio - Refactoring Summary
## Vue d'ensemble
Refactoring complet du système audio pour supporter plusieurs types de samples (entiers et flottants) avec une architecture générique optimisée pour le temps réel.
## Architecture
### Option choisie: Générique + Enum plat
- **`AudioChunkData<T: Sample>`**: Structure générique pour factoriser le code
- **`AudioChunk`**: Enum plat avec 6 variants (I8, I16, I24, I32, F32, F64)
- **`Sample` trait**: Interface unifiée pour tous les types de samples
## Nouveaux fichiers créés
### 1. `src/sample_types.rs`
Définition du trait `Sample` et du type `I24` (24-bit audio).
**Features principales:**
- Type `I24` wrapper sur `i32` avec validation de plage (±2^23)
- Trait `Sample` implémenté pour: i8, i16, I24, i32, f32, f64
- Conversions normalisées vers/depuis f64 et f32
- Tests unitaires complets
### 2. `src/conversions.rs`
Module complet de conversions entre tous les types audio.
**Features principales:**
- **Conversions Int → Int**: Utilise `bitdepth_change_stereo` avec SIMD
- **Conversions Int → Float**: Utilise `i32_stereo_to_pairs_f32` avec SIMD
- **Conversions Float → Int**: Utilise `pairs_f32_to_i32_stereo` avec SIMD
- **Conversions Float → Float**: Direct avec cast
- **34 implémentations From/Into** pour conversions ergonomiques
- Tests de round-trip et validation
**Point clé**: Les conversions I32 ↔ F32/F64 n'ont **pas besoin** de paramètre BitDepth car le type définit lui-même sa résolution (I32 = ±2^31).
### 3. `src/macros.rs`
Macros pour simplifier la manipulation des AudioChunk et AudioSegment.
**Macros disponibles:**
- `extract_chunk_data!(chunk, TYPE)` - Extrait les données typées
- `match_chunk!(chunk, data => expr)` - Pattern matching unifié
- `map_chunk!(chunk, data => transform)` - Transformation préservant le type
- `is_chunk_type!(chunk, TYPE)` - Prédicat de type
- `extract_audio_chunk!(segment)` - Extrait AudioChunk d'un segment
- `extract_sync_marker!(segment)` - Extrait SyncMarker d'un segment
- `match_segment!(segment, chunk => ..., marker => ...)` - Match sur segment
**Tests**: 7 tests unitaires
## Fichiers modifiés
### 1. `src/audio_chunk.rs` - Refactoring complet
**Avant:**
```rust
pub struct AudioChunk {
stereo: Arc<[[i32; 2]]>,
sample_rate: u32,
bit_depth: BitDepth,
}
```
**Après:**
```rust
pub struct AudioChunkData<T: Sample> {
stereo: Arc<[[T; 2]]>,
sample_rate: u32,
gain_db: f64, // Toujours en dB
}
pub enum AudioChunk {
I8(Arc<AudioChunkData<i8>>),
I16(Arc<AudioChunkData<i16>>),
I24(Arc<AudioChunkData<I24>>),
I32(Arc<AudioChunkData<i32>>),
F32(Arc<AudioChunkData<f32>>),
F64(Arc<AudioChunkData<f64>>),
}
```
**Nouvelles méthodes:**
- `AudioChunk::to_f32()`, `to_f64()`, `to_i32()` - Conversions de type
- `AudioChunk::set_gain_db()` - Modification du gain
- `AudioChunk::type_name()` - Nom du type runtime
- Implémentations spécialisées pour i32, f32, f64
**Tests**: 4 tests unitaires
### 2. `src/audio_segment.rs` - Helpers ergonomiques
**Nouvelles méthodes d'accès:**
- `as_chunk()` - Récupère le AudioChunk
- `as_sync_marker()` - Récupère le SyncMarker
- `as_track_metadata()` - Extrait les métadonnées de track
- `as_error()` - Récupère le message d'erreur
**Helpers de conversion:**
- `to_f32_chunk()` - Convertit vers F32
- `to_i32_chunk()` - Convertit vers I32
**Helpers de propriétés:**
- `sample_rate()` - Sample rate du chunk
- `frame_count()` - Nombre de frames
- `gain_db()` - Gain en dB
- `chunk_type_name()` - Type du chunk
**Manipulation du gain:**
- `with_gain_db(gain_db)` - Nouveau segment avec gain absolu
- `adjust_gain_db(delta_db)` - Nouveau segment avec gain relatif
**Tests**: 4 tests unitaires
### 3. `src/dsp/int_float.rs` - Simplification
**Changements:**
- ❌ Suppression du trait `BitDepthType` obsolète
- ❌ Suppression des types `Bit8`, `Bit16`, `Bit24`, `Bit32`
- ✅ Utilisation de l'enum `BitDepth` du module principal
- ✅ Fonctions SIMD préservées et optimisées
- ✅ Paramètres runtime au lieu de génériques
### 4. `src/dsp/resampling.rs` - Mise à jour BitDepth
**Changements:**
- Type `ResamplingError` créé (remplace `AudioError` manquant)
- `Resampler.bit_depth: u32``BitDepth`
- Match sur les variants d'enum au lieu de valeurs numériques
- Qualité de resampling adaptée au bit depth (VeryHigh pour 24/32-bit)
### 5. `src/lib.rs` - Exports et organisation
**Ajouts:**
- `mod macros` avec `#[macro_use]`
- `pub use sample_types::{I24, Sample}`
- `pub use audio_segment::_AudioSegment` (pour les macros)
- `pub mod conversions`
**Temporairement désactivé:**
- `mod nodes` (commenté)
## Statistiques de tests
### Tests réussis: **35/35** ✅
**Répartition:**
- `audio_chunk`: 4 tests
- `audio_segment`: 4 tests
- `conversions`: 12 tests
- `macros`: 7 tests
- `sample_types`: 5 tests
- `events`: 3 tests
### Couverture des conversions
**From/Into implémentations: 34 au total**
- Wrapper conversions (6): AudioChunkData → AudioChunk
- I16 ↔ I32 (2)
- I24 ↔ I32 (2)
- I32 ↔ F32 (2)
- I32 ↔ F64 (2)
- F32 ↔ F64 (2)
- Et toutes les autres combinaisons...
## Optimisations
### Performance temps réel
- **Objectif**: Audio 192kHz/24-bit stéréo en temps réel
- **SIMD**: Toutes les conversions critiques utilisent les fonctions SIMD du module DSP
- **Zero-copy**: Partage via `Arc<[[T; 2]]>`
- **Lazy evaluation**: Le gain n'est appliqué que lors de la lecture des frames
### Harmonisation du gain
-**Tous les gains en dB** (décibels)
- ✅ Helpers de conversion: `db_to_linear()`, `linear_to_db()`
- ❌ Plus d'interfaces linéaires (sauf helpers de conversion)
## Exemple d'utilisation
Voir [`examples/audio_chunk_api.rs`](examples/audio_chunk_api.rs) pour une démonstration complète.
### Création rapide
```rust
// Chunk I32
let chunk = AudioChunkData::new(
vec![[1000i32, 2000i32]],
48000,
0.0
);
// Segment avec gain
let segment = AudioSegment::new_chunk_with_gain_db(
0, 0.0,
vec![[1000i32, 2000i32]],
48000,
BitDepth::B32,
6.0 // +6 dB
);
```
### Conversions
```rust
// Via méthodes
let chunk_f32 = audio_chunk.to_f32();
// Via From/Into
let chunk_i32: Arc<AudioChunkData<i32>> = (&*chunk_i16).into();
```
### Macros
```rust
// Type checking
if is_chunk_type!(&chunk, I32) {
// ...
}
// Pattern matching universel
match_chunk!(&chunk, data => {
println!("{} frames", data.len());
});
// Transformation
let with_gain = map_chunk!(&chunk, data => {
data.set_gain_db(6.0)
});
```
### Helpers AudioSegment
```rust
// Accès ergonomique
if let Some(sr) = segment.sample_rate() {
println!("Sample rate: {}", sr);
}
// Manipulation du gain
let louder = segment.adjust_gain_db(3.0)?;
// Conversion
let f32_chunk = segment.to_f32_chunk()?;
```
## Points clés de design
### 1. Type = Résolution
Chaque type définit sa propre résolution:
- I8 = ±2^7 (128)
- I16 = ±2^15 (32,768)
- I24 = ±2^23 (8,388,608)
- I32 = ±2^31 (2,147,483,648)
- F32 / F64 = normalisé [-1.0, 1.0]
**Conséquence**: Pas besoin de paramètre `BitDepth` pour les conversions I32 ↔ Float.
### 2. Gain toujours en dB
- Plus de gains linéaires dans l'API principale
- Conversions disponibles via helpers si nécessaire
- Évaluation paresseuse du gain
### 3. Immutabilité
- Toutes les modifications créent de nouvelles instances
- Partage efficace via `Arc`
- Pas de copy-on-write nécessaire pour les données audio
### 4. Stéréo strict
- Format fixe: `[[T; 2]]` (gauche, droite)
- Pas de support multicanal pour l'instant
- Optimisé pour le cas d'usage principal
## Compilation et tests
```bash
# Build
cargo build --package pmoaudio
# Tests
cargo test --package pmoaudio --lib
# Exemple
cargo run --package pmoaudio --example audio_chunk_api
```
**Statut**: ✅ Compilation sans erreur, tous les tests passent
## Travail futur (optionnel)
Les tâches suivantes ont été identifiées mais ne sont pas critiques:
1. **Benchmark temps réel 192kHz/24-bit**
- Valider les performances en conditions réelles
- Mesurer l'overhead des conversions
2. **Macros avancées**
- Macros procédurales pour génération de code
- DSL pour pipelines audio
3. **Support multicanal**
- Format `[[T; N]]` générique
- Gestion des configurations surround
4. **Réactivation des Nodes**
- Mise à jour avec la nouvelle API
- Tests d'intégration complets
## Notes de migration
Pour le code existant utilisant l'ancienne API:
### AudioChunk
**Avant:**
```rust
let chunk = AudioChunk::new(stereo, 48000, BitDepth::B32);
let gain = chunk.gain_linear();
```
**Après:**
```rust
let chunk_data = AudioChunkData::new(stereo, 48000, 0.0);
let chunk = AudioChunk::I32(chunk_data);
let gain = chunk.gain_linear(); // Toujours disponible
```
### AudioSegment
**Avant:**
```rust
segment.chunk.sample_rate
```
**Après:**
```rust
segment.sample_rate().unwrap() // Avec helper
// ou
segment.as_chunk().unwrap().sample_rate() // Direct
```
---
**Date**: 2025-11-01
**Version**: PMOAudio 0.1.0
**Status**: ✅ Refactoring complet, tous les tests passent

View File

@@ -0,0 +1,194 @@
//! Exemples d'utilisation de l'API AudioChunk et AudioSegment
//!
//! Ce fichier démontre les différentes façons de créer et manipuler
//! des chunks audio avec la nouvelle architecture générique.
use pmoaudio::*;
fn main() {
println!("=== Exemples d'utilisation de l'API AudioChunk ===\n");
// ============ Création de chunks de différents types ============
example_create_chunks();
// ============ Conversions entre types ============
example_conversions();
// ============ Utilisation des macros ============
example_macros();
// ============ AudioSegment et helpers ============
example_audio_segments();
// ============ Manipulation du gain ============
example_gain_manipulation();
}
fn example_create_chunks() {
println!(">>> Création de chunks audio\n");
// Chunk I32 stéréo
let stereo_i32 = vec![[1000i32, 2000i32], [3000i32, 4000i32]];
let chunk_i32 = AudioChunkData::new(stereo_i32, 48000, 0.0);
println!("Chunk I32: {} frames @ {}Hz", chunk_i32.len(), chunk_i32.sample_rate());
// Chunk F32 stéréo (normalisé [-1.0, 1.0])
let stereo_f32 = vec![[0.5f32, -0.5f32], [0.8f32, -0.8f32]];
let chunk_f32 = AudioChunkData::new(stereo_f32, 48000, 0.0);
println!("Chunk F32: {} frames @ {}Hz", chunk_f32.len(), chunk_f32.sample_rate());
// Chunk depuis canaux séparés
let left = vec![100i32, 200i32, 300i32];
let right = vec![150i32, 250i32, 350i32];
let chunk_from_channels = AudioChunkData::<i32>::from_channels(left, right, 44100);
println!("Chunk from channels: {} frames", chunk_from_channels.len());
// Chunk avec gain
let chunk_with_gain = AudioChunkData::new(
vec![[1000i32, 2000i32]],
48000,
6.0, // +6 dB
);
println!("Chunk with gain: {} dB\n", chunk_with_gain.gain_db());
}
fn example_conversions() {
println!(">>> Conversions entre types\n");
// Créer un chunk I32
let i32_data = vec![[1_000_000i32, 2_000_000i32]];
let chunk_i32 = AudioChunkData::new(i32_data, 48000, 0.0);
let audio_chunk = AudioChunk::I32(chunk_i32);
println!("Type original: {}", audio_chunk.type_name());
// Conversion vers F32
let audio_chunk_f32 = audio_chunk.to_f32();
println!("Après conversion to_f32: {}", audio_chunk_f32.type_name());
// Conversion vers F64
let audio_chunk_f64 = audio_chunk_f32.to_f64();
println!("Après conversion to_f64: {}", audio_chunk_f64.type_name());
// Retour vers I32
let audio_chunk_back = audio_chunk_f64.to_i32();
println!("Après conversion to_i32: {}", audio_chunk_back.type_name());
// Utilisation des traits From/Into
let chunk_i16 = AudioChunkData::new(vec![[1000i16, 2000i16]], 48000, 0.0);
let chunk_i32_from_i16: std::sync::Arc<AudioChunkData<i32>> = (&*chunk_i16).into();
println!("\nConversion I16 → I32 via Into: {} frames", chunk_i32_from_i16.len());
println!();
}
fn example_macros() {
println!(">>> Utilisation des macros\n");
// Créer différents types de chunks
let chunk_i32 = AudioChunk::I32(AudioChunkData::new(vec![[100i32, 200i32]], 48000, 0.0));
let chunk_f32 = AudioChunk::F32(AudioChunkData::new(vec![[0.5f32, -0.5f32]], 48000, 0.0));
// Macro is_chunk_type!
println!("chunk_i32 is I32: {}", is_chunk_type!(&chunk_i32, I32));
println!("chunk_i32 is F32: {}", is_chunk_type!(&chunk_i32, F32));
println!("chunk_f32 is F32: {}", is_chunk_type!(&chunk_f32, F32));
// Macro extract_chunk_data!
if let Some(data) = extract_chunk_data!(&chunk_i32, I32) {
println!("\nExtracted I32 data: {} frames", data.len());
}
// Macro match_chunk! pour traiter n'importe quel type
let frame_count = match_chunk!(&chunk_i32, data => {
data.len()
});
println!("Frame count via match_chunk: {}", frame_count);
// Macro map_chunk! pour transformer tout en préservant le type
let chunk_with_gain = map_chunk!(&chunk_i32, data => {
data.set_gain_db(6.0)
});
println!("\nGain après map_chunk: {} dB", chunk_with_gain.gain_db());
println!();
}
fn example_audio_segments() {
println!(">>> AudioSegment et helpers\n");
// Créer un segment audio
let segment = AudioSegment::new_chunk(
0,
0.0,
vec![[1000i32, 2000i32], [3000i32, 4000i32]],
48000,
BitDepth::B32,
);
// Accès aux propriétés via les helpers
println!("Segment info:");
println!(" - Type: {}", segment.chunk_type_name().unwrap());
println!(" - Sample rate: {} Hz", segment.sample_rate().unwrap());
println!(" - Frame count: {}", segment.frame_count().unwrap());
println!(" - Gain: {} dB", segment.gain_db().unwrap());
// Conversion du chunk
if let Some(f32_chunk) = segment.to_f32_chunk() {
println!("\nChunk converti en F32: {}", f32_chunk.type_name());
}
// Créer un marqueur de sync
let heartbeat = AudioSegment::new_hearbeat(1, 1.0);
println!("\nHeartbeat segment:");
println!(" - Is audio: {}", heartbeat.is_audio_chunk());
println!(" - Is heartbeat: {}", heartbeat.is_heartbeat());
// Macro extract_audio_chunk!
if let Some(chunk) = extract_audio_chunk!(&*segment) {
println!("\nExtracted chunk type: {}", chunk.type_name());
}
// Macro match_segment!
let info = match_segment!(&*segment,
chunk => format!("Audio chunk: {}", chunk.type_name()),
_marker => "Sync marker".to_string()
);
println!("Segment info via macro: {}", info);
println!();
}
fn example_gain_manipulation() {
println!(">>> Manipulation du gain\n");
// Créer un segment
let segment = AudioSegment::new_chunk(
0,
0.0,
vec![[1000i32, 2000i32]],
48000,
BitDepth::B32,
);
println!("Gain initial: {} dB", segment.gain_db().unwrap());
// Définir un gain absolu
let segment_6db = segment.with_gain_db(6.0).unwrap();
println!("Après with_gain_db(6.0): {} dB", segment_6db.gain_db().unwrap());
// Ajuster le gain (relatif)
let segment_9db = segment_6db.adjust_gain_db(3.0).unwrap();
println!("Après adjust_gain_db(+3.0): {} dB", segment_9db.gain_db().unwrap());
// Les segments originaux ne sont pas modifiés (immutabilité)
println!("Gain du segment original: {} dB", segment.gain_db().unwrap());
// Conversion gain linéaire ↔ dB
let linear_gain = db_to_linear(6.0);
let gain_db = linear_to_db(linear_gain);
println!("\n6 dB = {:.4}x (linéaire)", linear_gain);
println!("{:.4}x = {:.2} dB", linear_gain, gain_db);
println!();
}

View File

@@ -1,404 +1,456 @@
//! AudioChunk : Représentation générique de données audio stéréo
//!
//! Cette nouvelle architecture supporte différents types de samples :
//! - Entiers : i8, i16, I24 (24-bit), i32
//! - Flottants : f32, f64
//!
//! L'utilisation de génériques permet de factoriser le code tout en gardant
//! des performances optimales grâce à la monomorphisation.
use std::sync::Arc;
use crate::{dsp, BitDepth};
use crate::{dsp, BitDepth, Sample, I24};
/// Représente un chunk audio stéréo avec données partagées via Arc
// ============================================================================
// AudioChunkData<T> : Structure générique pour un chunk audio typé
// ============================================================================
/// Représente un chunk audio stéréo typé avec partage zero-copy 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.
/// Cette structure générique encapsule des données audio de n'importe quel type
/// de sample (i8, i16, I24, i32, f32, f64). Les données sont partagées via `Arc`
/// pour permettre un partage efficace entre plusieurs consumers sans copier.
///
/// # 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)
/// - Le clonage d'un `AudioChunkData` ne clone que le pointeur Arc (très rapide)
/// - Les données audio réelles ne sont jamais copiées tant qu'on ne modifie pas
/// - Plusieurs nodes peuvent partager le même chunk simultanément
///
/// # Gain
///
/// Le gain est stocké en décibels (dB) et n'est pas appliqué aux données tant
/// qu'on n'appelle pas explicitement `apply_gain()`. Cela permet de propager
/// des changements de gain sans recopier les données.
///
/// # Exemples
///
/// ```
/// use pmoaudio::{AudioChunk, BitDepth};
/// use pmoaudio::{AudioChunkData, I24};
///
/// // Créer un chunk avec des données générées
/// let stereo = vec![[0, 100], [200, 300], [400, 500]];
/// let chunk = AudioChunk::new(0, stereo, 48_000, BitDepth::B24);
/// // Créer un chunk I24
/// let stereo = vec![[I24::new(1_000_000).unwrap(), I24::new(500_000).unwrap()]; 1000];
/// let chunk = AudioChunkData::new(stereo, 48_000, 0.0);
///
/// assert_eq!(chunk.len(), 3);
/// assert_eq!(chunk.len(), 1000);
/// assert_eq!(chunk.sample_rate(), 48_000);
/// ```
#[derive(Debug, Clone)]
pub struct AudioChunk {
/// Numéro dordre dans le flux.
/// Sert à conserver la séquence et détecter déventuelles pertes.
order: u64,
pub struct AudioChunkData<T: Sample> {
/// Frames stéréo [L, R], partagées et immuables via Arc
stereo: Arc<[[T; 2]]>,
/// Canal gauche, partagé et immuable.
/// Toute transformation doit créer un nouveau `AudioChunk`.
stereo: Arc<[[i32; 2]]>,
/// Taux déchantillonnage (Hz).
/// Exemples : 44 100, 48 000, 96 000, 192 000.
/// Taux d'échantillonnage en Hz (44100, 48000, 96000, 192000, etc.)
sample_rate: u32,
/// Profondeur de bits des échantillons audio effectifs.
/// Gain appliqué au flux audio, en décibels (dB)
///
/// Indique la résolution utile des valeurs dans les buffers.
/// Exemples : `16` pour un flux PCM 16 bits, `24` pour du PCM 24 bits, `32` pour du plein i32.
/// Ce champ permet dadapter les traitements DSP (normalisation, conversion, etc.).
bit_depth: BitDepth,
/// Gain appliqué au flux audio, en décibels (dB).
///
/// Conversion : `gain_linear = 10^(gain_db / 20)`
/// Valeur par défaut : `0.0 dB` (aucune modification).
/// Exemples : `-6 dB` ≈ moitié du volume ; `+6 dB` ≈ double.
gain: f64,
/// Conversion : `gain_linear = 10^(gain_db / 20)`
/// Valeur par défaut : `0.0 dB` (aucune modification)
/// Exemples : `-6 dB` ≈ moitié du volume ; `+6 dB` ≈ double
gain_db: f64,
}
impl AudioChunk {
impl<T: Sample> AudioChunkData<T> {
/// 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
/// * `stereo` - Samples interleavés par frame `[L, R]`
/// * `stereo` - Frames stéréo `[L, R]`
/// * `sample_rate` - Taux d'échantillonnage en Hz
/// * `bit_depth` - Profondeur de bits des échantillons
/// * `gain_db` - Gain initial en décibels (0.0 = unity gain)
///
/// # Exemples
///
/// ```
/// use pmoaudio::{AudioChunk, BitDepth};
/// use pmoaudio::AudioChunkData;
///
/// let chunk = AudioChunk::new(
/// 0,
/// vec![[0, 0], [1_000_000, 1_000_000]],
/// let chunk = AudioChunkData::new(
/// vec![[0.0f32, 0.0f32]; 1000],
/// 48_000,
/// BitDepth::B24,
/// 0.0,
/// );
/// ```
pub fn new(
order: u64,
stereo: Vec<[i32; 2]>,
sample_rate: u32,
bit_depth: BitDepth,
) -> Arc<Self> {
pub fn new(stereo: Vec<[T; 2]>, sample_rate: u32, gain_db: f64) -> Arc<Self> {
Arc::new(Self {
order,
stereo: Arc::from(stereo),
sample_rate,
bit_depth,
gain: 0.0,
gain_db,
})
}
/// Crée un chunk avec un gain spécifique (en dB)
pub fn with_gain_db(
order: u64,
stereo: Vec<[i32; 2]>,
sample_rate: u32,
bit_depth: BitDepth,
gain_db: f64,
) -> Arc<Self> {
Self::new(order, stereo, sample_rate, bit_depth).set_gain_db(gain_db)
}
/// Crée un chunk avec un gain spécifique (en gain linéaire).
///
/// Le gain linéaire sera converti en décibels.
pub fn with_gain_linear(
order: u64,
stereo: Vec<[i32; 2]>,
sample_rate: u32,
bit_depth: BitDepth,
gain_linear: f64,
) -> Arc<Self> {
Self::new(order, stereo, sample_rate, bit_depth).set_gain_linear(gain_linear)
}
/// Construit un chunk à partir de deux vecteurs `i32` séparés (L/R).
pub fn from_channels_i32(
order: u64,
left: Vec<i32>,
right: Vec<i32>,
sample_rate: u32,
bit_depth: BitDepth,
) -> Arc<Self> {
assert_eq!(
left.len(),
right.len(),
"channels must have identical length"
);
let stereo = left
.into_iter()
.zip(right.into_iter())
.map(|(l, r)| [l, r])
.collect();
Self::new(order, stereo, sample_rate, bit_depth)
}
/// Construit un chunk à partir de vecteurs `f32` normalisés dans [-1.0, 1.0].
pub fn from_channels_f32(
order: u64,
left: Vec<f32>,
right: Vec<f32>,
sample_rate: u32,
bit_depth: BitDepth,
) -> Arc<Self> {
assert_eq!(
left.len(),
right.len(),
"channels must have identical length"
);
let stereo = left
.into_iter()
.zip(right.into_iter())
.map(|(l, r)| [quantize_sample(l, bit_depth), quantize_sample(r, bit_depth)])
.collect();
Self::new(order, stereo, sample_rate, bit_depth)
}
/// Construit un chunk à partir de frames stéréo normalisées [-1.0, 1.0].
pub fn from_pairs_f32(
order: u64,
pairs: Vec<[f32; 2]>,
sample_rate: u32,
bit_depth: BitDepth,
) -> Arc<Self> {
let stereo = pairs
.into_iter()
.map(|p| {
[
quantize_sample(p[0], bit_depth),
quantize_sample(p[1], bit_depth),
]
})
.collect();
Self::new(order, stereo, sample_rate, bit_depth)
}
/// Retourne le nombre d'échantillons par canal
///
/// # Exemples
///
/// ```
/// use pmoaudio::{AudioChunk, BitDepth};
///
/// let chunk = AudioChunk::new(0, vec![[0i32; 2]; 1000], 48_000, BitDepth::B24);
/// assert_eq!(chunk.len(), 1000);
/// ```
/// Retourne le nombre d'échantillons par canal (frames)
#[inline]
pub fn len(&self) -> usize {
self.stereo.len()
}
/// Vérifie si le chunk est vide
#[inline]
pub fn is_empty(&self) -> bool {
self.stereo.is_empty()
}
/// Numéro de séquence du chunk dans le flux.
pub fn order(&self) -> u64 {
self.order
}
/// Taux d'échantillonnage (Hz).
/// Taux d'échantillonnage (Hz)
#[inline]
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
/// Profondeur de bits effective.
pub fn bit_depth(&self) -> BitDepth {
self.bit_depth
}
/// Gain courant en décibels.
/// Gain courant en décibels
#[inline]
pub fn gain_db(&self) -> f64 {
self.gain
self.gain_db
}
/// Gain sous forme linéaire.
/// Gain sous forme linéaire
#[inline]
pub fn gain_linear(&self) -> f64 {
db_to_linear(self.gain)
db_to_linear(self.gain_db)
}
/// Convertit un gain linéaire (>0) en décibels.
pub fn gain_db_from_linear(gain_linear: f64) -> f64 {
linear_to_db(gain_linear)
}
/// Convertit un gain en décibels vers un gain linéaire.
pub fn gain_linear_from_db(gain_db: f64) -> f64 {
db_to_linear(gain_db)
}
/// Retourne une vue immuable sur les frames `[L,R]`.
pub fn frames(&self) -> &[[i32; 2]] {
/// Retourne une vue immuable sur les frames `[L, R]`
#[inline]
pub fn frames(&self) -> &[[T; 2]] {
&self.stereo
}
/// Clone les frames stéréo dans un `Vec`.
pub fn clone_frames(&self) -> Vec<[i32; 2]> {
/// Clone les frames stéréo dans un `Vec`
#[inline]
pub fn clone_frames(&self) -> Vec<[T; 2]> {
self.stereo.to_vec()
}
/// Convertit les frames au format `f32` normalisé [-1.0, 1.0].
pub fn to_pairs_f32(&self) -> Vec<[f32; 2]> {
self.stereo
.iter()
.map(|frame| {
[
dequantize_sample(frame[0], self.bit_depth),
dequantize_sample(frame[1], self.bit_depth),
]
})
.collect()
}
/// Clone les données pour permettre une modification (Copy-on-Write)
/// Définit le gain (retourne un nouveau chunk avec le même Arc mais gain différent)
///
/// Cette méthode doit être appelée uniquement si vous avez besoin de modifier
/// les échantillons. Pour une simple lecture, utilisez [`frames`](Self::frames).
///
/// # Exemples
///
/// ```
/// use pmoaudio::{AudioChunk, BitDepth};
///
/// let chunk = AudioChunk::new(0, vec![[1, 2], [3, 4]], 48_000, BitDepth::B24);
/// let mut frames = chunk.clone_data();
/// frames[0][0] /= 2;
/// ```
pub fn clone_data(&self) -> Vec<[i32; 2]> {
self.stereo.to_vec()
}
pub fn set_data(&mut self, stereo: Vec<[i32; 2]>) {
self.stereo = Arc::from(stereo);
}
/// Applique le gain et retourne un nouveau chunk avec les données modifiées
///
/// Cette méthode crée un nouveau chunk avec les samples multipliés par le gain.
/// Utile pour les nodes qui doivent matérialiser le gain avant la sortie.
///
/// # Exemples
///
/// ```
/// use pmoaudio::{AudioChunk, BitDepth};
///
/// let chunk = AudioChunk::from_pairs_f32(
/// 0,
/// vec![[0.5, 0.25], [0.25, 0.125]],
/// 48_000,
/// BitDepth::B24,
/// );
/// let chunk = chunk.set_gain_linear(0.5);
/// let applied = chunk.apply_gain();
/// let frames = applied.to_pairs_f32();
///
/// assert!((frames[0][0] - 0.25).abs() < 1e-3);
/// assert!((applied.gain_db()).abs() < f64::EPSILON); // Gain réinitialisé après application
/// ```
pub fn apply_gain(self: Arc<Self>) -> Arc<Self> {
if self.gain.abs() < f64::EPSILON {
// Pas de gain à appliquer, retourner la même instance
return self;
}
let mut stereo = self.clone_data();
dsp::apply_gain_stereo(&mut stereo, self.gain);
Self::new(self.order, stereo, self.sample_rate, self.bit_depth)
}
pub fn set_gain_db(&self, gain: f64) -> Arc<Self> {
/// Cette méthode est très peu coûteuse car elle ne clone que la structure, pas les données audio.
pub fn set_gain_db(&self, gain_db: f64) -> Arc<Self> {
Arc::new(Self {
order: self.order,
stereo: self.stereo.clone(),
sample_rate: self.sample_rate,
bit_depth: self.bit_depth,
gain,
gain_db,
})
}
/// Définit le gain à l'aide d'un facteur linéaire (>0).
/// Définit le gain à l'aide d'un facteur linéaire (>0)
pub fn set_gain_linear(&self, gain_linear: f64) -> Arc<Self> {
self.set_gain_db(linear_to_db(gain_linear))
}
/// Modifie le gain de ce chunk (retourne un nouveau chunk avec le même Arc mais gain différent)
///
/// Cette méthode est très peu coûteuse car elle ne clone que la structure, pas les données audio.
/// Modifie le gain de ce chunk (ajoute un delta en dB)
pub fn with_modified_gain_db(&self, delta_gain_db: f64) -> Arc<Self> {
self.set_gain_db(self.gain + delta_gain_db)
self.set_gain_db(self.gain_db + delta_gain_db)
}
/// Modifie le gain via un facteur linéaire multiplié au gain courant.
/// Modifie le gain via un facteur linéaire multiplié au gain courant
pub fn with_modified_gain_linear(&self, gain_linear: f64) -> Arc<Self> {
self.with_modified_gain_db(linear_to_db(gain_linear))
}
}
pub fn get_bit_depth(&self) -> BitDepth {
self.bit_depth
// Méthodes spécifiques pour les types entiers (i8, i16, I24, i32)
impl AudioChunkData<i32> {
/// Applique le gain et retourne un nouveau chunk avec les données modifiées
///
/// Cette méthode crée un nouveau chunk avec les samples multipliés par le gain.
/// Le gain du chunk résultant est remis à 0.0 dB.
pub fn apply_gain(self: Arc<Self>) -> Arc<Self> {
if self.gain_db.abs() < f64::EPSILON {
return self; // Pas de gain à appliquer
}
let mut stereo = self.clone_frames();
dsp::apply_gain_stereo(&mut stereo, self.gain_db);
AudioChunkData::new(stereo, self.sample_rate, 0.0)
}
pub fn set_bit_depth(self: Arc<Self>, new_depth: BitDepth) -> Arc<Self> {
if self.bit_depth == new_depth {
/// Construit un chunk depuis deux vecteurs `i32` séparés (L/R)
pub fn from_channels(left: Vec<i32>, right: Vec<i32>, sample_rate: u32) -> Arc<Self> {
assert_eq!(left.len(), right.len(), "channels must have identical length");
let stereo = left
.into_iter()
.zip(right.into_iter())
.map(|(l, r)| [l, r])
.collect();
AudioChunkData::new(stereo, sample_rate, 0.0)
}
/// Change la profondeur de bits (bit depth conversion)
pub fn set_bit_depth(self: Arc<Self>, old_depth: BitDepth, new_depth: BitDepth) -> Arc<Self> {
if old_depth == new_depth {
return self;
}
let mut stereo = self.clone_data();
dsp::bitdepth_change_stereo(&mut stereo, self.bit_depth, new_depth);
let mut stereo = self.clone_frames();
dsp::bitdepth_change_stereo(&mut stereo, old_depth, new_depth);
Arc::new(Self {
order: self.order,
stereo: Arc::from(stereo),
sample_rate: self.sample_rate,
bit_depth: new_depth,
gain: self.gain,
gain_db: self.gain_db,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// Méthodes spécifiques pour f32
impl AudioChunkData<f32> {
/// Applique le gain et retourne un nouveau chunk avec les données modifiées
pub fn apply_gain(self: Arc<Self>) -> Arc<Self> {
if self.gain_db.abs() < f64::EPSILON {
return self; // Pas de gain à appliquer
}
#[test]
fn test_audio_chunk_creation() {
let stereo: Vec<[i32; 2]> = vec![
[0, 10], // frame 0 : L=0, R=10
[20, 30], // frame 1 : L=20, R=30
[40, 50], // frame 2 : L=40, R=50
];
let chunk = AudioChunk::new(0, stereo, 48000, BitDepth::B24);
let gain_linear = db_to_linear(self.gain_db) as f32;
let mut stereo = self.clone_frames();
for frame in &mut stereo {
frame[0] *= gain_linear;
frame[1] *= gain_linear;
}
assert_eq!(chunk.order(), 0);
assert_eq!(chunk.len(), 3);
assert_eq!(chunk.sample_rate(), 48000);
assert!(!chunk.is_empty());
AudioChunkData::new(stereo, self.sample_rate, 0.0)
}
/// Construit un chunk depuis deux vecteurs `f32` séparés (L/R)
pub fn from_channels(left: Vec<f32>, right: Vec<f32>, sample_rate: u32) -> Arc<Self> {
assert_eq!(left.len(), right.len(), "channels must have identical length");
let stereo = left
.into_iter()
.zip(right.into_iter())
.map(|(l, r)| [l, r])
.collect();
AudioChunkData::new(stereo, sample_rate, 0.0)
}
}
fn quantize_sample(sample: f32, bit_depth: BitDepth) -> i32 {
let max_value = bit_depth.max_value() as f64;
let upper = max_value - 1.0;
let lower = -max_value;
let scaled = (sample as f64 * upper).round();
scaled.clamp(lower, upper) as i32
// Méthodes spécifiques pour f64
impl AudioChunkData<f64> {
/// Applique le gain et retourne un nouveau chunk avec les données modifiées
pub fn apply_gain(self: Arc<Self>) -> Arc<Self> {
if self.gain_db.abs() < f64::EPSILON {
return self; // Pas de gain à appliquer
}
let gain_linear = db_to_linear(self.gain_db);
let mut stereo = self.clone_frames();
for frame in &mut stereo {
frame[0] *= gain_linear;
frame[1] *= gain_linear;
}
AudioChunkData::new(stereo, self.sample_rate, 0.0)
}
/// Construit un chunk depuis deux vecteurs `f64` séparés (L/R)
pub fn from_channels(left: Vec<f64>, right: Vec<f64>, sample_rate: u32) -> Arc<Self> {
assert_eq!(left.len(), right.len(), "channels must have identical length");
let stereo = left
.into_iter()
.zip(right.into_iter())
.map(|(l, r)| [l, r])
.collect();
AudioChunkData::new(stereo, sample_rate, 0.0)
}
}
fn dequantize_sample(sample: i32, bit_depth: BitDepth) -> f32 {
let max_value = bit_depth.max_value();
sample as f32 / max_value
// ============================================================================
// AudioChunk : Enum pour tous les types de chunks
// ============================================================================
/// Enum contenant tous les types de chunks audio possibles
///
/// Cette enum permet de manipuler des chunks de différents types dans un
/// pipeline unifié, tout en conservant l'information de type.
///
/// # Variantes
///
/// - `I8` : Échantillons 8-bit signés
/// - `I16` : Échantillons 16-bit signés
/// - `I24` : Échantillons 24-bit signés (stockés sur i32)
/// - `I32` : Échantillons 32-bit signés
/// - `F32` : Échantillons flottants 32-bit normalisés [-1.0, 1.0]
/// - `F64` : Échantillons flottants 64-bit normalisés [-1.0, 1.0]
///
/// # Exemples
///
/// ```
/// use pmoaudio::{AudioChunk, AudioChunkData};
///
/// let chunk_f32 = AudioChunkData::new(vec![[0.5f32, 0.25f32]; 1000], 48_000, 0.0);
/// let chunk = AudioChunk::F32(chunk_f32);
///
/// match &chunk {
/// AudioChunk::F32(data) => println!("F32 chunk with {} frames", data.len()),
/// _ => println!("Other type"),
/// }
/// ```
#[derive(Debug, Clone)]
pub enum AudioChunk {
I8(Arc<AudioChunkData<i8>>),
I16(Arc<AudioChunkData<i16>>),
I24(Arc<AudioChunkData<I24>>),
I32(Arc<AudioChunkData<i32>>),
F32(Arc<AudioChunkData<f32>>),
F64(Arc<AudioChunkData<f64>>),
}
impl AudioChunk {
/// Retourne le nombre de frames du chunk
pub fn len(&self) -> usize {
match self {
AudioChunk::I8(d) => d.len(),
AudioChunk::I16(d) => d.len(),
AudioChunk::I24(d) => d.len(),
AudioChunk::I32(d) => d.len(),
AudioChunk::F32(d) => d.len(),
AudioChunk::F64(d) => d.len(),
}
}
/// Vérifie si le chunk est vide
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Taux d'échantillonnage (Hz)
pub fn sample_rate(&self) -> u32 {
match self {
AudioChunk::I8(d) => d.sample_rate(),
AudioChunk::I16(d) => d.sample_rate(),
AudioChunk::I24(d) => d.sample_rate(),
AudioChunk::I32(d) => d.sample_rate(),
AudioChunk::F32(d) => d.sample_rate(),
AudioChunk::F64(d) => d.sample_rate(),
}
}
/// Gain courant en décibels
pub fn gain_db(&self) -> f64 {
match self {
AudioChunk::I8(d) => d.gain_db(),
AudioChunk::I16(d) => d.gain_db(),
AudioChunk::I24(d) => d.gain_db(),
AudioChunk::I32(d) => d.gain_db(),
AudioChunk::F32(d) => d.gain_db(),
AudioChunk::F64(d) => d.gain_db(),
}
}
/// Gain sous forme linéaire
pub fn gain_linear(&self) -> f64 {
db_to_linear(self.gain_db())
}
/// Définit le gain en dB
pub fn set_gain_db(&self, gain_db: f64) -> Self {
match self {
AudioChunk::I8(d) => AudioChunk::I8(d.set_gain_db(gain_db)),
AudioChunk::I16(d) => AudioChunk::I16(d.set_gain_db(gain_db)),
AudioChunk::I24(d) => AudioChunk::I24(d.set_gain_db(gain_db)),
AudioChunk::I32(d) => AudioChunk::I32(d.set_gain_db(gain_db)),
AudioChunk::F32(d) => AudioChunk::F32(d.set_gain_db(gain_db)),
AudioChunk::F64(d) => AudioChunk::F64(d.set_gain_db(gain_db)),
}
}
/// Définit le gain via un facteur linéaire
pub fn set_gain_linear(&self, gain_linear: f64) -> Self {
self.set_gain_db(linear_to_db(gain_linear))
}
/// Modifie le gain (ajoute un delta en dB)
pub fn with_modified_gain_db(&self, delta_gain_db: f64) -> Self {
self.set_gain_db(self.gain_db() + delta_gain_db)
}
/// Applique le gain et retourne un nouveau chunk avec les données modifiées
///
/// Le gain du chunk résultant est remis à 0.0 dB.
pub fn apply_gain(self) -> Self {
match self {
AudioChunk::I8(d) => {
// Pour i8, on convert en i32, applique gain, puis reconvertit
// TODO: optimiser avec une version directe
let gain_db = d.gain_db();
if gain_db.abs() < f64::EPSILON {
return AudioChunk::I8(d);
}
let gain_linear = db_to_linear(gain_db) as f32;
let mut stereo = d.clone_frames();
for frame in &mut stereo {
frame[0] = (frame[0] as f32 * gain_linear).round().clamp(-128.0, 127.0) as i8;
frame[1] = (frame[1] as f32 * gain_linear).round().clamp(-128.0, 127.0) as i8;
}
AudioChunk::I8(AudioChunkData::new(stereo, d.sample_rate(), 0.0))
}
AudioChunk::I16(d) => {
let gain_db = d.gain_db();
if gain_db.abs() < f64::EPSILON {
return AudioChunk::I16(d);
}
let gain_linear = db_to_linear(gain_db) as f32;
let mut stereo = d.clone_frames();
for frame in &mut stereo {
frame[0] = (frame[0] as f32 * gain_linear).round().clamp(-32768.0, 32767.0) as i16;
frame[1] = (frame[1] as f32 * gain_linear).round().clamp(-32768.0, 32767.0) as i16;
}
AudioChunk::I16(AudioChunkData::new(stereo, d.sample_rate(), 0.0))
}
AudioChunk::I24(d) => {
let gain_db = d.gain_db();
if gain_db.abs() < f64::EPSILON {
return AudioChunk::I24(d);
}
let gain_linear = db_to_linear(gain_db) as f32;
let mut stereo = d.clone_frames();
for frame in &mut stereo {
let l = (frame[0].as_i32() as f32 * gain_linear).round().clamp(-8_388_608.0, 8_388_607.0) as i32;
let r = (frame[1].as_i32() as f32 * gain_linear).round().clamp(-8_388_608.0, 8_388_607.0) as i32;
frame[0] = I24::new_clamped(l);
frame[1] = I24::new_clamped(r);
}
AudioChunk::I24(AudioChunkData::new(stereo, d.sample_rate(), 0.0))
}
AudioChunk::I32(d) => AudioChunk::I32(d.apply_gain()),
AudioChunk::F32(d) => AudioChunk::F32(d.apply_gain()),
AudioChunk::F64(d) => AudioChunk::F64(d.apply_gain()),
}
}
/// Retourne le nom du type de sample
pub fn type_name(&self) -> &'static str {
match self {
AudioChunk::I8(_) => "i8",
AudioChunk::I16(_) => "i16",
AudioChunk::I24(_) => "I24",
AudioChunk::I32(_) => "i32",
AudioChunk::F32(_) => "f32",
AudioChunk::F64(_) => "f64",
}
}
}
// ============================================================================
// Fonctions utilitaires de conversion gain
// ============================================================================
const MIN_GAIN_DB: f64 = -120.0;
fn linear_to_db(gain_linear: f64) -> f64 {
/// Convertit un gain linéaire (>0) en décibels
#[inline]
pub fn linear_to_db(gain_linear: f64) -> f64 {
if gain_linear <= 0.0 {
MIN_GAIN_DB
} else {
@@ -406,6 +458,67 @@ fn linear_to_db(gain_linear: f64) -> f64 {
}
}
fn db_to_linear(gain_db: f64) -> f64 {
/// Convertit un gain en décibels vers un gain linéaire
#[inline]
pub fn db_to_linear(gain_db: f64) -> f64 {
10f64.powf(gain_db / 20.0)
}
/// Convertit un gain linéaire en décibels (méthode publique pour compatibilité)
pub fn gain_db_from_linear(gain_linear: f64) -> f64 {
linear_to_db(gain_linear)
}
/// Convertit un gain en décibels vers un gain linéaire (méthode publique pour compatibilité)
pub fn gain_linear_from_db(gain_db: f64) -> f64 {
db_to_linear(gain_db)
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_chunk_data_f32() {
let stereo: Vec<[f32; 2]> = vec![[0.5, 0.25], [0.75, 0.125]];
let chunk = AudioChunkData::new(stereo, 48000, 0.0);
assert_eq!(chunk.len(), 2);
assert_eq!(chunk.sample_rate(), 48000);
assert!(!chunk.is_empty());
assert_eq!(chunk.gain_db(), 0.0);
}
#[test]
fn test_audio_chunk_data_i32() {
let stereo: Vec<[i32; 2]> = vec![[1000, 2000], [3000, 4000]];
let chunk = AudioChunkData::new(stereo, 48000, -6.0);
assert_eq!(chunk.len(), 2);
assert_eq!(chunk.gain_db(), -6.0);
}
#[test]
fn test_audio_chunk_enum() {
let data_f32 = AudioChunkData::new(vec![[0.5f32, 0.25f32]; 1000], 48000, 0.0);
let chunk = AudioChunk::F32(data_f32);
assert_eq!(chunk.len(), 1000);
assert_eq!(chunk.sample_rate(), 48000);
assert_eq!(chunk.type_name(), "f32");
}
#[test]
fn test_gain_conversion() {
let linear = 2.0;
let db = linear_to_db(linear);
assert!((db - 6.0206).abs() < 0.01); // 2x ≈ +6dB
let back = db_to_linear(db);
assert!((back - linear).abs() < 0.001);
}
}

View File

@@ -0,0 +1,526 @@
use std::sync::Arc;
use pmometadata::TrackMetadata;
use crate::{AudioChunk, AudioChunkData, BitDepth, SyncMarker, linear_to_db};
pub enum _AudioSegment {
Chunk(Arc<AudioChunk>),
Sync(Arc<SyncMarker>),
}
pub struct AudioSegment {
pub order: u64,
pub timestamp_sec: f64,
pub segment: _AudioSegment,
}
impl AudioSegment {
/// Crée un nouveau segment audio depuis des frames i32
pub fn new_chunk(
order: u64,
timestamp_sec: f64,
stereo: Vec<[i32; 2]>,
sample_rate: u32,
_bit_depth: BitDepth, // Conservé pour compatibilité API
) -> Arc<Self> {
let chunk_data = AudioChunkData::new(stereo, sample_rate, 0.0);
let chunk = AudioChunk::I32(chunk_data);
Arc::new(Self {
order,
timestamp_sec,
segment: _AudioSegment::Chunk(Arc::new(chunk)),
})
}
/// Crée un nouveau segment audio avec gain (dB)
pub fn new_chunk_with_gain_db(
order: u64,
timestamp_sec: f64,
stereo: Vec<[i32; 2]>,
sample_rate: u32,
_bit_depth: BitDepth, // Conservé pour compatibilité API
gain_db: f64,
) -> Arc<Self> {
let chunk_data = AudioChunkData::new(stereo, sample_rate, gain_db);
let chunk = AudioChunk::I32(chunk_data);
Arc::new(Self {
order,
timestamp_sec,
segment: _AudioSegment::Chunk(Arc::new(chunk)),
})
}
/// Crée un nouveau segment audio avec gain linéaire
pub fn new_chunk_with_gain_linear(
order: u64,
timestamp_sec: f64,
stereo: Vec<[i32; 2]>,
sample_rate: u32,
_bit_depth: BitDepth, // Conservé pour compatibilité API
gain_linear: f64,
) -> Arc<Self> {
let chunk_data = AudioChunkData::new(stereo, sample_rate, linear_to_db(gain_linear));
let chunk = AudioChunk::I32(chunk_data);
Arc::new(Self {
order,
timestamp_sec,
segment: _AudioSegment::Chunk(Arc::new(chunk)),
})
}
/// Crée un segment audio depuis deux canaux i32 séparés (L/R)
pub fn new_chunk_from_channels_i32(
order: u64,
timestamp_sec: f64,
left: Vec<i32>,
right: Vec<i32>,
sample_rate: u32,
_bit_depth: BitDepth, // Conservé pour compatibilité API
) -> Arc<Self> {
let chunk_data = AudioChunkData::<i32>::from_channels(left, right, sample_rate);
let chunk = AudioChunk::I32(chunk_data);
Arc::new(Self {
order,
timestamp_sec,
segment: _AudioSegment::Chunk(Arc::new(chunk)),
})
}
/// Crée un segment audio depuis deux canaux f32 normalisés (L/R)
///
/// Convertit f32 normalisé [-1.0, 1.0] → i32 selon le bit_depth spécifié
pub fn new_chunk_from_channels_f32(
order: u64,
timestamp_sec: f64,
left: Vec<f32>,
right: Vec<f32>,
sample_rate: u32,
bit_depth: BitDepth,
) -> Arc<Self> {
assert_eq!(left.len(), right.len(), "channels must have identical length");
// Convertir f32 → i32 selon le bit_depth
let max_value = bit_depth.max_value();
let stereo: Vec<[i32; 2]> = left
.into_iter()
.zip(right.into_iter())
.map(|(l, r)| {
let l_scaled = (l * max_value).clamp(-max_value, max_value - 1.0).round() as i32;
let r_scaled = (r * max_value).clamp(-max_value, max_value - 1.0).round() as i32;
[l_scaled, r_scaled]
})
.collect();
let chunk_data = AudioChunkData::new(stereo, sample_rate, 0.0);
let chunk = AudioChunk::I32(chunk_data);
Arc::new(Self {
order,
timestamp_sec,
segment: _AudioSegment::Chunk(Arc::new(chunk)),
})
}
/// Crée un segment audio depuis des frames f32 normalisées
///
/// Convertit f32 normalisé [-1.0, 1.0] → i32 selon le bit_depth spécifié
pub fn new_chunk_from_pairs_f32(
order: u64,
timestamp_sec: f64,
pairs: Vec<[f32; 2]>,
sample_rate: u32,
bit_depth: BitDepth,
) -> Arc<Self> {
// Convertir f32 → i32 selon le bit_depth
let max_value = bit_depth.max_value();
let stereo: Vec<[i32; 2]> = pairs
.into_iter()
.map(|[l, r]| {
let l_scaled = (l * max_value).clamp(-max_value, max_value - 1.0).round() as i32;
let r_scaled = (r * max_value).clamp(-max_value, max_value - 1.0).round() as i32;
[l_scaled, r_scaled]
})
.collect();
let chunk_data = AudioChunkData::new(stereo, sample_rate, 0.0);
let chunk = AudioChunk::I32(chunk_data);
Arc::new(Self {
order,
timestamp_sec,
segment: _AudioSegment::Chunk(Arc::new(chunk)),
})
}
pub fn new_track_boundary(
order: u64,
timestamp_sec: f64,
metadata: Arc<dyn TrackMetadata>) -> Arc<Self> {
let marker = Arc::new(SyncMarker::TrackBoundary {
metadata: Arc::clone(&metadata),
});
Arc::new(Self {
order,
timestamp_sec,
segment: _AudioSegment::Sync(marker),
})
}
pub fn new_stream_metadata(
order: u64,
timestamp_sec: f64,
key: String,
value: String
) -> Arc<Self> {
let marker = Arc::new(SyncMarker::StreamMetadata { key, value });
Arc::new(Self {
order,
timestamp_sec,
segment: _AudioSegment::Sync(marker),
})
}
pub fn new_top_zero_sync() -> Arc<Self> {
let marker = Arc::new(SyncMarker::TopZeroSync);
Arc::new(Self{
order: 0,
timestamp_sec: 0.0,
segment: _AudioSegment::Sync(marker)
})
}
pub fn new_hearbeat(
order: u64,
timestamp_sec: f64,
) -> Arc<Self> {
let marker = Arc::new(SyncMarker::Heartbeat);
Arc::new(Self{
order: order,
timestamp_sec: timestamp_sec,
segment: _AudioSegment::Sync(marker)
})
}
pub fn new_end_of_stream(
order: u64,
timestamp_sec: f64,
) -> Arc<Self> {
let marker = Arc::new(SyncMarker::EndOfStream);
Arc::new(Self{
order: order,
timestamp_sec: timestamp_sec,
segment: _AudioSegment::Sync(marker),
})
}
pub fn new_error(
order: u64,
timestamp_sec: f64,
error: String,
) -> Arc<Self> {
let marker = Arc::new(SyncMarker::Error(error));
Arc::new(Self{
order: order,
timestamp_sec: timestamp_sec,
segment: _AudioSegment::Sync(marker),
})
}
pub fn is_audio_chunk(&self) -> bool {
matches!(self.segment, _AudioSegment::Chunk(_))
}
pub fn is_track_boundary(&self) -> bool {
matches!(
self.segment,
_AudioSegment::Sync(ref marker)
if matches!(**marker,
SyncMarker::TrackBoundary { .. }
)
)
}
pub fn is_stream_metadata(&self) -> bool {
matches!(
self.segment,
_AudioSegment::Sync(ref marker)
if matches!(**marker,
SyncMarker::StreamMetadata { .. }
)
)
}
pub fn is_heartbeat(&self) -> bool {
matches!(
self.segment,
_AudioSegment::Sync(ref marker)
if matches!(**marker, SyncMarker::Heartbeat)
)
}
pub fn is_top_zero_sync(&self) -> bool {
matches!(
self.segment,
_AudioSegment::Sync(ref marker)
if matches!(**marker, SyncMarker::TopZeroSync)
)
}
pub fn is_end_of_stream(&self) -> bool {
matches!(
self.segment,
_AudioSegment::Sync(ref marker)
if matches!(**marker, SyncMarker::EndOfStream)
)
}
pub fn is_error(&self) -> bool {
matches!(
self.segment,
_AudioSegment::Sync(ref marker)
if matches!(**marker, SyncMarker::Error(_))
)
}
// ============ Accesseurs typés pour AudioChunk ============
/// Récupère le AudioChunk si ce segment est un chunk audio
pub fn as_chunk(&self) -> Option<&Arc<AudioChunk>> {
match &self.segment {
_AudioSegment::Chunk(chunk) => Some(chunk),
_ => None,
}
}
/// Récupère le SyncMarker si ce segment est un marqueur de sync
pub fn as_sync_marker(&self) -> Option<&Arc<SyncMarker>> {
match &self.segment {
_AudioSegment::Sync(marker) => Some(marker),
_ => None,
}
}
/// Récupère les métadatas du track si c'est un TrackBoundary
pub fn as_track_metadata(&self) -> Option<&Arc<dyn TrackMetadata>> {
match &self.segment {
_AudioSegment::Sync(marker) => match &**marker {
SyncMarker::TrackBoundary { metadata } => Some(metadata),
_ => None,
},
_ => None,
}
}
/// Récupère le message d'erreur si c'est un marqueur Error
pub fn as_error(&self) -> Option<&str> {
match &self.segment {
_AudioSegment::Sync(marker) => match &**marker {
SyncMarker::Error(msg) => Some(msg.as_str()),
_ => None,
},
_ => None,
}
}
/// Convertit l'AudioChunk vers F32 si c'est un chunk audio
pub fn to_f32_chunk(&self) -> Option<AudioChunk> {
self.as_chunk().map(|chunk| chunk.to_f32())
}
/// Convertit l'AudioChunk vers I32 si c'est un chunk audio
pub fn to_i32_chunk(&self) -> Option<AudioChunk> {
self.as_chunk().map(|chunk| chunk.to_i32())
}
/// Récupère le sample rate du chunk audio
pub fn sample_rate(&self) -> Option<u32> {
self.as_chunk().map(|chunk| chunk.sample_rate())
}
/// Récupère le nombre de frames du chunk audio
pub fn frame_count(&self) -> Option<usize> {
self.as_chunk().map(|chunk| chunk.len())
}
/// Récupère le gain en dB du chunk audio
pub fn gain_db(&self) -> Option<f64> {
self.as_chunk().map(|chunk| chunk.gain_db())
}
/// Récupère le type du chunk audio (nom du type: "i32", "f32", etc.)
pub fn chunk_type_name(&self) -> Option<&'static str> {
self.as_chunk().map(|chunk| chunk.type_name())
}
/// Crée un nouveau segment avec le gain modifié (si c'est un chunk audio)
pub fn with_gain_db(&self, gain_db: f64) -> Option<Arc<Self>> {
self.as_chunk().map(|chunk| {
let new_chunk = chunk.set_gain_db(gain_db);
Arc::new(Self {
order: self.order,
timestamp_sec: self.timestamp_sec,
segment: _AudioSegment::Chunk(Arc::new(new_chunk)),
})
})
}
/// Crée un nouveau segment avec le gain ajusté (relatif, si c'est un chunk audio)
pub fn adjust_gain_db(&self, delta_db: f64) -> Option<Arc<Self>> {
self.as_chunk().map(|chunk| {
let new_gain = chunk.gain_db() + delta_db;
let new_chunk = chunk.set_gain_db(new_gain);
Arc::new(Self {
order: self.order,
timestamp_sec: self.timestamp_sec,
segment: _AudioSegment::Chunk(Arc::new(new_chunk)),
})
})
}
}
impl TryInto<Arc<AudioChunk>> for AudioSegment {
type Error = ();
fn try_into(self) -> Result<Arc<AudioChunk>, Self::Error> {
match self.segment {
_AudioSegment::Chunk(chunk) => Ok(chunk),
_ => Err(()),
}
}
}
impl TryInto<Arc<SyncMarker>> for AudioSegment {
type Error = ();
fn try_into(self) -> Result<Arc<SyncMarker>, Self::Error> {
match self.segment {
_AudioSegment::Sync(marker) => Ok(marker),
_ => Err(()),
}
}
}
impl<'a> TryInto<&'a Arc<AudioChunk>> for &'a AudioSegment {
type Error = ();
fn try_into(self) -> Result<&'a Arc<AudioChunk>, Self::Error> {
match &self.segment {
_AudioSegment::Chunk(ref chunk) => Ok(chunk),
_ => Err(()),
}
}
}
impl<'a> TryInto<&'a Arc<SyncMarker>> for &'a AudioSegment {
type Error = ();
fn try_into(self) -> Result<&'a Arc<SyncMarker>, Self::Error> {
match &self.segment {
_AudioSegment::Sync(ref marker) => Ok(marker),
_ => Err(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_segment_accessors() {
// Test avec un chunk audio
let segment = AudioSegment::new_chunk(
42,
1.5,
vec![[100i32, 200i32], [300i32, 400i32]],
48000,
BitDepth::B32,
);
assert!(segment.is_audio_chunk());
assert!(!segment.is_heartbeat());
assert!(segment.as_chunk().is_some());
assert!(segment.as_sync_marker().is_none());
assert_eq!(segment.sample_rate(), Some(48000));
assert_eq!(segment.frame_count(), Some(2));
assert_eq!(segment.gain_db(), Some(0.0));
assert_eq!(segment.chunk_type_name(), Some("i32"));
// Test avec un marqueur sync
let sync_segment = AudioSegment::new_hearbeat(10, 2.0);
assert!(!sync_segment.is_audio_chunk());
assert!(sync_segment.is_heartbeat());
assert!(sync_segment.as_chunk().is_none());
assert!(sync_segment.as_sync_marker().is_some());
assert_eq!(sync_segment.sample_rate(), None);
}
#[test]
fn test_audio_segment_gain_manipulation() {
let segment = AudioSegment::new_chunk(
0,
0.0,
vec![[100i32, 200i32]],
44100,
BitDepth::B32,
);
// Test with_gain_db
let segment_6db = segment.with_gain_db(6.0).unwrap();
assert_eq!(segment_6db.gain_db(), Some(6.0));
assert_eq!(segment_6db.order, 0);
assert_eq!(segment_6db.timestamp_sec, 0.0);
// Test adjust_gain_db
let segment_plus_3db = segment_6db.adjust_gain_db(3.0).unwrap();
assert_eq!(segment_plus_3db.gain_db(), Some(9.0));
// Test sur un sync marker (devrait retourner None)
let sync = AudioSegment::new_hearbeat(1, 1.0);
assert!(sync.with_gain_db(6.0).is_none());
assert!(sync.adjust_gain_db(3.0).is_none());
}
#[test]
fn test_audio_segment_conversions() {
let segment = AudioSegment::new_chunk(
0,
0.0,
vec![[1000000i32, 2000000i32]],
44100,
BitDepth::B32,
);
// Test to_f32_chunk
let f32_chunk = segment.to_f32_chunk();
assert!(f32_chunk.is_some());
assert_eq!(f32_chunk.unwrap().type_name(), "f32");
// Test to_i32_chunk
let i32_chunk = segment.to_i32_chunk();
assert!(i32_chunk.is_some());
assert_eq!(i32_chunk.unwrap().type_name(), "i32");
// Test sur un sync marker
let sync = AudioSegment::new_hearbeat(1, 1.0);
assert!(sync.to_f32_chunk().is_none());
assert!(sync.to_i32_chunk().is_none());
}
#[test]
fn test_audio_segment_error_marker() {
let error_msg = "Test error message";
let segment = AudioSegment::new_error(5, 2.5, error_msg.to_string());
assert!(segment.is_error());
assert_eq!(segment.as_error(), Some(error_msg));
// Autre type de segment ne devrait pas être une erreur
let sync = AudioSegment::new_hearbeat(1, 1.0);
assert!(!sync.is_error());
assert_eq!(sync.as_error(), None);
}
}

932
pmoaudio/src/conversions.rs Normal file
View File

@@ -0,0 +1,932 @@
//! Conversions entre différents types de AudioChunk
//!
//! Ce module fournit des conversions optimisées (SIMD où possible) entre
//! tous les types de samples audio supportés.
use std::sync::Arc;
use crate::{dsp, AudioChunk, AudioChunkData, BitDepth, I24};
// ============================================================================
// Conversions int → int (changement de bit depth)
// ============================================================================
//
// Ces fonctions utilisent la fonction DSP optimisée SIMD `bitdepth_change_stereo`
// pour les conversions i32 ↔ i32 avec différents bit depths.
/// Convertit i32 vers i8 (downsampling via bit depth change)
pub fn convert_i32_to_i8(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<i8>> {
let mut stereo = chunk.clone_frames();
// Utiliser la fonction DSP optimisée pour passer de B32 → B8
dsp::bitdepth_change_stereo(&mut stereo, BitDepth::B32, BitDepth::B8);
// Convertir i32 → i8 (les valeurs sont maintenant dans la plage i8)
let stereo_i8: Vec<[i8; 2]> = stereo
.into_iter()
.map(|[l, r]| [l as i8, r as i8])
.collect();
AudioChunkData::new(stereo_i8, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i32 vers i16 (downsampling via bit depth change)
pub fn convert_i32_to_i16(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<i16>> {
let mut stereo = chunk.clone_frames();
// Utiliser la fonction DSP optimisée pour passer de B32 → B16
dsp::bitdepth_change_stereo(&mut stereo, BitDepth::B32, BitDepth::B16);
// Convertir i32 → i16 (les valeurs sont maintenant dans la plage i16)
let stereo_i16: Vec<[i16; 2]> = stereo
.into_iter()
.map(|[l, r]| [l as i16, r as i16])
.collect();
AudioChunkData::new(stereo_i16, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i32 vers I24 (downsampling via bit depth change)
pub fn convert_i32_to_i24(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<I24>> {
let mut stereo = chunk.clone_frames();
// Utiliser la fonction DSP optimisée pour passer de B32 → B24
dsp::bitdepth_change_stereo(&mut stereo, BitDepth::B32, BitDepth::B24);
// Convertir i32 → I24 (les valeurs sont maintenant dans la plage I24)
let stereo_i24: Vec<[I24; 2]> = stereo
.into_iter()
.map(|[l, r]| [I24::new_clamped(l), I24::new_clamped(r)])
.collect();
AudioChunkData::new(stereo_i24, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i8 vers i32 (upsampling via bit depth change)
pub fn convert_i8_to_i32(chunk: &AudioChunkData<i8>) -> Arc<AudioChunkData<i32>> {
// Convertir i8 → i32 d'abord
let mut stereo: Vec<[i32; 2]> = chunk
.frames()
.iter()
.map(|[l, r]| [*l as i32, *r as i32])
.collect();
// Utiliser la fonction DSP optimisée pour passer de B8 → B32
dsp::bitdepth_change_stereo(&mut stereo, BitDepth::B8, BitDepth::B32);
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i16 vers i32 (upsampling via bit depth change)
pub fn convert_i16_to_i32(chunk: &AudioChunkData<i16>) -> Arc<AudioChunkData<i32>> {
// Convertir i16 → i32 d'abord
let mut stereo: Vec<[i32; 2]> = chunk
.frames()
.iter()
.map(|[l, r]| [*l as i32, *r as i32])
.collect();
// Utiliser la fonction DSP optimisée pour passer de B16 → B32
dsp::bitdepth_change_stereo(&mut stereo, BitDepth::B16, BitDepth::B32);
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit I24 vers i32 (upsampling via bit depth change)
pub fn convert_i24_to_i32(chunk: &AudioChunkData<I24>) -> Arc<AudioChunkData<i32>> {
// Convertir I24 → i32 d'abord
let mut stereo: Vec<[i32; 2]> = chunk
.frames()
.iter()
.map(|[l, r]| [l.as_i32(), r.as_i32()])
.collect();
// Utiliser la fonction DSP optimisée pour passer de B24 → B32
dsp::bitdepth_change_stereo(&mut stereo, BitDepth::B24, BitDepth::B32);
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
// ============================================================================
// Conversions int → float (normalisation)
// ============================================================================
/// Convertit i32 vers f32 via les fonctions DSP optimisées SIMD
///
/// I32 = 32 bits complets, donc normalisation par 2^31
pub fn convert_i32_to_f32(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<f32>> {
let frames = chunk.frames();
let len = frames.len();
// Séparer les canaux pour utiliser les fonctions DSP SIMD
let mut left = Vec::with_capacity(len);
let mut right = Vec::with_capacity(len);
for [l, r] in frames {
left.push(*l);
right.push(*r);
}
// Utiliser la fonction SIMD optimisée du module DSP avec BitDepth::B32
let mut out_pairs = vec![[0.0f32; 2]; len];
dsp::i32_stereo_to_pairs_f32(&left, &right, &mut out_pairs, BitDepth::B32);
AudioChunkData::new(out_pairs, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i32 vers f64
///
/// I32 = 32 bits complets, donc normalisation par 2^31
pub fn convert_i32_to_f64(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<f64>> {
// Via f32 puis upcast
let f32_chunk = convert_i32_to_f32(chunk);
convert_f32_to_f64(&f32_chunk)
}
/// Convertit I24 vers f32
pub fn convert_i24_to_f32(chunk: &AudioChunkData<I24>) -> Arc<AudioChunkData<f32>> {
let frames = chunk.frames();
let max_value = 8_388_608.0f32; // 2^23
let stereo: Vec<[f32; 2]> = frames
.iter()
.map(|[l, r]| {
let lf = l.as_i32() as f32 / max_value;
let rf = r.as_i32() as f32 / max_value;
[lf, rf]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit I24 vers f64
pub fn convert_i24_to_f64(chunk: &AudioChunkData<I24>) -> Arc<AudioChunkData<f64>> {
let frames = chunk.frames();
let max_value = 8_388_608.0f64; // 2^23
let stereo: Vec<[f64; 2]> = frames
.iter()
.map(|[l, r]| {
let lf = l.as_i32() as f64 / max_value;
let rf = r.as_i32() as f64 / max_value;
[lf, rf]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i16 vers f32
pub fn convert_i16_to_f32(chunk: &AudioChunkData<i16>) -> Arc<AudioChunkData<f32>> {
let frames = chunk.frames();
let max_value = 32_768.0f32; // 2^15
let stereo: Vec<[f32; 2]> = frames
.iter()
.map(|[l, r]| {
let lf = *l as f32 / max_value;
let rf = *r as f32 / max_value;
[lf, rf]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i16 vers f64
pub fn convert_i16_to_f64(chunk: &AudioChunkData<i16>) -> Arc<AudioChunkData<f64>> {
let frames = chunk.frames();
let max_value = 32_768.0f64; // 2^15
let stereo: Vec<[f64; 2]> = frames
.iter()
.map(|[l, r]| {
let lf = *l as f64 / max_value;
let rf = *r as f64 / max_value;
[lf, rf]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i8 vers f32
pub fn convert_i8_to_f32(chunk: &AudioChunkData<i8>) -> Arc<AudioChunkData<f32>> {
let frames = chunk.frames();
let max_value = 128.0f32; // 2^7
let stereo: Vec<[f32; 2]> = frames
.iter()
.map(|[l, r]| {
let lf = *l as f32 / max_value;
let rf = *r as f32 / max_value;
[lf, rf]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i8 vers f64
pub fn convert_i8_to_f64(chunk: &AudioChunkData<i8>) -> Arc<AudioChunkData<f64>> {
let frames = chunk.frames();
let max_value = 128.0f64; // 2^7
let stereo: Vec<[f64; 2]> = frames
.iter()
.map(|[l, r]| {
let lf = *l as f64 / max_value;
let rf = *r as f64 / max_value;
[lf, rf]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
// ============================================================================
// Conversions float → int (quantization)
// ============================================================================
/// Convertit f32 vers i32 via les fonctions DSP optimisées SIMD
///
/// I32 = 32 bits complets, donc quantization vers ±2^31
pub fn convert_f32_to_i32(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<i32>> {
let frames = chunk.frames();
let len = frames.len();
// Utiliser la fonction SIMD optimisée du module DSP avec BitDepth::B32
let mut left = vec![0i32; len];
let mut right = vec![0i32; len];
dsp::pairs_f32_to_i32_stereo(frames, &mut left, &mut right, BitDepth::B32);
// Recombiner en frames
let stereo: Vec<[i32; 2]> = left
.into_iter()
.zip(right.into_iter())
.map(|(l, r)| [l, r])
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit f64 vers i32 (via f32)
///
/// I32 = 32 bits complets, donc quantization vers ±2^31
pub fn convert_f64_to_i32(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i32>> {
// Downcast f64 → f32 puis quantize
let f32_chunk = convert_f64_to_f32(chunk);
convert_f32_to_i32(&f32_chunk)
}
/// Convertit f32 vers I24
pub fn convert_f32_to_i24(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<I24>> {
let frames = chunk.frames();
let max_value = 8_388_607.0f32; // 2^23 - 1
let min_value = -8_388_608.0f32; // -2^23
let stereo: Vec<[I24; 2]> = frames
.iter()
.map(|[l, r]| {
let l_scaled = (l * max_value).clamp(min_value, max_value).round() as i32;
let r_scaled = (r * max_value).clamp(min_value, max_value).round() as i32;
[I24::new_clamped(l_scaled), I24::new_clamped(r_scaled)]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit f64 vers I24
pub fn convert_f64_to_i24(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<I24>> {
let frames = chunk.frames();
let max_value = 8_388_607.0f64; // 2^23 - 1
let min_value = -8_388_608.0f64; // -2^23
let stereo: Vec<[I24; 2]> = frames
.iter()
.map(|[l, r]| {
let l_scaled = (l * max_value).clamp(min_value, max_value).round() as i32;
let r_scaled = (r * max_value).clamp(min_value, max_value).round() as i32;
[I24::new_clamped(l_scaled), I24::new_clamped(r_scaled)]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit f32 vers i16
pub fn convert_f32_to_i16(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<i16>> {
let frames = chunk.frames();
let max_value = 32_767.0f32; // 2^15 - 1
let min_value = -32_768.0f32; // -2^15
let stereo: Vec<[i16; 2]> = frames
.iter()
.map(|[l, r]| {
let l16 = (l * max_value).clamp(min_value, max_value).round() as i16;
let r16 = (r * max_value).clamp(min_value, max_value).round() as i16;
[l16, r16]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit f64 vers i16
pub fn convert_f64_to_i16(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i16>> {
let frames = chunk.frames();
let max_value = 32_767.0f64; // 2^15 - 1
let min_value = -32_768.0f64; // -2^15
let stereo: Vec<[i16; 2]> = frames
.iter()
.map(|[l, r]| {
let l16 = (l * max_value).clamp(min_value, max_value).round() as i16;
let r16 = (r * max_value).clamp(min_value, max_value).round() as i16;
[l16, r16]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit f32 vers i8
pub fn convert_f32_to_i8(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<i8>> {
let frames = chunk.frames();
let max_value = 127.0f32; // 2^7 - 1
let min_value = -128.0f32; // -2^7
let stereo: Vec<[i8; 2]> = frames
.iter()
.map(|[l, r]| {
let l8 = (l * max_value).clamp(min_value, max_value).round() as i8;
let r8 = (r * max_value).clamp(min_value, max_value).round() as i8;
[l8, r8]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit f64 vers i8
pub fn convert_f64_to_i8(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i8>> {
let frames = chunk.frames();
let max_value = 127.0f64; // 2^7 - 1
let min_value = -128.0f64; // -2^7
let stereo: Vec<[i8; 2]> = frames
.iter()
.map(|[l, r]| {
let l8 = (l * max_value).clamp(min_value, max_value).round() as i8;
let r8 = (r * max_value).clamp(min_value, max_value).round() as i8;
[l8, r8]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
// ============================================================================
// Conversions F32 ↔ F64
// ============================================================================
/// Convertit f32 vers f64 (upcast simple)
pub fn convert_f32_to_f64(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<f64>> {
let frames = chunk.frames();
let stereo: Vec<[f64; 2]> = frames
.iter()
.map(|[l, r]| [*l as f64, *r as f64])
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit f64 vers f32 (downcast simple)
pub fn convert_f64_to_f32(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<f32>> {
let frames = chunk.frames();
let stereo: Vec<[f32; 2]> = frames
.iter()
.map(|[l, r]| [*l as f32, *r as f32])
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
// ============================================================================
// Méthodes de conversion sur AudioChunk enum
// ============================================================================
impl AudioChunk {
/// Convertit ce chunk vers f32
///
/// Chaque type utilise sa plage native (I8=±2^7, I16=±2^15, I24=±2^23, I32=±2^31)
pub fn to_f32(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => AudioChunk::F32(convert_i8_to_f32(d)),
AudioChunk::I16(d) => AudioChunk::F32(convert_i16_to_f32(d)),
AudioChunk::I24(d) => AudioChunk::F32(convert_i24_to_f32(d)),
AudioChunk::I32(d) => AudioChunk::F32(convert_i32_to_f32(d)),
AudioChunk::F32(d) => AudioChunk::F32(d.clone()),
AudioChunk::F64(d) => AudioChunk::F32(convert_f64_to_f32(d)),
}
}
/// Convertit ce chunk vers f64
///
/// Chaque type utilise sa plage native (I8=±2^7, I16=±2^15, I24=±2^23, I32=±2^31)
pub fn to_f64(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => AudioChunk::F64(convert_i8_to_f64(d)),
AudioChunk::I16(d) => AudioChunk::F64(convert_i16_to_f64(d)),
AudioChunk::I24(d) => AudioChunk::F64(convert_i24_to_f64(d)),
AudioChunk::I32(d) => AudioChunk::F64(convert_i32_to_f64(d)),
AudioChunk::F32(d) => AudioChunk::F64(convert_f32_to_f64(d)),
AudioChunk::F64(d) => AudioChunk::F64(d.clone()),
}
}
/// Convertit ce chunk vers i32
///
/// I32 = 32 bits complets (±2^31)
pub fn to_i32(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => AudioChunk::I32(convert_i8_to_i32(d)),
AudioChunk::I16(d) => AudioChunk::I32(convert_i16_to_i32(d)),
AudioChunk::I24(d) => AudioChunk::I32(convert_i24_to_i32(d)),
AudioChunk::I32(d) => AudioChunk::I32(d.clone()),
AudioChunk::F32(d) => AudioChunk::I32(convert_f32_to_i32(d)),
AudioChunk::F64(d) => AudioChunk::I32(convert_f64_to_i32(d)),
}
}
/// Convertit ce chunk vers I24
pub fn to_i24(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => {
// I8 → I32 → I24
let i32_chunk = convert_i8_to_i32(d);
AudioChunk::I24(convert_i32_to_i24(&i32_chunk))
}
AudioChunk::I16(d) => {
// I16 → I32 → I24
let i32_chunk = convert_i16_to_i32(d);
AudioChunk::I24(convert_i32_to_i24(&i32_chunk))
}
AudioChunk::I24(d) => AudioChunk::I24(d.clone()),
AudioChunk::I32(d) => AudioChunk::I24(convert_i32_to_i24(d)),
AudioChunk::F32(d) => AudioChunk::I24(convert_f32_to_i24(d)),
AudioChunk::F64(d) => AudioChunk::I24(convert_f64_to_i24(d)),
}
}
/// Convertit ce chunk vers i16
pub fn to_i16(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => {
// I8 → I32 → I16
let i32_chunk = convert_i8_to_i32(d);
AudioChunk::I16(convert_i32_to_i16(&i32_chunk))
}
AudioChunk::I16(d) => AudioChunk::I16(d.clone()),
AudioChunk::I24(d) => {
// I24 → I32 → I16
let i32_chunk = convert_i24_to_i32(d);
AudioChunk::I16(convert_i32_to_i16(&i32_chunk))
}
AudioChunk::I32(d) => AudioChunk::I16(convert_i32_to_i16(d)),
AudioChunk::F32(d) => AudioChunk::I16(convert_f32_to_i16(d)),
AudioChunk::F64(d) => AudioChunk::I16(convert_f64_to_i16(d)),
}
}
/// Convertit ce chunk vers i8
pub fn to_i8(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => AudioChunk::I8(d.clone()),
AudioChunk::I16(d) => {
// I16 → I32 → I8
let i32_chunk = convert_i16_to_i32(d);
AudioChunk::I8(convert_i32_to_i8(&i32_chunk))
}
AudioChunk::I24(d) => {
// I24 → I32 → I8
let i32_chunk = convert_i24_to_i32(d);
AudioChunk::I8(convert_i32_to_i8(&i32_chunk))
}
AudioChunk::I32(d) => AudioChunk::I8(convert_i32_to_i8(d)),
AudioChunk::F32(d) => AudioChunk::I8(convert_f32_to_i8(d)),
AudioChunk::F64(d) => AudioChunk::I8(convert_f64_to_i8(d)),
}
}
}
// ============================================================================
// Implémentations des traits From/Into
// ============================================================================
// ---------- From<Arc<AudioChunkData<T>>> pour AudioChunk ----------
impl From<Arc<AudioChunkData<i8>>> for AudioChunk {
fn from(data: Arc<AudioChunkData<i8>>) -> Self {
AudioChunk::I8(data)
}
}
impl From<Arc<AudioChunkData<i16>>> for AudioChunk {
fn from(data: Arc<AudioChunkData<i16>>) -> Self {
AudioChunk::I16(data)
}
}
impl From<Arc<AudioChunkData<I24>>> for AudioChunk {
fn from(data: Arc<AudioChunkData<I24>>) -> Self {
AudioChunk::I24(data)
}
}
impl From<Arc<AudioChunkData<i32>>> for AudioChunk {
fn from(data: Arc<AudioChunkData<i32>>) -> Self {
AudioChunk::I32(data)
}
}
impl From<Arc<AudioChunkData<f32>>> for AudioChunk {
fn from(data: Arc<AudioChunkData<f32>>) -> Self {
AudioChunk::F32(data)
}
}
impl From<Arc<AudioChunkData<f64>>> for AudioChunk {
fn from(data: Arc<AudioChunkData<f64>>) -> Self {
AudioChunk::F64(data)
}
}
// ---------- From entre AudioChunkData types (sans BitDepth requis) ----------
// I8 conversions
impl From<&AudioChunkData<i8>> for Arc<AudioChunkData<i32>> {
fn from(chunk: &AudioChunkData<i8>) -> Self {
convert_i8_to_i32(chunk)
}
}
impl From<&AudioChunkData<i8>> for Arc<AudioChunkData<f32>> {
fn from(chunk: &AudioChunkData<i8>) -> Self {
convert_i8_to_f32(chunk)
}
}
impl From<&AudioChunkData<i8>> for Arc<AudioChunkData<f64>> {
fn from(chunk: &AudioChunkData<i8>) -> Self {
convert_i8_to_f64(chunk)
}
}
// I16 conversions
impl From<&AudioChunkData<i16>> for Arc<AudioChunkData<i32>> {
fn from(chunk: &AudioChunkData<i16>) -> Self {
convert_i16_to_i32(chunk)
}
}
impl From<&AudioChunkData<i16>> for Arc<AudioChunkData<f32>> {
fn from(chunk: &AudioChunkData<i16>) -> Self {
convert_i16_to_f32(chunk)
}
}
impl From<&AudioChunkData<i16>> for Arc<AudioChunkData<f64>> {
fn from(chunk: &AudioChunkData<i16>) -> Self {
convert_i16_to_f64(chunk)
}
}
// I24 conversions
impl From<&AudioChunkData<I24>> for Arc<AudioChunkData<i32>> {
fn from(chunk: &AudioChunkData<I24>) -> Self {
convert_i24_to_i32(chunk)
}
}
impl From<&AudioChunkData<I24>> for Arc<AudioChunkData<f32>> {
fn from(chunk: &AudioChunkData<I24>) -> Self {
convert_i24_to_f32(chunk)
}
}
impl From<&AudioChunkData<I24>> for Arc<AudioChunkData<f64>> {
fn from(chunk: &AudioChunkData<I24>) -> Self {
convert_i24_to_f64(chunk)
}
}
// I32 conversions vers types int (downsampling)
impl From<&AudioChunkData<i32>> for Arc<AudioChunkData<i8>> {
fn from(chunk: &AudioChunkData<i32>) -> Self {
convert_i32_to_i8(chunk)
}
}
impl From<&AudioChunkData<i32>> for Arc<AudioChunkData<i16>> {
fn from(chunk: &AudioChunkData<i32>) -> Self {
convert_i32_to_i16(chunk)
}
}
impl From<&AudioChunkData<i32>> for Arc<AudioChunkData<I24>> {
fn from(chunk: &AudioChunkData<i32>) -> Self {
convert_i32_to_i24(chunk)
}
}
// I32 conversions vers float (normalisation par 2^31)
impl From<&AudioChunkData<i32>> for Arc<AudioChunkData<f32>> {
fn from(chunk: &AudioChunkData<i32>) -> Self {
convert_i32_to_f32(chunk)
}
}
impl From<&AudioChunkData<i32>> for Arc<AudioChunkData<f64>> {
fn from(chunk: &AudioChunkData<i32>) -> Self {
convert_i32_to_f64(chunk)
}
}
// F32 conversions
impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<f64>> {
fn from(chunk: &AudioChunkData<f32>) -> Self {
convert_f32_to_f64(chunk)
}
}
impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<i8>> {
fn from(chunk: &AudioChunkData<f32>) -> Self {
convert_f32_to_i8(chunk)
}
}
impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<i16>> {
fn from(chunk: &AudioChunkData<f32>) -> Self {
convert_f32_to_i16(chunk)
}
}
impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<I24>> {
fn from(chunk: &AudioChunkData<f32>) -> Self {
convert_f32_to_i24(chunk)
}
}
impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<i32>> {
fn from(chunk: &AudioChunkData<f32>) -> Self {
convert_f32_to_i32(chunk)
}
}
// F64 conversions
impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<f32>> {
fn from(chunk: &AudioChunkData<f64>) -> Self {
convert_f64_to_f32(chunk)
}
}
impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<i8>> {
fn from(chunk: &AudioChunkData<f64>) -> Self {
convert_f64_to_i8(chunk)
}
}
impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<i16>> {
fn from(chunk: &AudioChunkData<f64>) -> Self {
convert_f64_to_i16(chunk)
}
}
impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<I24>> {
fn from(chunk: &AudioChunkData<f64>) -> Self {
convert_f64_to_i24(chunk)
}
}
impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<i32>> {
fn from(chunk: &AudioChunkData<f64>) -> Self {
convert_f64_to_i32(chunk)
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_i32_to_f32_roundtrip() {
let stereo = vec![[1_000_000_000i32, 2_000_000_000i32]; 100];
let chunk_i32 = AudioChunkData::new(stereo.clone(), 48_000, 0.0);
let chunk_f32 = convert_i32_to_f32(&chunk_i32);
let chunk_back = convert_f32_to_i32(&chunk_f32);
// Vérifier que les valeurs sont proches (tolérance d'arrondi)
// Note: Pour I32 on utilise toute la plage ±2^31
for (orig, back) in stereo.iter().zip(chunk_back.frames().iter()) {
assert!((orig[0] - back[0]).abs() <= 100); // Tolérance plus élevée pour 32-bit
assert!((orig[1] - back[1]).abs() <= 100);
}
}
#[test]
fn test_f32_to_f64_roundtrip() {
let stereo = vec![[0.5f32, -0.25f32]; 100];
let chunk_f32 = AudioChunkData::new(stereo.clone(), 48_000, 0.0);
let chunk_f64 = convert_f32_to_f64(&chunk_f32);
let chunk_back = convert_f64_to_f32(&chunk_f64);
// Vérifier égalité exacte (pas de perte de précision significative)
for (orig, back) in stereo.iter().zip(chunk_back.frames().iter()) {
assert!((orig[0] - back[0]).abs() < 1e-6);
assert!((orig[1] - back[1]).abs() < 1e-6);
}
}
#[test]
fn test_i16_to_i32_upsampling() {
let stereo = vec![[16_000i16, -8_000i16]; 10];
let chunk_i16 = AudioChunkData::new(stereo.clone(), 48_000, 0.0);
let chunk_i32 = convert_i16_to_i32(&chunk_i16);
// Vérifier que les valeurs sont correctement upsamplées (shift de 16 bits)
for (orig, result) in stereo.iter().zip(chunk_i32.frames().iter()) {
assert_eq!(result[0], (orig[0] as i32) << 16);
assert_eq!(result[1], (orig[1] as i32) << 16);
}
}
#[test]
fn test_i32_to_i16_downsampling() {
let stereo = vec![[1_000_000i32 << 16, -500_000i32 << 16]; 10];
let chunk_i32 = AudioChunkData::new(stereo.clone(), 48_000, 0.0);
let chunk_i16 = convert_i32_to_i16(&chunk_i32);
// Vérifier que les valeurs sont correctement downsamplées
for (orig, result) in stereo.iter().zip(chunk_i16.frames().iter()) {
assert_eq!(result[0], (orig[0] >> 16) as i16);
assert_eq!(result[1], (orig[1] >> 16) as i16);
}
}
#[test]
fn test_i24_conversions() {
let stereo = vec![
[I24::new(1_000_000).unwrap(), I24::new(-500_000).unwrap()];
10
];
let chunk_i24 = AudioChunkData::new(stereo.clone(), 48_000, 0.0);
// I24 → F32 → I24
let chunk_f32 = convert_i24_to_f32(&chunk_i24);
let chunk_back = convert_f32_to_i24(&chunk_f32);
for (orig, back) in stereo.iter().zip(chunk_back.frames().iter()) {
assert!((orig[0].as_i32() - back[0].as_i32()).abs() <= 1);
assert!((orig[1].as_i32() - back[1].as_i32()).abs() <= 1);
}
}
#[test]
fn test_audio_chunk_enum_conversions() {
// Créer un chunk I32
let stereo = vec![[1_000_000_000i32, -500_000_000i32]; 100];
let chunk_data = AudioChunkData::new(stereo, 48_000, 0.0);
let chunk = AudioChunk::I32(chunk_data);
// Convertir vers F32 (I32 utilise plage complète ±2^31)
let chunk_f32 = chunk.to_f32();
assert_eq!(chunk_f32.type_name(), "f32");
// Convertir vers I24
let chunk_i24 = chunk.to_i24();
assert_eq!(chunk_i24.type_name(), "I24");
// Convertir vers I16
let chunk_i16 = chunk.to_i16();
assert_eq!(chunk_i16.type_name(), "i16");
}
#[test]
fn test_from_trait_audio_chunk() {
// Test From<Arc<AudioChunkData<T>>> pour AudioChunk
let stereo_f32 = vec![[0.5f32, -0.25f32]; 100];
let chunk_data = AudioChunkData::new(stereo_f32, 48_000, 0.0);
// Utiliser From/Into
let chunk: AudioChunk = chunk_data.into();
assert_eq!(chunk.type_name(), "f32");
assert_eq!(chunk.len(), 100);
}
#[test]
fn test_from_trait_conversions() {
// Test From entre AudioChunkData types
let stereo_i16 = vec![[16_000i16, -8_000i16]; 50];
let chunk_i16 = AudioChunkData::new(stereo_i16, 48_000, 0.0);
// I16 → I32 via From
let chunk_i32: Arc<AudioChunkData<i32>> = (&*chunk_i16).into();
assert_eq!(chunk_i32.len(), 50);
// I16 → F32 via From
let chunk_f32: Arc<AudioChunkData<f32>> = (&*chunk_i16).into();
assert_eq!(chunk_f32.len(), 50);
// I16 → F64 via From
let chunk_f64: Arc<AudioChunkData<f64>> = (&*chunk_i16).into();
assert_eq!(chunk_f64.len(), 50);
}
#[test]
fn test_from_trait_i24() {
// Test conversions I24 via From
let stereo_i24 = vec![
[I24::new(1_000_000).unwrap(), I24::new(-500_000).unwrap()];
50
];
let chunk_i24 = AudioChunkData::new(stereo_i24, 48_000, 0.0);
// I24 → I32 via From
let chunk_i32: Arc<AudioChunkData<i32>> = (&*chunk_i24).into();
assert_eq!(chunk_i32.len(), 50);
// I24 → F32 via From
let chunk_f32: Arc<AudioChunkData<f32>> = (&*chunk_i24).into();
assert_eq!(chunk_f32.len(), 50);
}
#[test]
fn test_from_trait_float_conversions() {
// Test conversions float via From
let stereo_f32 = vec![[0.5f32, -0.25f32]; 50];
let chunk_f32 = AudioChunkData::new(stereo_f32, 48_000, 0.0);
// F32 → F64 via From
let chunk_f64: Arc<AudioChunkData<f64>> = (&*chunk_f32).into();
assert_eq!(chunk_f64.len(), 50);
// F32 → I16 via From
let chunk_i16: Arc<AudioChunkData<i16>> = (&*chunk_f32).into();
assert_eq!(chunk_i16.len(), 50);
// F32 → I24 via From
let chunk_i24: Arc<AudioChunkData<I24>> = (&*chunk_f32).into();
assert_eq!(chunk_i24.len(), 50);
}
#[test]
fn test_from_trait_roundtrip() {
// Test round-trip I24 → F32 → I24 via From
let original = vec![
[I24::new(1_000_000).unwrap(), I24::new(-500_000).unwrap()];
10
];
let chunk_i24 = AudioChunkData::new(original.clone(), 48_000, 0.0);
// I24 → F32 via From
let chunk_f32: Arc<AudioChunkData<f32>> = (&*chunk_i24).into();
// F32 → I24 via From
let chunk_back: Arc<AudioChunkData<I24>> = (&*chunk_f32).into();
// Vérifier la précision
for (orig, back) in original.iter().zip(chunk_back.frames().iter()) {
assert!((orig[0].as_i32() - back[0].as_i32()).abs() <= 1);
assert!((orig[1].as_i32() - back[1].as_i32()).abs() <= 1);
}
}
#[test]
fn test_from_trait_i32_conversions() {
// Test conversions I32 via From (maintenant disponibles!)
let stereo_i32 = vec![[1_000_000_000i32, -500_000_000i32]; 50];
let chunk_i32 = AudioChunkData::new(stereo_i32, 48_000, 0.0);
// I32 → F32 via From (normalisation par 2^31)
let chunk_f32: Arc<AudioChunkData<f32>> = (&*chunk_i32).into();
assert_eq!(chunk_f32.len(), 50);
// I32 → F64 via From
let chunk_f64: Arc<AudioChunkData<f64>> = (&*chunk_i32).into();
assert_eq!(chunk_f64.len(), 50);
// F32 → I32 via From (quantization vers 2^31)
let chunk_back_i32: Arc<AudioChunkData<i32>> = (&*chunk_f32).into();
assert_eq!(chunk_back_i32.len(), 50);
}
}

View File

@@ -1,48 +1,20 @@
use bytemuck::{cast_slice, cast_slice_mut};
use crate::BitDepth;
#[cfg(feature = "simd")]
use std::simd::num::{SimdFloat, SimdInt};
#[cfg(feature = "simd")]
use std::simd::{Simd, StdFloat};
/// Génère une implémentation de `BitDepth` pour une profondeur donnée.
/// Exemple :
/// ```ignore
/// use pmoaudio::dsp::int_float::{BitDepth, BitMax};
///
/// BitMax!(8);
/// assert_eq!(<Bit8 as BitDepth>::MAX_VALUE, 127.0);
/// ```
macro_rules! BitMax {
($bits:literal) => {
paste::paste! {
pub struct [<Bit $bits>];
impl BitDepth for [<Bit $bits>] {
const MAX_VALUE: f32 = ((1u32 << ($bits - 1)) as f32) - 1.0;
}
}
};
}
pub trait BitDepth {
const MAX_VALUE: f32; // Valeur max pour normaliser vers [-1.0, +1.0]
}
// Définir automatiquement les bit-depths
BitMax!(8);
BitMax!(16);
BitMax!(24);
BitMax!(32);
/* ====================== CŒURS CANONIQUES EN AoS ====================== */
// i32 L/R -> [[f32;2]]
// i32 L/R -> [[f32;2]] - version interne avec constante compile-time
#[cfg(feature = "simd")]
pub fn i32_stereo_to_pairs_f32<B: BitDepth>(
fn i32_stereo_to_pairs_f32_inner(
left: &[i32],
right: &[i32],
out_pairs: &mut [[f32; 2]],
max_value: f32,
) {
debug_assert_eq!(left.len(), right.len());
debug_assert_eq!(out_pairs.len(), left.len());
@@ -51,7 +23,7 @@ pub fn i32_stereo_to_pairs_f32<B: BitDepth>(
type Vf32 = Simd<f32, LANES>;
type Vi32 = Simd<i32, LANES>;
let scale = Vf32::splat(1.0 / B::MAX_VALUE);
let scale = Vf32::splat(1.0 / max_value);
let (l_chunks, l_tail) = left.as_chunks::<LANES>();
let (r_chunks, r_tail) = right.as_chunks::<LANES>();
@@ -69,47 +41,59 @@ pub fn i32_stereo_to_pairs_f32<B: BitDepth>(
}
}
let scale_scalar = 1.0 / max_value;
for (dst, (&l, &r)) in o_tail.iter_mut().zip(l_tail.iter().zip(r_tail.iter())) {
dst[0] = l as f32 * (1.0 / B::MAX_VALUE);
dst[1] = r as f32 * (1.0 / B::MAX_VALUE);
dst[0] = l as f32 * scale_scalar;
dst[1] = r as f32 * scale_scalar;
}
}
#[cfg(not(feature = "simd"))]
pub fn i32_stereo_to_pairs_f32<B: BitDepth>(
fn i32_stereo_to_pairs_f32_inner(
left: &[i32],
right: &[i32],
out_pairs: &mut [[f32; 2]],
max_value: f32,
) {
debug_assert_eq!(left.len(), right.len());
debug_assert_eq!(out_pairs.len(), left.len());
let scale = 1.0 / B::MAX_VALUE;
let scale = 1.0 / max_value;
for ((out, &l), &r) in out_pairs.iter_mut().zip(left).zip(right) {
out[0] = l as f32 * scale;
out[1] = r as f32 * scale;
}
}
// [[f32;2]] -> i32 L/R
/// Convertit deux canaux i32 (L/R) en pairs f32 normalisées [-1.0, 1.0]
pub fn i32_stereo_to_pairs_f32(
left: &[i32],
right: &[i32],
out_pairs: &mut [[f32; 2]],
bit_depth: BitDepth,
) {
i32_stereo_to_pairs_f32_inner(left, right, out_pairs, bit_depth.max_value());
}
// [[f32;2]] -> i32 L/R - version interne
#[cfg(feature = "simd")]
pub fn pairs_f32_to_i32_stereo<B: BitDepth>(
fn pairs_f32_to_i32_stereo_inner(
input_pairs: &[[f32; 2]],
left: &mut [i32],
right: &mut [i32],
max_value: f32,
) {
debug_assert_eq!(input_pairs.len(), left.len());
debug_assert_eq!(input_pairs.len(), right.len());
const LANES: usize = 8;
type Vf32 = Simd<f32, LANES>;
type Vi32 = Simd<i32, LANES>;
let vmax = B::MAX_VALUE;
let vmin = -B::MAX_VALUE;
let vscale = Vf32::splat(vmax);
let vmin = -max_value;
let vmax_clamp = max_value - 1.0; // évite l'overflow après round→cast
let vscale = Vf32::splat(max_value);
let vminv = Vf32::splat(vmin);
let vmaxv = Vf32::splat(vmax - 1.0); // évite loverflow après round→cast
let vmaxv = Vf32::splat(vmax_clamp);
let (in_chunks, in_tail) = input_pairs.as_chunks::<LANES>();
let (l_chunks, l_tail) = left.as_chunks_mut::<LANES>();
@@ -137,52 +121,65 @@ pub fn pairs_f32_to_i32_stereo<B: BitDepth>(
}
for (j, (l, r)) in in_tail.iter().zip(l_tail.iter_mut().zip(r_tail.iter_mut())) {
let lx = (j[0] * vmax).clamp(vmin, vmax - 1.0).round();
let rx = (j[1] * vmax).clamp(vmin, vmax - 1.0).round();
let lx = (j[0] * max_value).clamp(vmin, vmax_clamp).round();
let rx = (j[1] * max_value).clamp(vmin, vmax_clamp).round();
*l = lx as i32;
*r = rx as i32;
}
}
#[cfg(not(feature = "simd"))]
pub fn pairs_f32_to_i32_stereo<B: BitDepth>(
fn pairs_f32_to_i32_stereo_inner(
input_pairs: &[[f32; 2]],
left: &mut [i32],
right: &mut [i32],
max_value: f32,
) {
debug_assert_eq!(input_pairs.len(), left.len());
debug_assert_eq!(input_pairs.len(), right.len());
let vmax = B::MAX_VALUE;
let vmin = -B::MAX_VALUE;
let vmin = -max_value;
let vmax_clamp = max_value - 1.0;
for (i, pair) in input_pairs.iter().enumerate() {
let lx = (pair[0] * vmax).clamp(vmin, vmax - 1.0).round();
let rx = (pair[1] * vmax).clamp(vmin, vmax - 1.0).round();
let lx = (pair[0] * max_value).clamp(vmin, vmax_clamp).round();
let rx = (pair[1] * max_value).clamp(vmin, vmax_clamp).round();
left[i] = lx as i32;
right[i] = rx as i32;
}
}
/// Convertit pairs f32 normalisées [-1.0, 1.0] en deux canaux i32 (L/R)
pub fn pairs_f32_to_i32_stereo(
input_pairs: &[[f32; 2]],
left: &mut [i32],
right: &mut [i32],
bit_depth: BitDepth,
) {
pairs_f32_to_i32_stereo_inner(input_pairs, left, right, bit_depth.max_value());
}
/* ====================== WRAPPERS INTERLEAVÉS ====================== */
// i32 L/R -> interleaved [f32]
pub fn i32_stereo_to_interleaved_f32<B: BitDepth>(
/// Convertit deux canaux i32 (L/R) en buffer f32 interleaved normalisé [-1.0, 1.0]
pub fn i32_stereo_to_interleaved_f32(
left: &[i32],
right: &[i32],
out_interleaved: &mut [f32],
bit_depth: BitDepth,
) {
debug_assert_eq!(out_interleaved.len(), left.len() * 2);
let out_pairs: &mut [[f32; 2]] = cast_slice_mut(out_interleaved);
i32_stereo_to_pairs_f32::<B>(left, right, out_pairs);
i32_stereo_to_pairs_f32(left, right, out_pairs, bit_depth);
}
// interleaved [f32] -> i32 L/R
pub fn interleaved_f32_to_i32_stereo<B: BitDepth>(
/// Convertit buffer f32 interleaved normalisé [-1.0, 1.0] en deux canaux i32 (L/R)
pub fn interleaved_f32_to_i32_stereo(
input_interleaved: &[f32],
left: &mut [i32],
right: &mut [i32],
bit_depth: BitDepth,
) {
debug_assert_eq!(input_interleaved.len(), left.len() * 2);
let input_pairs: &[[f32; 2]] = cast_slice(input_interleaved);
pairs_f32_to_i32_stereo::<B>(input_pairs, left, right);
pairs_f32_to_i32_stereo(input_pairs, left, right, bit_depth);
}

View File

@@ -1,3 +1,5 @@
//! Module DSP pour les conversions et traitements audio optimisés (SIMD)
pub mod depth;
pub mod gain;
pub mod int_float;

View File

@@ -2,41 +2,51 @@ use soxr::format::Stereo;
use soxr::params::{QualityRecipe, QualitySpec, RuntimeSpec};
use soxr::Soxr;
use crate::dsp::int_float::{Bit16, Bit24, Bit32, Bit8};
use crate::dsp::{i32_stereo_to_pairs_f32, pairs_f32_to_i32_stereo};
use crate::AudioError;
use crate::BitDepth;
// Type d'erreur simple pour resampling
#[derive(Debug)]
pub struct ResamplingError(pub String);
impl std::fmt::Display for ResamplingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Resampling error: {}", self.0)
}
}
impl std::error::Error for ResamplingError {}
pub struct Resampler {
source_hz: f64,
dest_hz: f64,
bit_depth: u32,
bit_depth: BitDepth,
soxr: Soxr<Stereo<f32>>,
}
pub fn build_resampler(
source_hz: u32,
dest_hz: u32,
bit_depth: u32,
) -> Result<Resampler, AudioError> {
bit_depth: BitDepth,
) -> Result<Resampler, ResamplingError> {
let qrecipe = match bit_depth {
8 => QualityRecipe::Medium,
16 => QualityRecipe::high(), // High plutôt que Bits16 pour 16-bit
24 => QualityRecipe::very_high(), // VeryHigh pour 24-bit
32 => QualityRecipe::very_high(), // VeryHigh pour 32-bit
_ => unreachable!(), // Déjà vérifié plus haut
BitDepth::B8 => QualityRecipe::Medium,
BitDepth::B16 => QualityRecipe::high(), // High pour 16-bit
BitDepth::B24 => QualityRecipe::very_high(), // VeryHigh pour 24-bit
BitDepth::B32 => QualityRecipe::very_high(), // VeryHigh pour 32-bit
};
let quality = QualitySpec::new(qrecipe); // Phase response linear, no steep filter
let rt = RuntimeSpec::default();
let soxr = Soxr::<Stereo<f32>>::new_with_params(source_hz as f64, dest_hz as f64, quality, rt)
.map_err(|e| AudioError::ProcessingError(e.to_string()))?;
.map_err(|e| ResamplingError(e.to_string()))?;
Ok(Resampler {
source_hz: source_hz as f64,
dest_hz: dest_hz as f64,
bit_depth: bit_depth,
soxr: soxr,
bit_depth,
soxr,
})
}
@@ -44,31 +54,22 @@ pub fn resampling(left: &[i32], right: &[i32], resampler: &mut Resampler) -> (Ve
if left.len() != right.len() {
panic!("Left and right channels must have the same length");
}
let mut input = vec![[0.0f32; 2]; left.len()];
match resampler.bit_depth {
8 => i32_stereo_to_pairs_f32::<Bit8>(left, right, &mut input),
16 => i32_stereo_to_pairs_f32::<Bit16>(left, right, &mut input),
24 => i32_stereo_to_pairs_f32::<Bit24>(left, right, &mut input),
32 => i32_stereo_to_pairs_f32::<Bit32>(left, right, &mut input),
_ => panic!("Unsupported bit depth: {}", resampler.bit_depth),
}
// Convertir i32 → f32 normalisé
let mut input = vec![[0.0f32; 2]; left.len()];
i32_stereo_to_pairs_f32(left, right, &mut input, resampler.bit_depth);
// Resampling
let output_len =
((input.len() as f64) * resampler.dest_hz / resampler.source_hz).ceil() as usize;
let mut output = vec![[0.0f32; 2]; output_len];
resampler.soxr.process(&input, &mut output).unwrap();
// Convertir f32 normalisé → i32
let mut oleft = vec![0i32; output.len()];
let mut oright = vec![0i32; output.len()];
match resampler.bit_depth {
8 => pairs_f32_to_i32_stereo::<Bit8>(&output, &mut oleft, &mut oright),
16 => pairs_f32_to_i32_stereo::<Bit16>(&output, &mut oleft, &mut oright),
24 => pairs_f32_to_i32_stereo::<Bit24>(&output, &mut oleft, &mut oright),
32 => pairs_f32_to_i32_stereo::<Bit32>(&output, &mut oleft, &mut oright),
_ => unreachable!(), // Déjà vérifié plus haut
};
pairs_f32_to_i32_stereo(&output, &mut oleft, &mut oright, resampler.bit_depth);
(oleft, oright)
}

View File

@@ -82,18 +82,32 @@ use std::simd::*;
mod audio_chunk;
pub mod events;
mod nodes;
// mod nodes; // Temporairement déplacé hors du module
mod sync_marker;
mod audio_segment;
mod sample_types;
pub mod conversions;
#[macro_use]
mod macros;
pub mod bit_depth;
pub mod dsp;
pub use audio_chunk::AudioChunk;
pub use audio_segment::{AudioSegment, _AudioSegment};
pub use sync_marker::{SyncMarker};
pub use audio_chunk::{AudioChunk, AudioChunkData, db_to_linear, gain_db_from_linear, gain_linear_from_db, linear_to_db};
pub use bit_depth::{Bit16, Bit24, Bit32, Bit8, BitDepth};
pub use sample_types::{I24, Sample};
pub use events::{
AudioDataEvent, EventPublisher, EventReceiver, NodeEvent, NodeListener, SourceNameUpdateEvent,
VolumeChangeEvent,
};
// Nodes temporairement désactivés
/*
pub use nodes::{
buffer_node::BufferNode,
chromecast_sink::{ChromecastConfig, ChromecastSink, ChromecastStats, StreamEncoding},
@@ -109,3 +123,4 @@ pub use nodes::{
volume_node::{HardwareVolumeNode, VolumeHandle, VolumeNode},
AudioError, AudioNode, MultiSubscriberNode, SingleSubscriberNode,
};
*/

304
pmoaudio/src/macros.rs Normal file
View File

@@ -0,0 +1,304 @@
/// Macros pour simplifier la manipulation des AudioChunk et AudioSegment
/// Extrait les données typées d'un AudioChunk
///
/// # Exemples
/// ```
/// use pmoaudio::{AudioChunk, AudioChunkData, extract_chunk_data};
///
/// fn process_i32(chunk: &AudioChunk) {
/// if let Some(data) = extract_chunk_data!(chunk, I32) {
/// println!("I32 chunk with {} frames", data.len());
/// }
/// }
/// ```
#[macro_export]
macro_rules! extract_chunk_data {
($chunk:expr, I8) => {
match $chunk {
$crate::AudioChunk::I8(data) => Some(data),
_ => None,
}
};
($chunk:expr, I16) => {
match $chunk {
$crate::AudioChunk::I16(data) => Some(data),
_ => None,
}
};
($chunk:expr, I24) => {
match $chunk {
$crate::AudioChunk::I24(data) => Some(data),
_ => None,
}
};
($chunk:expr, I32) => {
match $chunk {
$crate::AudioChunk::I32(data) => Some(data),
_ => None,
}
};
($chunk:expr, F32) => {
match $chunk {
$crate::AudioChunk::F32(data) => Some(data),
_ => None,
}
};
($chunk:expr, F64) => {
match $chunk {
$crate::AudioChunk::F64(data) => Some(data),
_ => None,
}
};
}
/// Match sur le type d'un AudioChunk avec exécution de code pour chaque cas
///
/// # Exemples
/// ```
/// use pmoaudio::{AudioChunk, match_chunk};
///
/// fn print_chunk_info(chunk: &AudioChunk) {
/// match_chunk!(chunk, data => {
/// println!("Chunk type: {}, frames: {}", chunk.type_name(), data.len());
/// });
/// }
/// ```
#[macro_export]
macro_rules! match_chunk {
($chunk:expr, $data:ident => $body:expr) => {
match $chunk {
$crate::AudioChunk::I8($data) => $body,
$crate::AudioChunk::I16($data) => $body,
$crate::AudioChunk::I24($data) => $body,
$crate::AudioChunk::I32($data) => $body,
$crate::AudioChunk::F32($data) => $body,
$crate::AudioChunk::F64($data) => $body,
}
};
}
/// Map sur un AudioChunk - transforme les données et retourne un nouveau AudioChunk du même type
///
/// # Exemples
/// ```
/// use pmoaudio::{AudioChunk, map_chunk};
///
/// fn add_gain_db(chunk: &AudioChunk, gain_db: f64) -> AudioChunk {
/// map_chunk!(chunk, data => {
/// data.set_gain_db(data.gain_db() + gain_db)
/// })
/// }
/// ```
#[macro_export]
macro_rules! map_chunk {
($chunk:expr, $data:ident => $transform:expr) => {
match $chunk {
$crate::AudioChunk::I8($data) => {
$crate::AudioChunk::I8($transform)
}
$crate::AudioChunk::I16($data) => {
$crate::AudioChunk::I16($transform)
}
$crate::AudioChunk::I24($data) => {
$crate::AudioChunk::I24($transform)
}
$crate::AudioChunk::I32($data) => {
$crate::AudioChunk::I32($transform)
}
$crate::AudioChunk::F32($data) => {
$crate::AudioChunk::F32($transform)
}
$crate::AudioChunk::F64($data) => {
$crate::AudioChunk::F64($transform)
}
}
};
}
/// Prédicat sur le type d'un AudioChunk
///
/// # Exemples
/// ```
/// use pmoaudio::{AudioChunk, is_chunk_type};
///
/// fn process_only_i32(chunk: &AudioChunk) {
/// if is_chunk_type!(chunk, I32) {
/// println!("Processing I32 chunk");
/// }
/// }
/// ```
#[macro_export]
macro_rules! is_chunk_type {
($chunk:expr, I8) => {
matches!($chunk, $crate::AudioChunk::I8(_))
};
($chunk:expr, I16) => {
matches!($chunk, $crate::AudioChunk::I16(_))
};
($chunk:expr, I24) => {
matches!($chunk, $crate::AudioChunk::I24(_))
};
($chunk:expr, I32) => {
matches!($chunk, $crate::AudioChunk::I32(_))
};
($chunk:expr, F32) => {
matches!($chunk, $crate::AudioChunk::F32(_))
};
($chunk:expr, F64) => {
matches!($chunk, $crate::AudioChunk::F64(_))
};
}
/// Extrait un AudioChunk d'un AudioSegment
///
/// # Exemples
/// ```
/// use pmoaudio::{AudioSegment, extract_audio_chunk};
///
/// fn get_chunk(segment: &AudioSegment) -> Option<&Arc<AudioChunk>> {
/// extract_audio_chunk!(segment)
/// }
/// ```
#[macro_export]
macro_rules! extract_audio_chunk {
($segment:expr) => {
match &$segment.segment {
$crate::_AudioSegment::Chunk(chunk) => Some(chunk),
_ => None,
}
};
}
/// Extrait un SyncMarker d'un AudioSegment
///
/// # Exemples
/// ```
/// use pmoaudio::{AudioSegment, extract_sync_marker};
///
/// fn get_marker(segment: &AudioSegment) -> Option<&Arc<SyncMarker>> {
/// extract_sync_marker!(segment)
/// }
/// ```
#[macro_export]
macro_rules! extract_sync_marker {
($segment:expr) => {
match &$segment.segment {
$crate::_AudioSegment::Sync(marker) => Some(marker),
_ => None,
}
};
}
/// Match sur le contenu d'un AudioSegment
///
/// # Exemples
/// ```
/// use pmoaudio::{AudioSegment, match_segment};
///
/// fn process_segment(segment: &AudioSegment) {
/// match_segment!(segment,
/// chunk => println!("Audio chunk: {}", chunk.type_name()),
/// marker => println!("Sync marker")
/// );
/// }
/// ```
#[macro_export]
macro_rules! match_segment {
($segment:expr, $chunk_name:ident => $chunk_body:expr, $marker_name:ident => $marker_body:expr) => {
match &$segment.segment {
$crate::_AudioSegment::Chunk($chunk_name) => $chunk_body,
$crate::_AudioSegment::Sync($marker_name) => $marker_body,
}
};
}
#[cfg(test)]
mod tests {
use crate::{AudioChunk, AudioChunkData, AudioSegment, BitDepth};
#[test]
fn test_extract_chunk_data() {
let data = AudioChunkData::new(vec![[100i32, 200i32]], 44100, 0.0);
let chunk = AudioChunk::I32(data.clone());
// Test extraction réussie
assert!(extract_chunk_data!(&chunk, I32).is_some());
assert!(extract_chunk_data!(&chunk, F32).is_none());
// Test avec F32
let f32_chunk = AudioChunk::F32(AudioChunkData::new(vec![[0.5f32, -0.5f32]], 44100, 0.0));
assert!(extract_chunk_data!(&f32_chunk, F32).is_some());
assert!(extract_chunk_data!(&f32_chunk, I32).is_none());
}
#[test]
fn test_match_chunk() {
let chunk = AudioChunk::I32(AudioChunkData::new(vec![[100i32, 200i32]], 44100, 0.0));
let len = match_chunk!(&chunk, data => data.len());
assert_eq!(len, 1);
let sample_rate = match_chunk!(&chunk, data => data.sample_rate());
assert_eq!(sample_rate, 44100);
}
#[test]
fn test_map_chunk() {
let chunk = AudioChunk::I32(AudioChunkData::new(vec![[100i32, 200i32]], 44100, 0.0));
let modified = map_chunk!(&chunk, data => data.set_gain_db(6.0));
match_chunk!(&modified, data => {
assert_eq!(data.gain_db(), 6.0);
});
}
#[test]
fn test_is_chunk_type() {
let i32_chunk = AudioChunk::I32(AudioChunkData::new(vec![[100i32, 200i32]], 44100, 0.0));
let f32_chunk = AudioChunk::F32(AudioChunkData::new(vec![[0.5f32, -0.5f32]], 44100, 0.0));
assert!(is_chunk_type!(&i32_chunk, I32));
assert!(!is_chunk_type!(&i32_chunk, F32));
assert!(is_chunk_type!(&f32_chunk, F32));
assert!(!is_chunk_type!(&f32_chunk, I32));
}
#[test]
fn test_extract_audio_chunk() {
let segment = AudioSegment::new_chunk(0, 0.0, vec![[100i32, 200i32]], 44100, BitDepth::B32);
assert!(extract_audio_chunk!(&*segment).is_some());
let sync_segment = AudioSegment::new_hearbeat(1, 1.0);
assert!(extract_audio_chunk!(&*sync_segment).is_none());
}
#[test]
fn test_extract_sync_marker() {
let segment = AudioSegment::new_hearbeat(1, 1.0);
assert!(extract_sync_marker!(&*segment).is_some());
let audio_segment = AudioSegment::new_chunk(0, 0.0, vec![[100i32, 200i32]], 44100, BitDepth::B32);
assert!(extract_sync_marker!(&*audio_segment).is_none());
}
#[test]
fn test_match_segment() {
let audio_segment = AudioSegment::new_chunk(0, 0.0, vec![[100i32, 200i32]], 44100, BitDepth::B32);
let result = match_segment!(&*audio_segment,
chunk => format!("audio: {}", chunk.type_name()),
_marker => "sync".to_string()
);
assert_eq!(result, "audio: i32");
let sync_segment = AudioSegment::new_hearbeat(1, 1.0);
let result = match_segment!(&*sync_segment,
_chunk => "audio".to_string(),
_marker => "sync".to_string()
);
assert_eq!(result, "sync");
}
}

View File

@@ -1,245 +0,0 @@
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
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::*;
use crate::BitDepth;
#[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![[0i32; 2]; 100], 48000, BitDepth::B24);
tx.send(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![[0i32; 2]; 100], 48000, BitDepth::B24);
tx.send(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);
}
}

View File

@@ -1,290 +0,0 @@
//! ChromecastSink - Diffuse le flux audio vers un périphérique Chromecast
//!
//! Ce module fournit un sink qui envoie le flux audio à un Chromecast.
//! Note: Cette implémentation est une version mock/skeleton. Une vraie implémentation
//! nécessiterait une bibliothèque comme `rust-cast` ou similaire.
use crate::{nodes::AudioError, AudioChunk};
use std::sync::Arc;
use tokio::sync::mpsc;
/// Configuration pour le ChromecastSink
#[derive(Debug, Clone)]
pub struct ChromecastConfig {
/// Nom ou adresse IP du Chromecast
pub device_address: String,
/// Nom amical du device
pub device_name: String,
/// Port de communication (défaut: 8009)
pub port: u16,
/// Taille du buffer de streaming
pub buffer_size: usize,
/// Format d'encodage pour le streaming
pub encoding: StreamEncoding,
}
impl Default for ChromecastConfig {
fn default() -> Self {
Self {
device_address: "192.168.1.100".to_string(),
device_name: "Living Room".to_string(),
port: 8009,
buffer_size: 50,
encoding: StreamEncoding::Mp3,
}
}
}
/// Formats d'encodage supportés pour le streaming
#[derive(Debug, Clone, Copy)]
pub enum StreamEncoding {
/// MP3 (compatible avec la plupart des Chromecasts)
Mp3,
/// AAC
Aac,
/// Opus
Opus,
/// PCM non compressé (haute qualité, bande passante élevée)
Pcm,
}
/// ChromecastSink - Diffuse vers un périphérique Chromecast
///
/// Ce sink encode le flux audio et le streame vers un Chromecast.
/// La connexion est établie lors de l'initialisation et maintenue pendant toute la durée.
///
/// # Implémentation actuelle
///
/// Cette version est un mock qui simule l'envoi au Chromecast.
/// Pour une vraie implémentation, il faudrait:
/// - Utiliser une bibliothèque comme `rust-cast`
/// - Établir une connexion TLS avec le device
/// - Lancer une application de récepteur sur le Chromecast
/// - Encoder l'audio dans le format approprié
/// - Streamer via HTTP ou WebSocket
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::{ChromecastSink, ChromecastConfig};
///
/// #[tokio::main]
/// async fn main() {
/// let config = ChromecastConfig {
/// device_address: "192.168.1.100".to_string(),
/// device_name: "Living Room".to_string(),
/// ..Default::default()
/// };
///
/// let (sink, sink_tx) = ChromecastSink::new("chromecast1".to_string(), config, 10);
///
/// tokio::spawn(async move {
/// sink.run().await.unwrap()
/// });
/// }
/// ```
pub struct ChromecastSink {
/// Identifiant du sink
node_id: String,
/// Channel pour recevoir les chunks audio
rx: mpsc::Receiver<Arc<AudioChunk>>,
/// Configuration
config: ChromecastConfig,
/// État de la connexion (mock)
connected: bool,
}
impl ChromecastSink {
/// Crée un nouveau ChromecastSink
///
/// # Arguments
///
/// * `node_id` - Identifiant unique du sink
/// * `config` - Configuration du Chromecast
/// * `channel_size` - Taille du buffer du channel
pub fn new(
node_id: String,
config: ChromecastConfig,
channel_size: usize,
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
let (tx, rx) = mpsc::channel(channel_size);
let sink = Self {
node_id,
rx,
config,
connected: false,
};
(sink, tx)
}
/// Établit la connexion avec le Chromecast (mock)
async fn connect(&mut self) -> Result<(), AudioError> {
println!(
"[{}] Connecting to Chromecast '{}' at {}:{}...",
self.node_id, self.config.device_name, self.config.device_address, self.config.port
);
// Simuler une connexion
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
self.connected = true;
println!(
"[{}] Connected to Chromecast '{}' successfully",
self.node_id, self.config.device_name
);
Ok(())
}
/// Envoie un chunk au Chromecast (mock)
async fn send_chunk(&self, _chunk: &AudioChunk) -> Result<(), AudioError> {
if !self.connected {
return Err(AudioError::ProcessingError(
"Not connected to Chromecast".to_string(),
));
}
// Dans une vraie implémentation:
// 1. Appliquer le gain
// 2. Encoder dans le format approprié (MP3, AAC, etc.)
// 3. Envoyer via le protocole Chromecast
// Pour l'instant, simplement simuler un délai d'envoi
tokio::time::sleep(tokio::time::Duration::from_micros(50)).await;
Ok(())
}
/// Déconnecte proprement du Chromecast (mock)
async fn disconnect(&mut self) -> Result<(), AudioError> {
if self.connected {
println!(
"[{}] Disconnecting from Chromecast '{}'...",
self.node_id, self.config.device_name
);
// Simuler la déconnexion
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
self.connected = false;
println!("[{}] Disconnected successfully", self.node_id);
}
Ok(())
}
/// Démarre la boucle de traitement du ChromecastSink
pub async fn run(mut self) -> Result<ChromecastStats, AudioError> {
// Établir la connexion
self.connect().await?;
let mut stats = ChromecastStats::new(self.node_id.clone(), self.config.device_name.clone());
// Boucle principale
while let Some(chunk) = self.rx.recv().await {
// Appliquer le gain si nécessaire
let chunk_to_send = if chunk.gain_db().abs() > f64::EPSILON {
Arc::clone(&chunk).apply_gain()
} else {
Arc::clone(&chunk)
};
// Envoyer au Chromecast
self.send_chunk(&chunk_to_send).await?;
stats.record_chunk(&chunk_to_send);
}
// Déconnexion propre
self.disconnect().await?;
stats.finalize();
Ok(stats)
}
}
/// Statistiques du ChromecastSink
#[derive(Debug, Clone)]
pub struct ChromecastStats {
pub node_id: String,
pub device_name: String,
pub chunks_sent: u64,
pub total_samples: u64,
pub total_duration_sec: f64,
}
impl ChromecastStats {
pub fn new(node_id: String, device_name: String) -> Self {
Self {
node_id,
device_name,
chunks_sent: 0,
total_samples: 0,
total_duration_sec: 0.0,
}
}
pub fn record_chunk(&mut self, chunk: &AudioChunk) {
self.chunks_sent += 1;
self.total_samples += chunk.len() as u64;
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
}
pub fn finalize(&mut self) {
// Calculs finaux si nécessaire
}
pub fn display(&self) {
println!("\n=== Chromecast Statistics: {} ===", self.node_id);
println!("Device: {}", self.device_name);
println!("Chunks sent: {}", self.chunks_sent);
println!("Total samples: {}", self.total_samples);
println!("Total duration: {:.3} sec", self.total_duration_sec);
println!("==================================\n");
}
}
#[cfg(test)]
mod tests {
use std::i32;
use super::*;
use crate::BitDepth;
#[tokio::test]
async fn test_chromecast_sink_basic() {
let config = ChromecastConfig {
device_address: "127.0.0.1".to_string(),
device_name: "Test Device".to_string(),
..Default::default()
};
let (sink, tx) = ChromecastSink::new("test".to_string(), config, 10);
let handle = tokio::spawn(async move { sink.run().await });
// Envoyer quelques chunks
for i in 0..5 {
let stereo = vec![[i32::MAX / 2; 2]; 1000];
let chunk = AudioChunk::new(i, stereo, 48000, BitDepth::B24);
tx.send(chunk).await.unwrap();
}
drop(tx);
let stats = handle.await.unwrap().unwrap();
assert_eq!(stats.chunks_sent, 5);
assert_eq!(stats.device_name, "Test Device");
}
}

View File

@@ -1,162 +0,0 @@
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
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 pairs = chunk.to_pairs_f32();
let mut resampled = 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 + 1 < pairs.len() {
let frac = src_pos - src_idx as f64;
let alpha = (1.0 - frac) as f32;
let beta = frac as f32;
let left_sample = pairs[src_idx][0] * alpha + pairs[src_idx + 1][0] * beta;
let right_sample = pairs[src_idx][1] * alpha + pairs[src_idx + 1][1] * beta;
resampled.push([left_sample, right_sample]);
} else if src_idx < pairs.len() {
resampled.push(pairs[src_idx]);
}
}
let mut new_chunk = AudioChunk::from_pairs_f32(
chunk.order(),
resampled,
target_sample_rate,
chunk.bit_depth(),
);
if chunk.gain_db().abs() > f64::EPSILON {
new_chunk = new_chunk.set_gain_db(chunk.gain_db());
}
self.subscribers.push(new_chunk).await?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BitDepth;
#[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::from_channels_f32(
0,
vec![1.0, 2.0, 3.0],
vec![4.0, 5.0, 6.0],
48000,
BitDepth::B24,
);
tx.send(chunk.clone()).await.unwrap();
// Recevoir le chunk
let received = out_rx.recv().await.unwrap();
assert!(Arc::ptr_eq(&chunk, &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::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
tx.send(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::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
tx.send(chunk.clone()).await.unwrap();
// Le chunk devrait être passé sans modification
let received = out_rx.recv().await.unwrap();
assert!(Arc::ptr_eq(&chunk, &received));
}
}

View File

@@ -1,500 +0,0 @@
//! DiskSink - Écrit le flux audio dans un fichier
//!
//! Ce module fournit un sink qui écrit les chunks audio sur disque,
//! avec support de la dérivation automatique du nom de fichier depuis la source.
use crate::{events::SourceNameUpdateEvent, nodes::AudioError, AudioChunk};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use tokio::sync::{mpsc, RwLock};
/// Configuration pour le DiskSink
#[derive(Debug, Clone)]
pub struct DiskSinkConfig {
/// Chemin racine où écrire les fichiers
pub output_dir: PathBuf,
/// Nom de fichier explicite (optionnel)
/// Si None, sera dérivé du nom de la source
pub filename: Option<String>,
/// Format d'écriture
pub format: AudioFileFormat,
/// Taille du buffer d'écriture (en chunks)
pub buffer_size: usize,
}
impl Default for DiskSinkConfig {
fn default() -> Self {
Self {
output_dir: PathBuf::from("."),
filename: None,
format: AudioFileFormat::Wav,
buffer_size: 100,
}
}
}
/// Formats de fichiers audio supportés
#[derive(Debug, Clone, Copy)]
pub enum AudioFileFormat {
/// Format WAV (non compressé)
Wav,
/// Format FLAC (compressé sans perte)
Flac,
/// Format brut PCM
Raw,
}
impl AudioFileFormat {
/// Retourne l'extension de fichier appropriée
pub fn extension(&self) -> &str {
match self {
AudioFileFormat::Wav => "wav",
AudioFileFormat::Flac => "flac",
AudioFileFormat::Raw => "pcm",
}
}
}
/// DiskSink - Écrit le flux audio dans un fichier sur disque
///
/// Ce sink consomme les chunks audio et les écrit dans un fichier.
/// Le nom du fichier peut être dérivé automatiquement du nom de la source
/// via les événements `SourceNameUpdateEvent`.
///
/// # Caractéristiques
///
/// - Écriture asynchrone avec buffer
/// - Dérivation automatique du nom de fichier depuis la source
/// - Support de plusieurs formats (WAV, FLAC, PCM brut)
/// - Gestion du gain : applique le gain avant l'écriture
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::{DiskSink, DiskSinkConfig};
/// use std::path::PathBuf;
///
/// #[tokio::main]
/// async fn main() {
/// let config = DiskSinkConfig {
/// output_dir: PathBuf::from("/tmp/audio"),
/// filename: Some("output.wav".to_string()),
/// ..Default::default()
/// };
///
/// let (sink, sink_tx) = DiskSink::new("disk1".to_string(), config, 10);
///
/// tokio::spawn(async move {
/// sink.run().await.unwrap()
/// });
/// }
/// ```
pub struct DiskSink {
/// Identifiant du sink
node_id: String,
/// Channel pour recevoir les chunks audio
rx: mpsc::Receiver<Arc<AudioChunk>>,
/// Configuration
config: DiskSinkConfig,
/// Nom de fichier résolu (partagé)
resolved_filename: Arc<RwLock<Option<PathBuf>>>,
/// Receiver pour les événements de nom de source (optionnel)
source_name_rx: Option<mpsc::Receiver<SourceNameUpdateEvent>>,
/// Writer pour le fichier
writer: Option<AudioFileWriter>,
}
impl DiskSink {
/// Crée un nouveau DiskSink
///
/// # Arguments
///
/// * `node_id` - Identifiant unique du sink
/// * `config` - Configuration du sink
/// * `channel_size` - Taille du buffer du channel
pub fn new(
node_id: String,
config: DiskSinkConfig,
channel_size: usize,
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
let (tx, rx) = mpsc::channel(channel_size);
let sink = Self {
node_id,
rx,
config,
resolved_filename: Arc::new(RwLock::new(None)),
source_name_rx: None,
writer: None,
};
(sink, tx)
}
/// Configure la source des événements de nom de source
pub fn set_source_name_source(&mut self, rx: mpsc::Receiver<SourceNameUpdateEvent>) {
self.source_name_rx = Some(rx);
}
/// Résout le nom du fichier de sortie
///
/// Si un filename explicite est fourni dans la config, l'utilise.
/// Sinon, utilise le source_name avec l'extension appropriée.
fn resolve_filename(&self, source_name: Option<&str>) -> PathBuf {
let filename = if let Some(ref explicit_name) = self.config.filename {
explicit_name.clone()
} else if let Some(name) = source_name {
// Nettoyer le nom de la source pour en faire un nom de fichier valide
let clean_name = name
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect::<String>();
format!("{}.{}", clean_name, self.config.format.extension())
} else {
// Fallback sur un nom par défaut
format!("{}.{}", self.node_id, self.config.format.extension())
};
self.config.output_dir.join(filename)
}
/// Initialise le writer pour le fichier de sortie
async fn initialize_writer(&mut self, source_name: Option<&str>) -> Result<(), AudioError> {
let path = self.resolve_filename(source_name);
*self.resolved_filename.write().await = Some(path.clone());
// Créer le répertoire parent si nécessaire
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to create directory: {}", e))
})?;
}
// Créer le writer approprié selon le format
let writer = match self.config.format {
AudioFileFormat::Wav => AudioFileWriter::new_wav(path).await?,
AudioFileFormat::Flac => {
// FLAC nécessiterait une bibliothèque externe, pour l'instant utiliser WAV
AudioFileWriter::new_wav(path).await?
}
AudioFileFormat::Raw => AudioFileWriter::new_raw(path).await?,
};
self.writer = Some(writer);
Ok(())
}
/// Démarre la boucle de traitement du DiskSink
pub async fn run(mut self) -> Result<DiskSinkStats, AudioError> {
let mut stats = DiskSinkStats::new(self.node_id.clone());
let mut source_name: Option<String> = None;
let mut initialized = false;
loop {
tokio::select! {
// Recevoir les chunks audio
chunk_opt = self.rx.recv() => {
match chunk_opt {
Some(chunk) => {
// Initialiser le writer à la réception du premier chunk
if !initialized {
self.initialize_writer(source_name.as_deref()).await?;
initialized = true;
}
// Appliquer le gain avant l'écriture
let chunk_with_gain = if chunk.gain_db().abs() > f64::EPSILON {
Arc::clone(&chunk).apply_gain()
} else {
Arc::clone(&chunk)
};
// Écrire le chunk
if let Some(ref mut writer) = self.writer {
writer.write_chunk(&chunk_with_gain).await?;
stats.record_chunk(&chunk_with_gain);
}
}
None => {
// Channel fermé, terminer
break;
}
}
}
// Recevoir les mises à jour du nom de source
source_event_opt = async {
if let Some(ref mut rx) = self.source_name_rx {
rx.recv().await
} else {
std::future::pending().await
}
} => {
if let Some(event) = source_event_opt {
source_name = Some(event.source_name.clone());
// Si on n'a pas encore initialisé, le nom sera utilisé plus tard
// Sinon, on pourrait décider de fermer le fichier actuel et d'en créer un nouveau
}
}
}
}
// Fermer le fichier proprement
if let Some(writer) = self.writer {
writer.close().await?;
}
stats.finalize();
Ok(stats)
}
}
/// Writer pour fichiers audio
struct AudioFileWriter {
file: File,
format: AudioFileFormat,
sample_rate: Option<u32>,
total_samples: usize,
}
impl AudioFileWriter {
/// Crée un writer WAV
async fn new_wav(path: PathBuf) -> Result<Self, AudioError> {
let file = File::create(path)
.await
.map_err(|e| AudioError::ProcessingError(format!("Failed to create file: {}", e)))?;
Ok(Self {
file,
format: AudioFileFormat::Wav,
sample_rate: None,
total_samples: 0,
})
}
/// Crée un writer pour PCM brut
async fn new_raw(path: PathBuf) -> Result<Self, AudioError> {
let file = File::create(path)
.await
.map_err(|e| AudioError::ProcessingError(format!("Failed to create file: {}", e)))?;
Ok(Self {
file,
format: AudioFileFormat::Raw,
sample_rate: None,
total_samples: 0,
})
}
/// Écrit un chunk audio
async fn write_chunk(&mut self, chunk: &AudioChunk) -> Result<(), AudioError> {
// Enregistrer le sample rate du premier chunk
if self.sample_rate.is_none() {
let sr = chunk.sample_rate();
self.sample_rate = Some(sr);
// Pour WAV, écrire l'en-tête (simplifié)
if matches!(self.format, AudioFileFormat::Wav) {
self.write_wav_header(sr).await?;
}
}
// Convertir en bytes (little-endian 16-bit PCM)
let mut bytes = Vec::with_capacity(chunk.len() * 4);
let max_val = chunk.bit_depth().max_value();
for frame in chunk.frames() {
let left = (frame[0] as f32 / max_val).clamp(-1.0, 1.0);
let right = (frame[1] as f32 / max_val).clamp(-1.0, 1.0);
let sample_i16 = (left * 32767.0) as i16;
bytes.extend_from_slice(&sample_i16.to_le_bytes());
let sample_r16 = (right * 32767.0) as i16;
bytes.extend_from_slice(&sample_r16.to_le_bytes());
}
self.file.write_all(&bytes).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to write audio data: {}", e))
})?;
self.total_samples += chunk.len();
Ok(())
}
/// Écrit un en-tête WAV simplifié
async fn write_wav_header(&mut self, sample_rate: u32) -> Result<(), AudioError> {
// En-tête WAV basique (sera mis à jour à la fermeture)
let mut header = Vec::new();
// RIFF chunk
header.extend_from_slice(b"RIFF");
header.extend_from_slice(&0u32.to_le_bytes()); // Taille (à mettre à jour)
header.extend_from_slice(b"WAVE");
// fmt chunk
header.extend_from_slice(b"fmt ");
header.extend_from_slice(&16u32.to_le_bytes()); // Taille du fmt chunk
header.extend_from_slice(&1u16.to_le_bytes()); // Format PCM
header.extend_from_slice(&2u16.to_le_bytes()); // 2 canaux (stéréo)
header.extend_from_slice(&sample_rate.to_le_bytes());
header.extend_from_slice(&(sample_rate * 4).to_le_bytes()); // Byte rate
header.extend_from_slice(&4u16.to_le_bytes()); // Block align
header.extend_from_slice(&16u16.to_le_bytes()); // Bits per sample
// data chunk header
header.extend_from_slice(b"data");
header.extend_from_slice(&0u32.to_le_bytes()); // Taille des données (à mettre à jour)
self.file.write_all(&header).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to write WAV header: {}", e))
})?;
Ok(())
}
/// Ferme le fichier et met à jour l'en-tête si nécessaire
async fn close(mut self) -> Result<(), AudioError> {
if matches!(self.format, AudioFileFormat::Wav) {
// Mettre à jour les tailles dans l'en-tête WAV
let data_size = (self.total_samples * 4) as u32; // 2 bytes per sample * 2 channels
let file_size = data_size + 36;
// Positionner au début et réécrire les tailles
use tokio::io::AsyncSeekExt;
self.file
.seek(std::io::SeekFrom::Start(4))
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to seek in file: {}", e))
})?;
self.file
.write_all(&file_size.to_le_bytes())
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to update file size: {}", e))
})?;
self.file
.seek(std::io::SeekFrom::Start(40))
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to seek in file: {}", e))
})?;
self.file
.write_all(&data_size.to_le_bytes())
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to update data size: {}", e))
})?;
}
self.file
.flush()
.await
.map_err(|e| AudioError::ProcessingError(format!("Failed to flush file: {}", e)))?;
Ok(())
}
}
/// Statistiques du DiskSink
#[derive(Debug, Clone)]
pub struct DiskSinkStats {
pub node_id: String,
pub chunks_written: u64,
pub total_samples: u64,
pub total_duration_sec: f64,
}
impl DiskSinkStats {
pub fn new(node_id: String) -> Self {
Self {
node_id,
chunks_written: 0,
total_samples: 0,
total_duration_sec: 0.0,
}
}
pub fn record_chunk(&mut self, chunk: &AudioChunk) {
self.chunks_written += 1;
self.total_samples += chunk.len() as u64;
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
}
pub fn finalize(&mut self) {
// Pourrait effectuer des calculs finaux ici
}
pub fn display(&self) {
println!("\n=== DiskSink Statistics: {} ===", self.node_id);
println!("Chunks written: {}", self.chunks_written);
println!("Total samples: {}", self.total_samples);
println!("Total duration: {:.3} sec", self.total_duration_sec);
println!("============================\n");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BitDepth;
#[tokio::test]
async fn test_disk_sink_basic() {
let temp_dir = std::env::temp_dir().join("pmoaudio_test");
tokio::fs::create_dir_all(&temp_dir).await.unwrap();
let config = DiskSinkConfig {
output_dir: temp_dir.clone(),
filename: Some("test_output.wav".to_string()),
format: AudioFileFormat::Wav,
buffer_size: 10,
};
let (sink, tx) = DiskSink::new("test".to_string(), config, 10);
let handle = tokio::spawn(async move { sink.run().await });
// Envoyer quelques chunks
for i in 0..5 {
let chunk = AudioChunk::from_channels_f32(
i,
vec![0.5; 1000],
vec![0.5; 1000],
48000,
BitDepth::B24,
);
tx.send(chunk).await.unwrap();
}
drop(tx);
let stats = handle.await.unwrap().unwrap();
assert_eq!(stats.chunks_written, 5);
// Vérifier que le fichier existe
let output_path = temp_dir.join("test_output.wav");
assert!(output_path.exists());
// Nettoyage
tokio::fs::remove_file(output_path).await.ok();
tokio::fs::remove_dir(temp_dir).await.ok();
}
}

View File

@@ -1,254 +0,0 @@
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
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_db: f32,
}
impl DspNode {
pub fn new(channel_size: usize, gain_db: f32) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
gain_db,
};
(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_db.abs() < f32::EPSILON {
// Gain = 0 dB, pas de transformation nécessaire
self.subscribers.push(chunk).await?;
continue;
}
let gain_linear = AudioChunk::gain_linear_from_db(self.gain_db as f64) as f32;
let mut pairs = chunk.to_pairs_f32();
for frame in &mut pairs {
frame[0] *= gain_linear;
frame[1] *= gain_linear;
}
let mut new_chunk = AudioChunk::from_pairs_f32(
chunk.order(),
pairs,
chunk.sample_rate(),
chunk.bit_depth(),
);
if chunk.gain_db().abs() > f64::EPSILON {
new_chunk = new_chunk.set_gain_db(chunk.gain_db());
}
self.subscribers.push(new_chunk).await?;
}
Ok(())
}
/// Met à jour le gain dynamiquement (nécessite un `Arc<RwLock<f32>>` dans une version réelle)
pub fn set_gain_db(&mut self, gain_db: f32) {
self.gain_db = gain_db;
}
}
/// 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 pairs = chunk.to_pairs_f32();
let mut filtered = Vec::with_capacity(pairs.len());
for sample in pairs.iter() {
self.prev_left = self.prev_left + self.alpha * (sample[0] - self.prev_left);
self.prev_right = self.prev_right + self.alpha * (sample[1] - self.prev_right);
filtered.push([self.prev_left, self.prev_right]);
}
let mut new_chunk = AudioChunk::from_pairs_f32(
chunk.order(),
filtered,
chunk.sample_rate(),
chunk.bit_depth(),
);
if chunk.gain_db().abs() > f64::EPSILON {
new_chunk = new_chunk.set_gain_db(chunk.gain_db());
}
self.subscribers.push(new_chunk).await?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BitDepth;
#[tokio::test]
async fn test_dsp_node_unity_gain() {
let (mut node, tx) = DspNode::new(10, 0.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::from_channels_f32(
0,
vec![0.25, 0.5, 0.75],
vec![0.1, 0.2, 0.3],
48000,
BitDepth::B24,
);
tx.send(chunk.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, &received));
}
#[tokio::test]
async fn test_dsp_node_gain() {
let gain_db = AudioChunk::gain_db_from_linear(2.0) as f32;
let (mut node, tx) = DspNode::new(10, gain_db);
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::from_channels_f32(
0,
vec![0.25, 0.5, 0.75],
vec![0.1, 0.2, 0.3],
48000,
BitDepth::B24,
);
tx.send(chunk).await.unwrap();
// Vérifier que le gain a été appliqué
let received = out_rx.recv().await.unwrap();
let frames = received.to_pairs_f32();
const EPS: f32 = 1e-3;
assert!((frames[0][0] - 0.5).abs() < EPS);
assert!((frames[1][0] - 1.0).abs() < EPS);
assert!((frames[2][0] - 1.0).abs() < EPS); // Clamp at full scale
assert!((frames[0][1] - 0.2).abs() < EPS);
assert!((frames[1][1] - 0.4).abs() < EPS);
assert!((frames[2][1] - 0.6).abs() < EPS);
}
#[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::from_channels_f32(
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,
BitDepth::B24,
);
tx.send(chunk).await.unwrap();
// Le filtre devrait lisser le signal
let received = out_rx.recv().await.unwrap();
let frames = received.to_pairs_f32();
assert!(frames[0][0].abs() < 1.0); // Premier échantillon lissé
assert!(frames[2][0].abs() < 1.0); // Signal ne devrait pas atteindre 1.0 immédiatement
}
#[tokio::test]
async fn test_dsp_node_multiple_subscribers() {
let gain_db = AudioChunk::gain_db_from_linear(0.5) as f32;
let (mut node, tx) = DspNode::new(10, gain_db);
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::from_channels_f32(0, vec![0.8, 0.4], vec![0.8, 0.4], 48000, BitDepth::B24);
tx.send(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));
let frames = received1.to_pairs_f32();
const EPS: f32 = 1e-3;
assert!((frames[0][0] - 0.4).abs() < EPS); // 0.8 * 0.5
assert!((frames[1][0] - 0.2).abs() < EPS); // 0.4 * 0.5
}
}

View File

@@ -1,244 +0,0 @@
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk, BitDepth,
};
use pmoflac::{decode_audio_stream, StreamInfo};
use std::{path::PathBuf, sync::Arc};
use tokio::{fs::File, io::AsyncReadExt, sync::mpsc};
/// FileSource - Lit un fichier audio et publie des `AudioChunk`
///
/// Cette source utilise `pmoflac` pour décoder le fichier (FLAC/MP3/OGG/WAV/AIFF)
/// puis transforme les échantillons PCM en `AudioChunk` stéréo.
pub struct FileSource {
path: PathBuf,
chunk_frames: usize,
subscribers: MultiSubscriberNode,
}
impl FileSource {
/// Crée une nouvelle source de fichier.
///
/// * `path` - chemin du fichier audio à lire
/// * `chunk_frames` - nombre d'échantillons par canal par chunk
pub fn new<P: Into<PathBuf>>(path: P, chunk_frames: usize) -> Self {
Self {
path: path.into(),
chunk_frames: chunk_frames.max(1),
subscribers: MultiSubscriberNode::new(),
}
}
/// Ajoute un abonné qui recevra les chunks décodés.
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance la lecture du fichier et diffuse les chunks.
pub async fn run(self) -> Result<(), AudioError> {
let file = File::open(&self.path).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to open {:?}: {}", self.path, e))
})?;
let mut stream = decode_audio_stream(file)
.await
.map_err(|e| AudioError::ProcessingError(format!("Decode error: {}", e)))?;
let stream_info = stream.info().clone();
validate_stream(&stream_info)?;
let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize;
let chunk_byte_len = self.chunk_frames * frame_bytes;
let mut pending = Vec::new();
let mut read_buf = vec![0u8; frame_bytes * 512.max(self.chunk_frames)];
let mut chunk_index = 0u64;
loop {
if pending.len() < chunk_byte_len {
let read = stream.read(&mut read_buf).await.map_err(|e| {
AudioError::ProcessingError(format!("I/O error while decoding: {}", e))
})?;
if read == 0 {
break;
}
pending.extend_from_slice(&read_buf[..read]);
}
if pending.is_empty() {
break;
}
let frames_in_pending = pending.len() / frame_bytes;
let frames_to_emit = frames_in_pending.min(self.chunk_frames);
let take_bytes = frames_to_emit * frame_bytes;
let chunk_bytes = pending.drain(..take_bytes).collect::<Vec<u8>>();
let chunk = bytes_to_chunk(&chunk_bytes, &stream_info, frames_to_emit, chunk_index)?;
self.subscribers.push(chunk).await?;
chunk_index += 1;
}
// Reste éventuel (moins qu'un chunk complet)
if !pending.is_empty() {
let frames = pending.len() / frame_bytes;
if frames > 0 {
let chunk = bytes_to_chunk(&pending, &stream_info, frames, chunk_index)?;
self.subscribers.push(chunk).await?;
}
}
stream
.wait()
.await
.map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?;
Ok(())
}
}
fn validate_stream(info: &StreamInfo) -> Result<(), AudioError> {
if !(1..=2).contains(&info.channels) {
return Err(AudioError::ProcessingError(format!(
"Unsupported channel count: {}",
info.channels
)));
}
match info.bits_per_sample {
8 | 16 | 24 | 32 => Ok(()),
other => Err(AudioError::ProcessingError(format!(
"Unsupported bit depth: {}",
other
))),
}
}
fn bytes_to_chunk(
chunk_bytes: &[u8],
info: &StreamInfo,
frames: usize,
order: u64,
) -> Result<Arc<AudioChunk>, AudioError> {
let bytes_per_sample = info.bytes_per_sample();
let channels = info.channels as usize;
let frame_bytes = bytes_per_sample * channels;
let mut left = Vec::with_capacity(frames);
let mut right = Vec::with_capacity(frames);
for frame_idx in 0..frames {
let base = frame_idx * frame_bytes;
let l = sample_to_f32(
&chunk_bytes[base..base + bytes_per_sample],
info.bits_per_sample,
)?;
let r = if channels == 1 {
l
} else {
sample_to_f32(
&chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample],
info.bits_per_sample,
)?
};
left.push(l);
right.push(r);
}
let bit_depth = BitDepth::from_u32_strict(info.bits_per_sample as u32);
Ok(AudioChunk::from_channels_f32(
order,
left,
right,
info.sample_rate,
bit_depth,
))
}
fn sample_to_f32(sample_bytes: &[u8], bits: u8) -> Result<f32, AudioError> {
let sample = match bits {
8 => i8::from_le_bytes([sample_bytes[0]]) as i32,
16 => i16::from_le_bytes(sample_bytes.try_into().unwrap()) as i32,
24 => {
let mut buf = [0u8; 4];
buf[..3].copy_from_slice(sample_bytes);
// Sign extend manually
if sample_bytes[2] & 0x80 != 0 {
buf[3] = 0xFF;
}
i32::from_le_bytes(buf)
}
32 => i32::from_le_bytes(sample_bytes.try_into().unwrap()),
other => {
return Err(AudioError::ProcessingError(format!(
"Unsupported bit depth: {}",
other
)))
}
};
let max = ((1i64 << (bits as i64 - 1)).saturating_sub(1)) as f32;
Ok((sample as f32) / max)
}
#[cfg(test)]
mod tests {
use super::*;
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
use std::io::Cursor;
use tokio::io::AsyncWriteExt;
use tokio::sync::mpsc;
#[tokio::test]
async fn test_file_source_decodes_flac() {
let temp_dir = tempfile::tempdir().unwrap();
let flac_path = temp_dir.path().join("test.flac");
let sample_rate = 48_000;
let frames = 256;
let mut pcm = Vec::with_capacity(frames * 4);
for i in 0..frames {
let sample = ((i % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5; // simple ramp
let sample_i16 = (sample * 32767.0) as i16;
pcm.extend_from_slice(&sample_i16.to_le_bytes());
pcm.extend_from_slice(&sample_i16.to_le_bytes());
}
let format = PcmFormat {
sample_rate,
channels: 2,
bits_per_sample: 16,
};
let mut flac_stream =
encode_flac_stream(Cursor::new(pcm.clone()), format, EncoderOptions::default())
.await
.unwrap();
let mut file = File::create(&flac_path).await.expect("create flac file");
tokio::io::copy(&mut flac_stream, &mut file)
.await
.expect("write flac");
file.flush().await.expect("flush file");
flac_stream.wait().await.unwrap();
let mut source = FileSource::new(&flac_path, 64);
let (tx, mut rx) = mpsc::channel(4);
source.add_subscriber(tx);
tokio::spawn(async move {
source.run().await.unwrap();
});
let mut received = 0usize;
while let Some(chunk) = rx.recv().await {
received += chunk.len();
assert_eq!(chunk.sample_rate(), sample_rate);
let scale = 1.0 / chunk.bit_depth().max_value();
if let Some(frame) = chunk.frames().first() {
assert!(((frame[0] as f32) * scale).abs() <= 1.0); // sample range sanity
}
}
assert_eq!(received, frames);
}
}

View File

@@ -1,300 +0,0 @@
use crate::{nodes::AudioError, AudioChunk};
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
use std::{
collections::VecDeque,
path::PathBuf,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use tokio::{
fs::File,
io::{self, AsyncRead, AsyncWriteExt, ReadBuf},
sync::mpsc,
};
/// Sink qui encode les `AudioChunk` reçus au format FLAC.
pub struct FlacFileSink {
rx: mpsc::Receiver<Arc<AudioChunk>>,
path: PathBuf,
encoder_options: EncoderOptions,
pcm_buffer_capacity: usize,
}
impl FlacFileSink {
/// Crée un sink FLAC avec les options par défaut (compression 5).
pub fn new<P: Into<PathBuf>>(
path: P,
channel_size: usize,
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
Self::with_options(path, channel_size, EncoderOptions::default())
}
/// Crée un sink FLAC avec des options explicites.
pub fn with_options<P: Into<PathBuf>>(
path: P,
channel_size: usize,
encoder_options: EncoderOptions,
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
let (tx, rx) = mpsc::channel(channel_size);
let sink = Self {
rx,
path: path.into(),
encoder_options,
pcm_buffer_capacity: 8,
};
(sink, tx)
}
/// Lance l'encodage vers le fichier cible.
pub async fn run(self) -> Result<FlacFileSinkStats, AudioError> {
let FlacFileSink {
mut rx,
path,
encoder_options,
pcm_buffer_capacity,
} = self;
let first_chunk = rx.recv().await.ok_or_else(|| {
AudioError::ProcessingError("FlacFileSink: no audio data received".into())
})?;
if first_chunk.len() == 0 {
return Err(AudioError::ProcessingError(
"FlacFileSink: received empty chunk".into(),
));
}
let format = PcmFormat {
sample_rate: first_chunk.sample_rate(),
channels: 2,
bits_per_sample: 16,
};
if let Err(err) = format.validate() {
return Err(AudioError::ProcessingError(format!(
"Invalid PCM format: {}",
err
)));
}
let (pcm_tx, pcm_rx) = mpsc::channel::<Vec<u8>>(pcm_buffer_capacity);
let pump_handle = tokio::spawn(pump_chunks(first_chunk, rx, pcm_tx));
let reader = ByteStreamReader::new(pcm_rx);
let mut flac_stream = encode_flac_stream(reader, format, encoder_options)
.await
.map_err(|e| AudioError::ProcessingError(format!("FLAC encode init failed: {}", e)))?;
let mut output = File::create(&path).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to create {:?}: {}", path, e))
})?;
tokio::io::copy(&mut flac_stream, &mut output)
.await
.map_err(|e| AudioError::ProcessingError(format!("FLAC write failed: {}", e)))?;
output.flush().await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to flush {:?}: {}", path, e))
})?;
flac_stream
.wait()
.await
.map_err(|e| AudioError::ProcessingError(format!("FLAC encoder task failed: {}", e)))?;
let pump_stats = pump_handle
.await
.map_err(|e| AudioError::ProcessingError(format!("Pump task panicked: {}", e)))??;
Ok(FlacFileSinkStats {
path,
chunks_received: pump_stats.chunks,
total_samples: pump_stats.samples,
total_duration_sec: pump_stats.duration_sec,
})
}
}
struct PumpStats {
chunks: u64,
samples: u64,
duration_sec: f64,
}
async fn pump_chunks(
first_chunk: Arc<AudioChunk>,
mut rx: mpsc::Receiver<Arc<AudioChunk>>,
pcm_tx: mpsc::Sender<Vec<u8>>,
) -> Result<PumpStats, AudioError> {
let mut chunks = 0u64;
let mut samples = 0u64;
let mut duration_sec = 0.0f64;
let expected_rate = first_chunk.sample_rate();
let mut current = Some(first_chunk);
loop {
let chunk_opt = if let Some(ch) = current.take() {
Some(ch)
} else {
rx.recv().await
};
let chunk = match chunk_opt {
Some(ch) => ch,
None => break,
};
if chunk.sample_rate() != expected_rate {
return Err(AudioError::ProcessingError(format!(
"FlacFileSink: inconsistent sample rate ({} vs {})",
chunk.sample_rate(),
expected_rate
)));
}
let pcm_bytes = chunk_to_pcm_bytes(&chunk);
if pcm_bytes.is_empty() {
continue;
}
pcm_tx
.send(pcm_bytes)
.await
.map_err(|_| AudioError::SendError)?;
chunks += 1;
samples += chunk.len() as u64;
duration_sec += chunk.len() as f64 / expected_rate as f64;
}
Ok(PumpStats {
chunks,
samples,
duration_sec,
})
}
fn chunk_to_pcm_bytes(chunk: &AudioChunk) -> Vec<u8> {
let len = chunk.len();
let mut bytes = Vec::with_capacity(len * 4);
let gain = chunk.gain_linear() as f32;
let scale = 1.0f32 / chunk.bit_depth().max_value();
for frame in chunk.frames() {
let left = (frame[0] as f32 * scale * gain).clamp(-1.0, 1.0);
let right = (frame[1] as f32 * scale * gain).clamp(-1.0, 1.0);
let left_i16 = (left * 32767.0) as i16;
let right_i16 = (right * 32767.0) as i16;
bytes.extend_from_slice(&left_i16.to_le_bytes());
bytes.extend_from_slice(&right_i16.to_le_bytes());
}
bytes
}
struct ByteStreamReader {
rx: mpsc::Receiver<Vec<u8>>,
buffer: VecDeque<u8>,
finished: bool,
}
impl ByteStreamReader {
fn new(rx: mpsc::Receiver<Vec<u8>>) -> Self {
Self {
rx,
buffer: VecDeque::new(),
finished: false,
}
}
}
impl AsyncRead for ByteStreamReader {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
loop {
if !self.buffer.is_empty() {
let to_copy = self.buffer.len().min(buf.remaining());
if to_copy == 0 {
return Poll::Ready(Ok(()));
}
// VecDeque::make_contiguous pour copier efficacement
let slice = self.buffer.make_contiguous();
buf.put_slice(&slice[..to_copy]);
self.buffer.drain(..to_copy);
return Poll::Ready(Ok(()));
}
if self.finished {
return Poll::Ready(Ok(()));
}
match Pin::new(&mut self.rx).poll_recv(cx) {
Poll::Ready(Some(bytes)) => {
if bytes.is_empty() {
continue;
}
self.buffer.extend(bytes);
}
Poll::Ready(None) => {
self.finished = true;
return Poll::Ready(Ok(()));
}
Poll::Pending => return Poll::Pending,
}
}
}
}
/// Statistiques produites par le `FlacFileSink`.
#[derive(Debug, Clone)]
pub struct FlacFileSinkStats {
pub path: PathBuf,
pub chunks_received: u64,
pub total_samples: u64,
pub total_duration_sec: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BitDepth;
use pmoflac::decode_flac_stream;
use tokio::io::AsyncReadExt;
#[tokio::test]
async fn test_flac_file_sink_writes_audio() {
let temp_dir = tempfile::tempdir().unwrap();
let output_path = temp_dir.path().join("output.flac");
let (sink, tx) = FlacFileSink::new(&output_path, 8);
let handle = tokio::spawn(async move { sink.run().await.unwrap() });
let chunk = AudioChunk::from_channels_f32(
0,
vec![0.25; 256],
vec![0.5; 256],
44_100,
BitDepth::B24,
);
tx.send(chunk).await.unwrap();
drop(tx);
let stats = handle.await.unwrap();
assert_eq!(stats.chunks_received, 1);
assert_eq!(stats.total_samples, 256);
let file = File::open(&output_path).await.unwrap();
let mut stream = decode_flac_stream(file).await.unwrap();
let info = stream.info().clone();
assert_eq!(info.channels, 2);
assert_eq!(info.sample_rate, 44_100);
let mut decoded = Vec::new();
stream.read_to_end(&mut decoded).await.unwrap();
stream.wait().await.unwrap();
assert_eq!(decoded.len(), 256 * 4); // 256 frames * 2 channels * 2 bytes
}
}

View File

@@ -1,149 +0,0 @@
//! 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 chromecast_sink;
pub mod decoder_node;
pub mod disk_sink;
pub mod dsp_node;
pub mod file_source;
pub mod flac_file_sink;
pub mod mpd_sink;
pub mod sink_node;
pub mod source_node;
pub mod timer_node;
pub mod volume_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 {}

View File

@@ -1,398 +0,0 @@
//! MpdSink - Envoie le flux audio à un démon MPD (Music Player Daemon)
//!
//! Ce module fournit un sink qui streame l'audio vers un démon MPD distant ou local.
//! Note: Cette implémentation est une version mock/skeleton. Une vraie implémentation
//! nécessiterait le protocole MPD complet et l'utilisation de bibliothèques comme `mpd`.
use crate::{nodes::AudioError, AudioChunk};
use std::sync::Arc;
use tokio::sync::mpsc;
/// Configuration pour le MpdSink
#[derive(Debug, Clone)]
pub struct MpdConfig {
/// Adresse du serveur MPD
pub host: String,
/// Port du serveur MPD (défaut: 6600)
pub port: u16,
/// Mot de passe optionnel
pub password: Option<String>,
/// Nom de l'output MPD à utiliser (optionnel)
pub output_name: Option<String>,
/// Taille du buffer
pub buffer_size: usize,
/// Format d'envoi
pub format: MpdAudioFormat,
}
impl Default for MpdConfig {
fn default() -> Self {
Self {
host: "localhost".to_string(),
port: 6600,
password: None,
output_name: None,
buffer_size: 50,
format: MpdAudioFormat::S16Le,
}
}
}
/// Formats audio supportés par MPD
#[derive(Debug, Clone, Copy)]
pub enum MpdAudioFormat {
/// Signed 16-bit Little Endian
S16Le,
/// Signed 24-bit Little Endian
S24Le,
/// Signed 32-bit Little Endian
S32Le,
/// Float 32-bit
F32,
}
impl MpdAudioFormat {
/// Retourne le nom du format pour le protocole MPD
pub fn as_mpd_string(&self) -> &str {
match self {
MpdAudioFormat::S16Le => "16:16:2",
MpdAudioFormat::S24Le => "24:24:2",
MpdAudioFormat::S32Le => "32:32:2",
MpdAudioFormat::F32 => "f:32:2",
}
}
}
/// MpdSink - Streame vers un démon MPD
///
/// Ce sink se connecte à un serveur MPD et lui envoie le flux audio.
/// MPD peut ensuite router l'audio vers différents outputs (ALSA, PulseAudio, HTTP, etc.).
///
/// # Implémentation actuelle
///
/// Cette version est un mock qui simule la communication avec MPD.
/// Pour une vraie implémentation, il faudrait:
/// - Implémenter le protocole MPD (commandes textuelles sur TCP)
/// - S'authentifier si nécessaire
/// - Configurer le format audio
/// - Envoyer les données PCM via le protocole approprié
/// - Gérer les commandes de contrôle (play, pause, stop)
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::{MpdSink, MpdConfig};
///
/// #[tokio::main]
/// async fn main() {
/// let config = MpdConfig {
/// host: "localhost".to_string(),
/// port: 6600,
/// password: None,
/// ..Default::default()
/// };
///
/// let (sink, sink_tx) = MpdSink::new("mpd1".to_string(), config, 10);
///
/// tokio::spawn(async move {
/// sink.run().await.unwrap()
/// });
/// }
/// ```
pub struct MpdSink {
/// Identifiant du sink
node_id: String,
/// Channel pour recevoir les chunks audio
rx: mpsc::Receiver<Arc<AudioChunk>>,
/// Configuration
config: MpdConfig,
/// État de la connexion (mock)
connected: bool,
/// Version du serveur MPD (mock)
mpd_version: Option<String>,
}
impl MpdSink {
/// Crée un nouveau MpdSink
///
/// # Arguments
///
/// * `node_id` - Identifiant unique du sink
/// * `config` - Configuration MPD
/// * `channel_size` - Taille du buffer du channel
pub fn new(
node_id: String,
config: MpdConfig,
channel_size: usize,
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
let (tx, rx) = mpsc::channel(channel_size);
let sink = Self {
node_id,
rx,
config,
connected: false,
mpd_version: None,
};
(sink, tx)
}
/// Établit la connexion avec le serveur MPD (mock)
async fn connect(&mut self) -> Result<(), AudioError> {
println!(
"[{}] Connecting to MPD at {}:{}...",
self.node_id, self.config.host, self.config.port
);
// Simuler une connexion TCP
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
// Dans une vraie implémentation:
// 1. Établir connexion TCP
// 2. Lire la bannière de version
// 3. S'authentifier si password fourni
// 4. Configurer le format audio
self.mpd_version = Some("0.23.0".to_string());
self.connected = true;
println!(
"[{}] Connected to MPD v{} successfully",
self.node_id,
self.mpd_version.as_ref().unwrap()
);
// Configurer le format audio
self.configure_audio_format().await?;
Ok(())
}
/// Configure le format audio sur MPD (mock)
async fn configure_audio_format(&self) -> Result<(), AudioError> {
println!(
"[{}] Configuring audio format: {}",
self.node_id,
self.config.format.as_mpd_string()
);
// Dans une vraie implémentation:
// Envoyer une commande MPD pour configurer le format
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
Ok(())
}
/// Envoie un chunk au serveur MPD (mock)
async fn send_chunk(&self, _chunk: &AudioChunk) -> Result<(), AudioError> {
if !self.connected {
return Err(AudioError::ProcessingError(
"Not connected to MPD".to_string(),
));
}
// Dans une vraie implémentation:
// 1. Appliquer le gain
// 2. Convertir dans le format approprié (S16LE, etc.)
// 3. Envoyer via le protocole MPD (probablement via une commande `sendmessage` ou pipe)
// Simuler un délai d'envoi
tokio::time::sleep(tokio::time::Duration::from_micros(50)).await;
Ok(())
}
/// Déconnecte proprement du serveur MPD (mock)
async fn disconnect(&mut self) -> Result<(), AudioError> {
if self.connected {
println!("[{}] Disconnecting from MPD...", self.node_id);
// Dans une vraie implémentation:
// Envoyer la commande "close"
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
self.connected = false;
println!("[{}] Disconnected successfully", self.node_id);
}
Ok(())
}
/// Démarre la boucle de traitement du MpdSink
pub async fn run(mut self) -> Result<MpdStats, AudioError> {
// Établir la connexion
self.connect().await?;
let mut stats = MpdStats::new(
self.node_id.clone(),
format!("{}:{}", self.config.host, self.config.port),
);
// Boucle principale
while let Some(chunk) = self.rx.recv().await {
// Appliquer le gain si nécessaire
let chunk_to_send = if chunk.gain_db().abs() > f64::EPSILON {
Arc::clone(&chunk).apply_gain()
} else {
Arc::clone(&chunk)
};
// Envoyer au serveur MPD
self.send_chunk(&chunk_to_send).await?;
stats.record_chunk(&chunk_to_send);
}
// Déconnexion propre
self.disconnect().await?;
stats.finalize();
Ok(stats)
}
/// Retourne un handle pour contrôler le sink (mock)
pub fn get_handle(&self) -> MpdHandle {
MpdHandle {
node_id: self.node_id.clone(),
}
}
}
/// Handle pour contrôler le MpdSink
///
/// Permet d'envoyer des commandes de contrôle au serveur MPD
#[derive(Clone)]
pub struct MpdHandle {
node_id: String,
}
impl MpdHandle {
/// Commande play (mock)
pub async fn play(&self) -> Result<(), AudioError> {
println!("[{}] MPD command: play", self.node_id);
Ok(())
}
/// Commande pause (mock)
pub async fn pause(&self) -> Result<(), AudioError> {
println!("[{}] MPD command: pause", self.node_id);
Ok(())
}
/// Commande stop (mock)
pub async fn stop(&self) -> Result<(), AudioError> {
println!("[{}] MPD command: stop", self.node_id);
Ok(())
}
/// Change le volume MPD (0-100) (mock)
pub async fn set_volume(&self, volume: u8) -> Result<(), AudioError> {
let clamped = volume.min(100);
println!("[{}] MPD command: setvol {}", self.node_id, clamped);
Ok(())
}
}
/// Statistiques du MpdSink
#[derive(Debug, Clone)]
pub struct MpdStats {
pub node_id: String,
pub server_address: String,
pub chunks_sent: u64,
pub total_samples: u64,
pub total_duration_sec: f64,
}
impl MpdStats {
pub fn new(node_id: String, server_address: String) -> Self {
Self {
node_id,
server_address,
chunks_sent: 0,
total_samples: 0,
total_duration_sec: 0.0,
}
}
pub fn record_chunk(&mut self, chunk: &AudioChunk) {
self.chunks_sent += 1;
self.total_samples += chunk.len() as u64;
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
}
pub fn finalize(&mut self) {
// Calculs finaux si nécessaire
}
pub fn display(&self) {
println!("\n=== MPD Sink Statistics: {} ===", self.node_id);
println!("Server: {}", self.server_address);
println!("Chunks sent: {}", self.chunks_sent);
println!("Total samples: {}", self.total_samples);
println!("Total duration: {:.3} sec", self.total_duration_sec);
println!("===============================\n");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BitDepth;
#[tokio::test]
async fn test_mpd_sink_basic() {
let config = MpdConfig {
host: "localhost".to_string(),
port: 6600,
..Default::default()
};
let (sink, tx) = MpdSink::new("test".to_string(), config, 10);
let handle = tokio::spawn(async move { sink.run().await });
// Envoyer quelques chunks
for i in 0..5 {
let chunk = AudioChunk::from_channels_f32(
i,
vec![0.5; 1000],
vec![0.5; 1000],
48000,
BitDepth::B24,
);
tx.send(chunk).await.unwrap();
}
drop(tx);
let stats = handle.await.unwrap().unwrap();
assert_eq!(stats.chunks_sent, 5);
assert_eq!(stats.server_address, "localhost:6600");
}
#[tokio::test]
async fn test_mpd_handle() {
let config = MpdConfig::default();
let (sink, _tx) = MpdSink::new("test".to_string(), config, 10);
let handle = sink.get_handle();
// Tester les commandes (mock)
handle.play().await.unwrap();
handle.pause().await.unwrap();
handle.set_volume(75).await.unwrap();
handle.stop().await.unwrap();
}
}

View File

@@ -1,212 +0,0 @@
use crate::{nodes::AudioError, AudioChunk};
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;
let len = chunk.len() as u64;
self.total_samples += len;
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
let inv_max = 1.0f32 / chunk.bit_depth().max_value();
let mut peak_left = self.peak_left;
let mut peak_right = self.peak_right;
let mut sum_squares_left = 0.0f64;
let mut sum_squares_right = 0.0f64;
for frame in chunk.frames() {
let left = frame[0] as f32 * inv_max;
let right = frame[1] as f32 * inv_max;
let left_abs = left.abs();
let right_abs = right.abs();
if left_abs > peak_left {
peak_left = left_abs;
}
if right_abs > peak_right {
peak_right = right_abs;
}
let l64 = left as f64;
let r64 = right as f64;
sum_squares_left += l64 * l64;
sum_squares_right += r64 * r64;
}
self.peak_left = peak_left;
self.peak_right = peak_right;
let prev_samples = self.total_samples - len;
self.rms_left = ((self.rms_left.powi(2) * prev_samples as f64 + sum_squares_left)
/ self.total_samples as f64)
.sqrt();
self.rms_right = ((self.rms_right.powi(2) * prev_samples 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::*;
use crate::BitDepth;
const BD: BitDepth = BitDepth::B24;
#[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::from_channels_f32(i, vec![0.0; 100], vec![0.0; 100], 48000, BD);
tx.send(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::from_channels_f32(i, vec![1.0; 1000], vec![0.5; 1000], 48000, BD);
tx.send(chunk).await.unwrap();
}
drop(tx);
let stats = handle.await.unwrap().unwrap();
assert_eq!(stats.chunks_received, 3);
assert_eq!(stats.total_samples, 3000);
assert!((stats.peak_left - 1.0).abs() < 1e-6);
assert!((stats.peak_right - 0.5).abs() < 1e-6);
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::from_channels_f32(i, vec![0.0; 100], vec![0.0; 100], 48000, BD);
tx.send(chunk).await.unwrap();
}
drop(tx);
let total_samples = handle.await.unwrap().unwrap();
assert_eq!(total_samples, 500);
}
}

View File

@@ -1,171 +0,0 @@
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk, BitDepth,
};
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,
}
const DEFAULT_BIT_DEPTH: BitDepth = BitDepth::B24;
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,
) -> Arc<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::from_channels_f32(order, left, right, sample_rate, DEFAULT_BIT_DEPTH)
}
/// 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(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 stereo = vec![[0i32; 2]; chunk_size];
let chunk = AudioChunk::new(i, stereo, sample_rate, DEFAULT_BIT_DEPTH);
self.subscribers.push(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(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 pairs = chunk.to_pairs_f32();
let left: Vec<f32> = pairs.iter().map(|frame| frame[0]).collect();
// 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.frames().iter().all(|frame| frame[0] == 0));
assert!(chunk.frames().iter().all(|frame| frame[1] == 0));
}
}
}

View File

@@ -1,291 +0,0 @@
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
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::*;
use crate::BitDepth;
#[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 stereo = vec![[0i32; 2]; 1000];
let chunk = AudioChunk::new(i, stereo, 48000, BitDepth::B24);
tx.send(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::from_channels_i32(
42,
vec![100, 200, 300],
vec![400, 500, 600],
48000,
BitDepth::B24,
);
tx.send(chunk.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, &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![[0i32; 2]; 48000], 48000, BitDepth::B24);
tx.send(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![[0i32; 2]; 96000], 96000, BitDepth::B24);
tx.send(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);
}
}

View File

@@ -1,363 +0,0 @@
//! Volume nodes - Contrôle du volume audio
//!
//! Ce module fournit des nodes pour ajuster le volume du flux audio,
//! avec support du volume master/secondaire et notification des changements.
use crate::{
events::{EventPublisher, VolumeChangeEvent},
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
/// VolumeNode - Applique un gain au flux audio (contrôle software)
///
/// Ce node modifie le champ `gain` de chaque `AudioChunk` qui le traverse.
/// Le gain est multiplié avec le gain existant du chunk, permettant ainsi
/// une chaîne de contrôles de volume.
///
/// # Caractéristiques
///
/// - Thread-safe : le volume peut être modifié pendant l'exécution via `set_volume`
/// - Notification : émet des événements `VolumeChangeEvent` lors des changements
/// - Master/Slave : peut s'abonner à un volume master pour synchronisation
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::VolumeNode;
///
/// #[tokio::main]
/// async fn main() {
/// let (volume_node, volume_tx) = VolumeNode::new("Room 1".to_string(), 0.8, 10);
///
/// // Modifier le volume pendant l'exécution
/// let handle = volume_node.get_handle();
/// tokio::spawn(async move {
/// tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
/// handle.set_volume(0.5).await;
/// });
///
/// tokio::spawn(async move { volume_node.run().await.unwrap() });
/// }
/// ```
pub struct VolumeNode {
/// Channel pour recevoir les chunks audio
rx: mpsc::Receiver<Arc<AudioChunk>>,
/// Subscribers pour les chunks modifiés
subscribers: MultiSubscriberNode,
/// Volume courant (partagé via RwLock pour lecture/écriture thread-safe)
volume: Arc<RwLock<f32>>,
/// Publisher pour les événements de changement de volume
volume_publisher: EventPublisher<VolumeChangeEvent>,
/// Identifiant unique du node (pour traçabilité)
node_id: String,
/// Receiver pour les événements de volume master (optionnel)
master_volume_rx: Option<mpsc::Receiver<VolumeChangeEvent>>,
}
impl VolumeNode {
/// Crée un nouveau VolumeNode
///
/// # Arguments
///
/// * `node_id` - Identifiant unique du node
/// * `initial_volume` - Volume initial (0.0 à 1.0)
/// * `channel_size` - Taille du buffer du channel
pub fn new(
node_id: String,
initial_volume: f32,
channel_size: usize,
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
volume: Arc::new(RwLock::new(initial_volume)),
volume_publisher: EventPublisher::new(),
node_id,
master_volume_rx: None,
};
(node, tx)
}
/// Ajoute un subscriber pour recevoir les chunks audio modifiés
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
self.subscribers.add_subscriber(tx);
}
/// Ajoute un subscriber pour les événements de changement de volume
pub fn subscribe_volume_events(&mut self, tx: mpsc::Sender<VolumeChangeEvent>) {
self.volume_publisher.subscribe(tx);
}
/// Configure ce node pour écouter un volume master
///
/// Le node appliquera à la fois son volume local ET le volume master reçu.
pub fn set_master_volume_source(&mut self, rx: mpsc::Receiver<VolumeChangeEvent>) {
self.master_volume_rx = Some(rx);
}
/// Retourne un handle pour contrôler le volume depuis un autre contexte
pub fn get_handle(&self) -> VolumeHandle {
VolumeHandle {
volume: self.volume.clone(),
node_id: self.node_id.clone(),
publisher: Arc::new(RwLock::new(self.volume_publisher.clone())),
}
}
/// Démarre la boucle de traitement du VolumeNode
pub async fn run(mut self) -> Result<(), AudioError> {
let mut master_volume = 1.0f32;
loop {
tokio::select! {
// Recevoir les chunks audio
chunk_opt = self.rx.recv() => {
match chunk_opt {
Some(chunk) => {
let local_volume = *self.volume.read().await;
let total_volume = (local_volume * master_volume).max(0.0);
// Créer un nouveau chunk avec le gain modifié (conversion vers dB)
let modified_chunk =
chunk.with_modified_gain_linear(total_volume as f64);
// Envoyer aux subscribers
self.subscribers.push(modified_chunk).await?;
}
None => {
// Channel fermé, terminer
break;
}
}
}
// Recevoir les mises à jour du volume master (si configuré)
master_event_opt = async {
if let Some(ref mut rx) = self.master_volume_rx {
rx.recv().await
} else {
// Bloquer indéfiniment si pas de master
std::future::pending().await
}
} => {
if let Some(event) = master_event_opt {
master_volume = event.volume;
// Optionnel : re-publier l'événement combiné
let local_volume = *self.volume.read().await;
let combined_event = VolumeChangeEvent {
volume: local_volume * master_volume,
source_node_id: self.node_id.clone(),
};
self.volume_publisher.publish(combined_event).await;
}
}
}
}
Ok(())
}
}
/// Handle pour contrôler un VolumeNode depuis un autre contexte
///
/// Ce handle permet de modifier le volume et de notifier les subscribers
/// sans avoir accès direct au node.
#[derive(Clone)]
pub struct VolumeHandle {
volume: Arc<RwLock<f32>>,
node_id: String,
publisher: Arc<RwLock<EventPublisher<VolumeChangeEvent>>>,
}
impl VolumeHandle {
/// Modifie le volume
///
/// # Arguments
///
/// * `new_volume` - Nouveau volume (0.0 à 1.0)
pub async fn set_volume(&self, new_volume: f32) {
let clamped = new_volume.clamp(0.0, 1.0);
*self.volume.write().await = clamped;
// Publier l'événement de changement
let event = VolumeChangeEvent {
volume: clamped,
source_node_id: self.node_id.clone(),
};
self.publisher.read().await.publish(event).await;
}
/// Obtient le volume courant
pub async fn get_volume(&self) -> f32 {
*self.volume.read().await
}
/// Augmente le volume de manière relative
pub async fn adjust_volume(&self, delta: f32) {
let current = *self.volume.read().await;
self.set_volume(current + delta).await;
}
}
/// HardwareVolumeNode - Contrôle matériel du volume
///
/// Ce node simule un contrôle hardware du volume. Dans une implémentation réelle,
/// il communiquerait avec le driver audio pour ajuster le volume matériel.
///
/// Pour cette version, il agit de manière similaire à `VolumeNode` mais pourrait
/// être étendu pour utiliser des APIs système spécifiques.
pub struct HardwareVolumeNode {
inner: VolumeNode,
}
impl HardwareVolumeNode {
/// Crée un nouveau HardwareVolumeNode
pub fn new(
node_id: String,
initial_volume: f32,
channel_size: usize,
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
let (inner, tx) = VolumeNode::new(node_id, initial_volume, channel_size);
(Self { inner }, tx)
}
/// Ajoute un subscriber
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
self.inner.add_subscriber(tx);
}
/// Obtient un handle pour contrôler le volume
pub fn get_handle(&self) -> VolumeHandle {
self.inner.get_handle()
}
/// Démarre la boucle de traitement
pub async fn run(self) -> Result<(), AudioError> {
// Dans une vraie implémentation, on communiquerait avec le hardware ici
// Pour l'instant, délègue au VolumeNode standard
self.inner.run().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BitDepth;
#[tokio::test]
async fn test_volume_node_basic() {
let (mut node, tx) = VolumeNode::new("test".to_string(), 0.5, 10);
let (out_tx, mut out_rx) = mpsc::channel(10);
node.add_subscriber(out_tx);
let handle = tokio::spawn(async move { node.run().await });
// Envoyer un chunk avec gain 1.0
let chunk =
AudioChunk::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
tx.send(chunk).await.unwrap();
// Recevoir le chunk modifié
let modified = out_rx.recv().await.unwrap();
assert!((modified.gain_linear() - 0.5).abs() < 1e-6);
drop(tx);
handle.await.unwrap().unwrap();
}
#[tokio::test]
async fn test_volume_handle() {
let (node, tx) = VolumeNode::new("test".to_string(), 1.0, 10);
let handle = node.get_handle();
tokio::spawn(async move { node.run().await });
// Modifier le volume via le handle
handle.set_volume(0.3).await;
let volume = handle.get_volume().await;
assert!((volume - 0.3).abs() < f32::EPSILON);
drop(tx);
}
#[tokio::test]
async fn test_volume_events() {
let (mut node, tx) = VolumeNode::new("test".to_string(), 1.0, 10);
let (event_tx, mut event_rx) = mpsc::channel(10);
node.subscribe_volume_events(event_tx);
let handle = node.get_handle();
tokio::spawn(async move { node.run().await });
// Changer le volume
handle.set_volume(0.7).await;
// Vérifier l'événement
let event = event_rx.recv().await.unwrap();
assert!((event.volume - 0.7).abs() < f32::EPSILON);
assert_eq!(event.source_node_id, "test");
drop(tx);
}
#[tokio::test]
async fn test_master_slave_volume() {
// Créer le master
let (mut master, master_tx) = VolumeNode::new("master".to_string(), 1.0, 10);
let (master_event_tx, master_event_rx) = mpsc::channel(10);
master.subscribe_volume_events(master_event_tx);
let master_handle = master.get_handle();
// Créer le slave
let (mut slave, slave_tx) = VolumeNode::new("slave".to_string(), 0.8, 10);
slave.set_master_volume_source(master_event_rx);
let (out_tx, mut out_rx) = mpsc::channel(10);
slave.add_subscriber(out_tx);
tokio::spawn(async move { master.run().await });
tokio::spawn(async move { slave.run().await });
// Envoyer un chunk au slave
let chunk =
AudioChunk::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
slave_tx.send(chunk).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
// Modifier le volume master
master_handle.set_volume(0.5).await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
// Envoyer un autre chunk
let chunk2 =
AudioChunk::from_channels_f32(1, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
slave_tx.send(chunk2).await.unwrap();
// Le deuxième chunk devrait avoir un gain de 0.8 * 0.5 = 0.4 (≈ -7.96 dB)
let _first = out_rx.recv().await.unwrap(); // gain ≈ 0.8
let second = out_rx.recv().await.unwrap(); // gain ≈ 0.4
assert!((second.gain_linear() - 0.4).abs() < 0.01);
drop(master_tx);
drop(slave_tx);
}
}

View File

@@ -0,0 +1,342 @@
//! Types de samples audio et trait de conversion générique
use std::fmt;
/// Trait pour tous les types de samples audio supportés
///
/// Ce trait permet d'écrire du code générique sur différents types de samples
/// (entiers 8/16/24/32 bits et flottants 32/64 bits).
pub trait Sample: Copy + Clone + Send + Sync + 'static + fmt::Debug {
/// Nom du type pour le débogage
const NAME: &'static str;
/// Valeur minimale du type
const MIN: Self;
/// Valeur maximale du type
const MAX: Self;
/// Valeur zéro
const ZERO: Self;
/// Convertit le sample en f64 normalisé dans [-1.0, 1.0]
fn to_f64(self) -> f64;
/// Crée un sample depuis un f64 normalisé dans [-1.0, 1.0]
fn from_f64(value: f64) -> Self;
/// Convertit le sample en f32 normalisé dans [-1.0, 1.0]
fn to_f32(self) -> f32 {
self.to_f64() as f32
}
/// Crée un sample depuis un f32 normalisé dans [-1.0, 1.0]
fn from_f32(value: f32) -> Self {
Self::from_f64(value as f64)
}
}
// ============================================================================
// Type I24 : Échantillon audio 24-bit stocké dans un i32
// ============================================================================
/// Échantillon audio 24-bit signé, stocké dans un i32
///
/// Représente un sample audio de 24 bits de résolution effective,
/// stocké sur 32 bits pour l'alignement et les performances.
///
/// Plage valide : [-8_388_608, 8_388_607] (±2^23)
///
/// # Exemples
///
/// ```
/// use pmoaudio::I24;
///
/// let sample = I24::new(1_000_000).unwrap();
/// assert_eq!(sample.as_i32(), 1_000_000);
///
/// // Hors plage : erreur
/// assert!(I24::new(10_000_000).is_none());
/// ```
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct I24(i32);
impl I24 {
/// Valeur minimale : -2^23
pub const MIN_VALUE: i32 = -8_388_608;
/// Valeur maximale : 2^23 - 1
pub const MAX_VALUE: i32 = 8_388_607;
/// Valeur zéro
pub const ZERO: I24 = I24(0);
/// Valeur minimale
pub const MIN: I24 = I24(Self::MIN_VALUE);
/// Valeur maximale
pub const MAX: I24 = I24(Self::MAX_VALUE);
/// Crée un nouveau I24 depuis un i32, en vérifiant la plage valide
///
/// # Exemples
///
/// ```
/// use pmoaudio::I24;
///
/// assert!(I24::new(0).is_some());
/// assert!(I24::new(8_388_607).is_some());
/// assert!(I24::new(-8_388_608).is_some());
/// assert!(I24::new(10_000_000).is_none()); // Hors plage
/// ```
#[inline]
pub const fn new(value: i32) -> Option<Self> {
if value >= Self::MIN_VALUE && value <= Self::MAX_VALUE {
Some(I24(value))
} else {
None
}
}
/// Crée un nouveau I24 depuis un i32, en clampant à la plage valide
///
/// # Exemples
///
/// ```
/// use pmoaudio::I24;
///
/// assert_eq!(I24::new_clamped(10_000_000).as_i32(), 8_388_607);
/// assert_eq!(I24::new_clamped(-10_000_000).as_i32(), -8_388_608);
/// ```
#[inline]
pub const fn new_clamped(value: i32) -> Self {
let clamped = if value < Self::MIN_VALUE {
Self::MIN_VALUE
} else if value > Self::MAX_VALUE {
Self::MAX_VALUE
} else {
value
};
I24(clamped)
}
/// Crée un nouveau I24 depuis un i32 sans vérification
///
/// # Safety
///
/// Le caller doit garantir que `value` est dans [-8_388_608, 8_388_607]
#[inline]
pub const unsafe fn new_unchecked(value: i32) -> Self {
I24(value)
}
/// Retourne la valeur i32 interne
#[inline]
pub const fn as_i32(self) -> i32 {
self.0
}
/// Retourne la valeur i32 interne (alias pour compatibilité)
#[inline]
pub const fn get(self) -> i32 {
self.0
}
}
impl fmt::Debug for I24 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "I24({})", self.0)
}
}
impl fmt::Display for I24 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<I24> for i32 {
#[inline]
fn from(i24: I24) -> i32 {
i24.0
}
}
impl TryFrom<i32> for I24 {
type Error = &'static str;
#[inline]
fn try_from(value: i32) -> Result<Self, Self::Error> {
I24::new(value).ok_or("i32 value out of I24 range")
}
}
// ============================================================================
// Implémentations du trait Sample pour tous les types
// ============================================================================
impl Sample for i8 {
const NAME: &'static str = "i8";
const MIN: Self = i8::MIN;
const MAX: Self = i8::MAX;
const ZERO: Self = 0;
#[inline]
fn to_f64(self) -> f64 {
self as f64 / 128.0
}
#[inline]
fn from_f64(value: f64) -> Self {
(value * 127.0).clamp(-128.0, 127.0).round() as i8
}
}
impl Sample for i16 {
const NAME: &'static str = "i16";
const MIN: Self = i16::MIN;
const MAX: Self = i16::MAX;
const ZERO: Self = 0;
#[inline]
fn to_f64(self) -> f64 {
self as f64 / 32_768.0
}
#[inline]
fn from_f64(value: f64) -> Self {
(value * 32_767.0).clamp(-32_768.0, 32_767.0).round() as i16
}
}
impl Sample for I24 {
const NAME: &'static str = "I24";
const MIN: Self = I24::MIN;
const MAX: Self = I24::MAX;
const ZERO: Self = I24::ZERO;
#[inline]
fn to_f64(self) -> f64 {
self.0 as f64 / 8_388_608.0
}
#[inline]
fn from_f64(value: f64) -> Self {
let scaled = (value * 8_388_607.0).clamp(-8_388_608.0, 8_388_607.0).round() as i32;
I24(scaled)
}
}
impl Sample for i32 {
const NAME: &'static str = "i32";
const MIN: Self = i32::MIN;
const MAX: Self = i32::MAX;
const ZERO: Self = 0;
#[inline]
fn to_f64(self) -> f64 {
self as f64 / 2_147_483_648.0
}
#[inline]
fn from_f64(value: f64) -> Self {
(value * 2_147_483_647.0).clamp(-2_147_483_648.0, 2_147_483_647.0).round() as i32
}
}
impl Sample for f32 {
const NAME: &'static str = "f32";
const MIN: Self = -1.0;
const MAX: Self = 1.0;
const ZERO: Self = 0.0;
#[inline]
fn to_f64(self) -> f64 {
self as f64
}
#[inline]
fn from_f64(value: f64) -> Self {
value as f32
}
#[inline]
fn to_f32(self) -> f32 {
self
}
#[inline]
fn from_f32(value: f32) -> Self {
value
}
}
impl Sample for f64 {
const NAME: &'static str = "f64";
const MIN: Self = -1.0;
const MAX: Self = 1.0;
const ZERO: Self = 0.0;
#[inline]
fn to_f64(self) -> f64 {
self
}
#[inline]
fn from_f64(value: f64) -> Self {
value
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_i24_creation() {
assert_eq!(I24::new(0).unwrap().as_i32(), 0);
assert_eq!(I24::new(8_388_607).unwrap().as_i32(), 8_388_607);
assert_eq!(I24::new(-8_388_608).unwrap().as_i32(), -8_388_608);
assert!(I24::new(8_388_608).is_none());
assert!(I24::new(-8_388_609).is_none());
assert!(I24::new(10_000_000).is_none());
}
#[test]
fn test_i24_clamped() {
assert_eq!(I24::new_clamped(10_000_000).as_i32(), 8_388_607);
assert_eq!(I24::new_clamped(-10_000_000).as_i32(), -8_388_608);
assert_eq!(I24::new_clamped(1_000_000).as_i32(), 1_000_000);
}
#[test]
fn test_sample_trait_i24() {
let sample = I24::new(4_194_303).unwrap(); // ~0.5 en normalized
let normalized = sample.to_f64();
assert!((normalized - 0.5).abs() < 0.001);
let back = I24::from_f64(0.5);
assert!((back.as_i32() - 4_194_303).abs() <= 1); // Tolérance d'arrondi
}
#[test]
fn test_sample_trait_roundtrip_i16() {
let original: i16 = 16_000;
let normalized = original.to_f64();
let back = i16::from_f64(normalized);
assert!((back - original).abs() <= 1);
}
#[test]
fn test_sample_trait_roundtrip_f32() {
let original: f32 = 0.75;
let normalized = original.to_f64();
let back = f32::from_f64(normalized);
assert!((back - original).abs() < 1e-6);
}
}

View File

@@ -0,0 +1,13 @@
use std::sync::Arc;
use pmometadata::TrackMetadata;
pub enum SyncMarker {
TrackBoundary { metadata: Arc<dyn TrackMetadata> },
StreamMetadata { key: String, value: String },
TopZeroSync,
Heartbeat,
EndOfStream,
Error(String),
// autres cas à venir…
}