From 8fe110eeb7df059fe4dd9bbb8363406bc7c7e27a Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Thu, 30 Oct 2025 08:54:47 +0100 Subject: [PATCH] Restructuration de pmoaudio avec ajout des messages de synchro --- .DS_Store | Bin 10244 -> 10244 bytes .../pmoaudio}/nodes/buffer_node.rs | 20 +- .../pmoaudio}/nodes/chromecast_sink.rs | 0 .../pmoaudio}/nodes/decoder_node.rs | 0 .../pmoaudio}/nodes/disk_sink.rs | 0 .../pmoaudio}/nodes/dsp_node.rs | 0 .../pmoaudio}/nodes/file_source.rs | 0 .../pmoaudio}/nodes/flac_file_sink.rs | 0 .../src => old_code/pmoaudio}/nodes/mod.rs | 21 +- .../pmoaudio}/nodes/mpd_sink.rs | 0 .../pmoaudio}/nodes/sink_node.rs | 0 .../pmoaudio}/nodes/source_node.rs | 0 .../pmoaudio}/nodes/timer_node.rs | 0 .../pmoaudio}/nodes/volume_node.rs | 0 pmoaudio/Cargo.toml | 1 + pmoaudio/REFACTORING_SUMMARY.md | 342 +++++++ pmoaudio/examples/audio_chunk_api.rs | 194 ++++ pmoaudio/src/audio_chunk.rs | 697 +++++++------ pmoaudio/src/audio_segment.rs | 526 ++++++++++ pmoaudio/src/conversions.rs | 932 ++++++++++++++++++ pmoaudio/src/dsp/int_float.rs | 111 +-- pmoaudio/src/dsp/mod.rs | 2 + pmoaudio/src/dsp/resampling.rs | 59 +- pmoaudio/src/lib.rs | 19 +- pmoaudio/src/macros.rs | 304 ++++++ pmoaudio/src/sample_types.rs | 342 +++++++ pmoaudio/src/sync_marker.rs | 13 + 27 files changed, 3183 insertions(+), 400 deletions(-) rename {pmoaudio/src => old_code/pmoaudio}/nodes/buffer_node.rs (94%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/chromecast_sink.rs (100%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/decoder_node.rs (100%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/disk_sink.rs (100%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/dsp_node.rs (100%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/file_source.rs (100%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/flac_file_sink.rs (100%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/mod.rs (83%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/mpd_sink.rs (100%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/sink_node.rs (100%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/source_node.rs (100%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/timer_node.rs (100%) rename {pmoaudio/src => old_code/pmoaudio}/nodes/volume_node.rs (100%) create mode 100644 pmoaudio/REFACTORING_SUMMARY.md create mode 100644 pmoaudio/examples/audio_chunk_api.rs create mode 100644 pmoaudio/src/audio_segment.rs create mode 100644 pmoaudio/src/conversions.rs create mode 100644 pmoaudio/src/macros.rs create mode 100644 pmoaudio/src/sample_types.rs create mode 100644 pmoaudio/src/sync_marker.rs diff --git a/.DS_Store b/.DS_Store index 0081602c0829f90d4aea6a6e21804f096e4f1c9a..eb3eed6f94fdab2864329514a575aa0be28a52d9 100644 GIT binary patch delta 35 gcmZn(XbIR5EXHAKX{4iIVq&y8TI>fuGH)d#0J$p(*8l(j delta 35 gcmZn(XbIR5EXHAIVydHHWNxrITI>fuGH)d#0Jt3q$p8QV diff --git a/pmoaudio/src/nodes/buffer_node.rs b/old_code/pmoaudio/nodes/buffer_node.rs similarity index 94% rename from pmoaudio/src/nodes/buffer_node.rs rename to old_code/pmoaudio/nodes/buffer_node.rs index e550d101..e0249349 100644 --- a/pmoaudio/src/nodes/buffer_node.rs +++ b/old_code/pmoaudio/nodes/buffer_node.rs @@ -1,6 +1,6 @@ use crate::{ nodes::{AudioError, MultiSubscriberNode}, - AudioChunk, + AudioSegment, }; use std::collections::VecDeque; use std::sync::Arc; @@ -8,7 +8,7 @@ use tokio::sync::{mpsc, RwLock}; /// Subscriber avec son propre offset dans le buffer struct BufferSubscriber { - tx: mpsc::Sender>, + tx: mpsc::Sender>, offset: usize, // Position dans le buffer circulaire } @@ -48,10 +48,10 @@ struct BufferSubscriber { /// } /// ``` pub struct BufferNode { - buffer: Arc>>>, + buffer: Arc>>>, subscribers: Arc>>, buffer_size: usize, - rx: mpsc::Receiver>, + rx: mpsc::Receiver>, next_subscribers: MultiSubscriberNode, // Pour passer au node suivant } @@ -61,7 +61,7 @@ impl 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>) { + pub fn new(buffer_size: usize, channel_size: usize) -> (Self, mpsc::Sender>) { let (tx, rx) = mpsc::channel(channel_size); let node = Self { @@ -78,7 +78,7 @@ impl BufferNode { /// Ajoute un abonné avec un offset spécifique (pour multiroom) pub async fn add_subscriber_with_offset( &self, - tx: mpsc::Sender>, + tx: mpsc::Sender>, offset: usize, ) { let mut subs = self.subscribers.write().await; @@ -86,12 +86,12 @@ impl BufferNode { } /// Ajoute un abonné sans offset (commence au chunk courant) - pub async fn add_subscriber(&self, tx: mpsc::Sender>) { + pub async fn add_subscriber(&self, tx: mpsc::Sender>) { 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>) { + pub fn add_next_subscriber(&mut self, tx: mpsc::Sender>) { self.next_subscribers.add_subscriber(tx); } @@ -206,7 +206,7 @@ mod tests { // Envoyer des chunks for i in 0..3 { - let chunk = AudioChunk::new(i, vec![[0i32; 2]; 100], 48000, BitDepth::B24); + let chunk = AudioSegment::AudioChunk(AudioChunk::new(i, vec![[0i32; 2]; 100], 48000, BitDepth::B24)); tx.send(chunk).await.unwrap(); } @@ -232,7 +232,7 @@ mod tests { // Envoyer 5 chunks for i in 0..5 { - let chunk = AudioChunk::new(i, vec![[0i32; 2]; 100], 48000, BitDepth::B24); + let chunk = AudioSegment::new(i, vec![[0i32; 2]; 100], 48000, BitDepth::B24); tx.send(chunk).await.unwrap(); } diff --git a/pmoaudio/src/nodes/chromecast_sink.rs b/old_code/pmoaudio/nodes/chromecast_sink.rs similarity index 100% rename from pmoaudio/src/nodes/chromecast_sink.rs rename to old_code/pmoaudio/nodes/chromecast_sink.rs diff --git a/pmoaudio/src/nodes/decoder_node.rs b/old_code/pmoaudio/nodes/decoder_node.rs similarity index 100% rename from pmoaudio/src/nodes/decoder_node.rs rename to old_code/pmoaudio/nodes/decoder_node.rs diff --git a/pmoaudio/src/nodes/disk_sink.rs b/old_code/pmoaudio/nodes/disk_sink.rs similarity index 100% rename from pmoaudio/src/nodes/disk_sink.rs rename to old_code/pmoaudio/nodes/disk_sink.rs diff --git a/pmoaudio/src/nodes/dsp_node.rs b/old_code/pmoaudio/nodes/dsp_node.rs similarity index 100% rename from pmoaudio/src/nodes/dsp_node.rs rename to old_code/pmoaudio/nodes/dsp_node.rs diff --git a/pmoaudio/src/nodes/file_source.rs b/old_code/pmoaudio/nodes/file_source.rs similarity index 100% rename from pmoaudio/src/nodes/file_source.rs rename to old_code/pmoaudio/nodes/file_source.rs diff --git a/pmoaudio/src/nodes/flac_file_sink.rs b/old_code/pmoaudio/nodes/flac_file_sink.rs similarity index 100% rename from pmoaudio/src/nodes/flac_file_sink.rs rename to old_code/pmoaudio/nodes/flac_file_sink.rs diff --git a/pmoaudio/src/nodes/mod.rs b/old_code/pmoaudio/nodes/mod.rs similarity index 83% rename from pmoaudio/src/nodes/mod.rs rename to old_code/pmoaudio/nodes/mod.rs index 0c55288a..2386b203 100644 --- a/pmoaudio/src/nodes/mod.rs +++ b/old_code/pmoaudio/nodes/mod.rs @@ -3,10 +3,11 @@ //! 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; +use crate::AudioSegment; + pub mod buffer_node; pub mod chromecast_sink; pub mod decoder_node; @@ -31,7 +32,7 @@ pub trait AudioNode: Send + Sync { /// # Erreurs /// /// Retourne `AudioError::SendError` si l'envoi échoue - async fn push(&mut self, chunk: Arc) -> Result<(), AudioError>; + async fn push(&mut self, chunk: Arc) -> Result<(), AudioError>; /// Ferme le node proprement async fn close(&mut self); @@ -52,15 +53,15 @@ pub trait AudioNode: Send + Sync { /// let node = SingleSubscriberNode::new(tx); /// ``` pub struct SingleSubscriberNode { - tx: mpsc::Sender>, + tx: mpsc::Sender>, } impl SingleSubscriberNode { - pub fn new(tx: mpsc::Sender>) -> Self { + pub fn new(tx: mpsc::Sender>) -> Self { Self { tx } } - pub async fn push(&self, chunk: Arc) -> Result<(), AudioError> { + pub async fn push(&self, chunk: Arc) -> Result<(), AudioError> { self.tx.send(chunk).await.map_err(|_| AudioError::SendError) } } @@ -68,7 +69,7 @@ impl SingleSubscriberNode { /// 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`, donc pas de copie +/// Tous les abonnés reçoivent le même `Arc`, donc pas de copie /// des données audio - seul le compteur de référence Arc est incrémenté. /// /// # Exemples @@ -86,7 +87,7 @@ impl SingleSubscriberNode { /// // Les deux abonnés recevront les mêmes chunks /// ``` pub struct MultiSubscriberNode { - subscribers: Vec>>, + subscribers: Vec>>, } impl MultiSubscriberNode { @@ -96,11 +97,11 @@ impl MultiSubscriberNode { } } - pub fn add_subscriber(&mut self, tx: mpsc::Sender>) { + pub fn add_subscriber(&mut self, tx: mpsc::Sender>) { self.subscribers.push(tx); } - pub async fn push(&self, chunk: Arc) -> Result<(), AudioError> { + pub async fn push(&self, chunk: Arc) -> Result<(), AudioError> { for tx in &self.subscribers { // On partage le même Arc avec tous les abonnés tx.send(chunk.clone()) @@ -110,7 +111,7 @@ impl MultiSubscriberNode { Ok(()) } - pub async fn try_push(&self, chunk: Arc) -> Result<(), AudioError> { + pub async fn try_push(&self, chunk: Arc) -> Result<(), AudioError> { for tx in &self.subscribers { // try_send non-bloquant, ignore si saturé let _ = tx.try_send(chunk.clone()); diff --git a/pmoaudio/src/nodes/mpd_sink.rs b/old_code/pmoaudio/nodes/mpd_sink.rs similarity index 100% rename from pmoaudio/src/nodes/mpd_sink.rs rename to old_code/pmoaudio/nodes/mpd_sink.rs diff --git a/pmoaudio/src/nodes/sink_node.rs b/old_code/pmoaudio/nodes/sink_node.rs similarity index 100% rename from pmoaudio/src/nodes/sink_node.rs rename to old_code/pmoaudio/nodes/sink_node.rs diff --git a/pmoaudio/src/nodes/source_node.rs b/old_code/pmoaudio/nodes/source_node.rs similarity index 100% rename from pmoaudio/src/nodes/source_node.rs rename to old_code/pmoaudio/nodes/source_node.rs diff --git a/pmoaudio/src/nodes/timer_node.rs b/old_code/pmoaudio/nodes/timer_node.rs similarity index 100% rename from pmoaudio/src/nodes/timer_node.rs rename to old_code/pmoaudio/nodes/timer_node.rs diff --git a/pmoaudio/src/nodes/volume_node.rs b/old_code/pmoaudio/nodes/volume_node.rs similarity index 100% rename from pmoaudio/src/nodes/volume_node.rs rename to old_code/pmoaudio/nodes/volume_node.rs diff --git a/pmoaudio/Cargo.toml b/pmoaudio/Cargo.toml index 0bb6aab2..4f8f5358 100644 --- a/pmoaudio/Cargo.toml +++ b/pmoaudio/Cargo.toml @@ -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" diff --git a/pmoaudio/REFACTORING_SUMMARY.md b/pmoaudio/REFACTORING_SUMMARY.md new file mode 100644 index 00000000..04202f1d --- /dev/null +++ b/pmoaudio/REFACTORING_SUMMARY.md @@ -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`**: 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 { + stereo: Arc<[[T; 2]]>, + sample_rate: u32, + gain_db: f64, // Toujours en dB +} + +pub enum AudioChunk { + I8(Arc>), + I16(Arc>), + I24(Arc>), + I32(Arc>), + F32(Arc>), + F64(Arc>), +} +``` + +**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> = (&*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 diff --git a/pmoaudio/examples/audio_chunk_api.rs b/pmoaudio/examples/audio_chunk_api.rs new file mode 100644 index 00000000..a96a3ac2 --- /dev/null +++ b/pmoaudio/examples/audio_chunk_api.rs @@ -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::::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> = (&*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!(); +} diff --git a/pmoaudio/src/audio_chunk.rs b/pmoaudio/src/audio_chunk.rs index 3a6dd971..befd000c 100644 --- a/pmoaudio/src/audio_chunk.rs +++ b/pmoaudio/src/audio_chunk.rs @@ -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 : 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>` 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 d’ordre dans le flux. - /// Sert à conserver la séquence et détecter d’éventuelles pertes. - order: u64, +pub struct AudioChunkData { + /// 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 d’adapter 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 AudioChunkData { /// 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 { + pub fn new(stereo: Vec<[T; 2]>, sample_rate: u32, gain_db: f64) -> Arc { 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::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::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, - right: Vec, - sample_rate: u32, - bit_depth: BitDepth, - ) -> Arc { - 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, - right: Vec, - sample_rate: u32, - bit_depth: BitDepth, - ) -> Arc { - 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 { - 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) -> Arc { - 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 { + /// 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 { 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.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.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.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 { + /// 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) -> Arc { + 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, new_depth: BitDepth) -> Arc { - if self.bit_depth == new_depth { + + /// Construit un chunk depuis deux vecteurs `i32` séparés (L/R) + pub fn from_channels(left: Vec, right: Vec, sample_rate: u32) -> Arc { + 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, old_depth: BitDepth, new_depth: BitDepth) -> Arc { + 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 { + /// Applique le gain et retourne un nouveau chunk avec les données modifiées + pub fn apply_gain(self: Arc) -> Arc { + 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, right: Vec, sample_rate: u32) -> Arc { + 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 { + /// Applique le gain et retourne un nouveau chunk avec les données modifiées + pub fn apply_gain(self: Arc) -> Arc { + 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, right: Vec, sample_rate: u32) -> Arc { + 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>), + I16(Arc>), + I24(Arc>), + I32(Arc>), + F32(Arc>), + F64(Arc>), } +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); + } +} diff --git a/pmoaudio/src/audio_segment.rs b/pmoaudio/src/audio_segment.rs new file mode 100644 index 00000000..16244a6e --- /dev/null +++ b/pmoaudio/src/audio_segment.rs @@ -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), + Sync(Arc), +} + +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 { + 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 { + 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 { + 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, + right: Vec, + sample_rate: u32, + _bit_depth: BitDepth, // Conservé pour compatibilité API + ) -> Arc { + let chunk_data = AudioChunkData::::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, + right: Vec, + sample_rate: u32, + bit_depth: BitDepth, + ) -> Arc { + 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 { + // 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) -> Arc { + 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 { + 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 { + 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 { + 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 { + 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 { + 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> { + 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> { + 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> { + 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 { + 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 { + self.as_chunk().map(|chunk| chunk.to_i32()) + } + + /// Récupère le sample rate du chunk audio + pub fn sample_rate(&self) -> Option { + self.as_chunk().map(|chunk| chunk.sample_rate()) + } + + /// Récupère le nombre de frames du chunk audio + pub fn frame_count(&self) -> Option { + self.as_chunk().map(|chunk| chunk.len()) + } + + /// Récupère le gain en dB du chunk audio + pub fn gain_db(&self) -> Option { + 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> { + 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> { + 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> for AudioSegment { + type Error = (); + + fn try_into(self) -> Result, Self::Error> { + match self.segment { + _AudioSegment::Chunk(chunk) => Ok(chunk), + _ => Err(()), + } + } +} + +impl TryInto> for AudioSegment { + type Error = (); + + fn try_into(self) -> Result, Self::Error> { + match self.segment { + _AudioSegment::Sync(marker) => Ok(marker), + _ => Err(()), + } + } +} + +impl<'a> TryInto<&'a Arc> for &'a AudioSegment { + type Error = (); + + fn try_into(self) -> Result<&'a Arc, Self::Error> { + match &self.segment { + _AudioSegment::Chunk(ref chunk) => Ok(chunk), + _ => Err(()), + } + } +} + +impl<'a> TryInto<&'a Arc> for &'a AudioSegment { + type Error = (); + + fn try_into(self) -> Result<&'a Arc, 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); + } +} diff --git a/pmoaudio/src/conversions.rs b/pmoaudio/src/conversions.rs new file mode 100644 index 00000000..716e1d91 --- /dev/null +++ b/pmoaudio/src/conversions.rs @@ -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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + // 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) -> Arc> { + // 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) -> Arc> { + // 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) -> Arc> { + 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) -> Arc> { + // 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + // 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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) -> Arc> { + 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>> pour AudioChunk ---------- + +impl From>> for AudioChunk { + fn from(data: Arc>) -> Self { + AudioChunk::I8(data) + } +} + +impl From>> for AudioChunk { + fn from(data: Arc>) -> Self { + AudioChunk::I16(data) + } +} + +impl From>> for AudioChunk { + fn from(data: Arc>) -> Self { + AudioChunk::I24(data) + } +} + +impl From>> for AudioChunk { + fn from(data: Arc>) -> Self { + AudioChunk::I32(data) + } +} + +impl From>> for AudioChunk { + fn from(data: Arc>) -> Self { + AudioChunk::F32(data) + } +} + +impl From>> for AudioChunk { + fn from(data: Arc>) -> Self { + AudioChunk::F64(data) + } +} + +// ---------- From entre AudioChunkData types (sans BitDepth requis) ---------- + +// I8 conversions +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i8_to_i32(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i8_to_f32(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i8_to_f64(chunk) + } +} + +// I16 conversions +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i16_to_i32(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i16_to_f32(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i16_to_f64(chunk) + } +} + +// I24 conversions +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i24_to_i32(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i24_to_f32(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i24_to_f64(chunk) + } +} + +// I32 conversions vers types int (downsampling) +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i32_to_i8(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i32_to_i16(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i32_to_i24(chunk) + } +} + +// I32 conversions vers float (normalisation par 2^31) +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i32_to_f32(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_i32_to_f64(chunk) + } +} + +// F32 conversions +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_f32_to_f64(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_f32_to_i8(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_f32_to_i16(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_f32_to_i24(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_f32_to_i32(chunk) + } +} + +// F64 conversions +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_f64_to_f32(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_f64_to_i8(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_f64_to_i16(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> Self { + convert_f64_to_i24(chunk) + } +} + +impl From<&AudioChunkData> for Arc> { + fn from(chunk: &AudioChunkData) -> 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>> 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> = (&*chunk_i16).into(); + assert_eq!(chunk_i32.len(), 50); + + // I16 → F32 via From + let chunk_f32: Arc> = (&*chunk_i16).into(); + assert_eq!(chunk_f32.len(), 50); + + // I16 → F64 via From + let chunk_f64: Arc> = (&*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> = (&*chunk_i24).into(); + assert_eq!(chunk_i32.len(), 50); + + // I24 → F32 via From + let chunk_f32: Arc> = (&*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> = (&*chunk_f32).into(); + assert_eq!(chunk_f64.len(), 50); + + // F32 → I16 via From + let chunk_i16: Arc> = (&*chunk_f32).into(); + assert_eq!(chunk_i16.len(), 50); + + // F32 → I24 via From + let chunk_i24: Arc> = (&*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> = (&*chunk_i24).into(); + + // F32 → I24 via From + let chunk_back: Arc> = (&*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> = (&*chunk_i32).into(); + assert_eq!(chunk_f32.len(), 50); + + // I32 → F64 via From + let chunk_f64: Arc> = (&*chunk_i32).into(); + assert_eq!(chunk_f64.len(), 50); + + // F32 → I32 via From (quantization vers 2^31) + let chunk_back_i32: Arc> = (&*chunk_f32).into(); + assert_eq!(chunk_back_i32.len(), 50); + } +} diff --git a/pmoaudio/src/dsp/int_float.rs b/pmoaudio/src/dsp/int_float.rs index 037f57e7..a790c907 100644 --- a/pmoaudio/src/dsp/int_float.rs +++ b/pmoaudio/src/dsp/int_float.rs @@ -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!(::MAX_VALUE, 127.0); -/// ``` -macro_rules! BitMax { - ($bits:literal) => { - paste::paste! { - pub struct []; - - impl BitDepth for [] { - 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( +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( type Vf32 = Simd; type Vi32 = Simd; - 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::(); let (r_chunks, r_tail) = right.as_chunks::(); @@ -69,47 +41,59 @@ pub fn i32_stereo_to_pairs_f32( } } + 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( +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( +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; - type Vi32 = Simd; - 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 l’overflow après round→cast + let vmaxv = Vf32::splat(vmax_clamp); let (in_chunks, in_tail) = input_pairs.as_chunks::(); let (l_chunks, l_tail) = left.as_chunks_mut::(); @@ -137,52 +121,65 @@ pub fn pairs_f32_to_i32_stereo( } 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( +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( +/// 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::(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( +/// 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::(input_pairs, left, right); + pairs_f32_to_i32_stereo(input_pairs, left, right, bit_depth); } diff --git a/pmoaudio/src/dsp/mod.rs b/pmoaudio/src/dsp/mod.rs index 074efde5..83a04541 100644 --- a/pmoaudio/src/dsp/mod.rs +++ b/pmoaudio/src/dsp/mod.rs @@ -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; diff --git a/pmoaudio/src/dsp/resampling.rs b/pmoaudio/src/dsp/resampling.rs index 12ae4781..5988a6e3 100644 --- a/pmoaudio/src/dsp/resampling.rs +++ b/pmoaudio/src/dsp/resampling.rs @@ -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>, } pub fn build_resampler( source_hz: u32, dest_hz: u32, - bit_depth: u32, -) -> Result { + bit_depth: BitDepth, +) -> Result { 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::>::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::(left, right, &mut input), - 16 => i32_stereo_to_pairs_f32::(left, right, &mut input), - 24 => i32_stereo_to_pairs_f32::(left, right, &mut input), - 32 => i32_stereo_to_pairs_f32::(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::(&output, &mut oleft, &mut oright), - 16 => pairs_f32_to_i32_stereo::(&output, &mut oleft, &mut oright), - 24 => pairs_f32_to_i32_stereo::(&output, &mut oleft, &mut oright), - 32 => pairs_f32_to_i32_stereo::(&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) } diff --git a/pmoaudio/src/lib.rs b/pmoaudio/src/lib.rs index 3c641184..bc2f309c 100644 --- a/pmoaudio/src/lib.rs +++ b/pmoaudio/src/lib.rs @@ -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, }; +*/ diff --git a/pmoaudio/src/macros.rs b/pmoaudio/src/macros.rs new file mode 100644 index 00000000..ad2b60c3 --- /dev/null +++ b/pmoaudio/src/macros.rs @@ -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> { +/// 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> { +/// 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"); + } +} diff --git a/pmoaudio/src/sample_types.rs b/pmoaudio/src/sample_types.rs new file mode 100644 index 00000000..ff6a8002 --- /dev/null +++ b/pmoaudio/src/sample_types.rs @@ -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 { + 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 for i32 { + #[inline] + fn from(i24: I24) -> i32 { + i24.0 + } +} + +impl TryFrom for I24 { + type Error = &'static str; + + #[inline] + fn try_from(value: i32) -> Result { + 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); + } +} diff --git a/pmoaudio/src/sync_marker.rs b/pmoaudio/src/sync_marker.rs new file mode 100644 index 00000000..679901d1 --- /dev/null +++ b/pmoaudio/src/sync_marker.rs @@ -0,0 +1,13 @@ +use std::sync::Arc; + +use pmometadata::TrackMetadata; + +pub enum SyncMarker { + TrackBoundary { metadata: Arc }, + StreamMetadata { key: String, value: String }, + TopZeroSync, + Heartbeat, + EndOfStream, + Error(String), + // autres cas à venir… +}