//! AudioChunk : Représentation générique de données audio stéréo //! //! Cette nouvelle architecture supporte différents types de samples : //! - Entiers : 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, Sample, I24}; // ============================================================================ // 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 générique encapsule des données audio de n'importe quel type /// de sample (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 /// /// - 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::{AudioChunkData, I24}; /// /// // 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(), 1000); /// assert_eq!(chunk.sample_rate(), 48_000); /// ``` #[derive(Debug, Clone)] pub struct AudioChunkData { /// Frames stéréo [L, R], partagées et immuables via Arc stereo: Arc<[[T; 2]]>, /// Taux d'échantillonnage en Hz (44100, 48000, 96000, 192000, etc.) sample_rate: u32, /// 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_db: f64, } impl AudioChunkData { /// Crée un nouveau chunk audio /// /// Les vecteurs sont automatiquement wrappés dans `Arc`. /// /// # Arguments /// /// * `stereo` - Frames stéréo `[L, R]` /// * `sample_rate` - Taux d'échantillonnage en Hz /// * `gain_db` - Gain initial en décibels (0.0 = unity gain) /// /// # Exemples /// /// ``` /// use pmoaudio::AudioChunkData; /// /// let chunk = AudioChunkData::new( /// vec![[0.0f32, 0.0f32]; 1000], /// 48_000, /// 0.0, /// ); /// ``` pub fn new(stereo: Vec<[T; 2]>, sample_rate: u32, gain_db: f64) -> Arc { Arc::new(Self { stereo: Arc::from(stereo), sample_rate, gain_db, }) } /// Crée un chunk de silence /// /// # Arguments /// /// * `frames` - Nombre de frames (échantillons par canal) /// * `sample_rate` - Taux d'échantillonnage en Hz /// /// # Exemples /// /// ``` /// use pmoaudio::AudioChunkData; /// /// let silence = AudioChunkData::::silence(48000, 48000); /// assert_eq!(silence.len(), 48000); /// ``` pub fn silence(frames: usize, sample_rate: u32) -> Arc { Self::new(vec![[T::ZERO; 2]; frames], sample_rate, 0.0) } /// 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() } /// Taux d'échantillonnage (Hz) #[inline] pub fn get_sample_rate(&self) -> u32 { self.sample_rate } /// Gain courant en décibels #[inline] pub fn get_gain_db(&self) -> f64 { self.gain_db } /// Gain sous forme linéaire #[inline] pub fn get_gain_linear(&self) -> f64 { gain_linear_from_db(self.gain_db) } /// Retourne une vue immuable sur les frames `[L, R]` #[inline] pub fn get_frames(&self) -> &[[T; 2]] { &self.stereo } /// Clone les frames stéréo dans un `Vec` #[inline] pub fn clone_frames(&self) -> Vec<[T; 2]> { self.stereo.to_vec() } /// Définit le gain (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. pub fn set_gain_db(&self, gain_db: f64) -> Arc { Arc::new(Self { stereo: self.stereo.clone(), sample_rate: self.sample_rate, gain_db, }) } /// 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(gain_db_from_linear(gain_linear)) } /// 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_db + delta_gain_db) } /// 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(gain_db_from_linear(gain_linear)) } } // Méthodes spécifiques pour les types entiers (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_i32(&mut stereo, self.gain_db); AudioChunkData::new(stereo, self.sample_rate, 0.0) } /// 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_frames(); dsp::bitdepth_change_stereo(&mut stereo, old_depth, new_depth); Arc::new(Self { stereo: Arc::from(stereo), sample_rate: self.sample_rate, gain_db: self.gain_db, }) } } // 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 } let gain_linear = gain_linear_from_db(self.gain_db) as f32; 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 `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) } } // 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 = gain_linear_from_db(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) } } // ============================================================================ // 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 /// /// - `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 { I16(Arc>), I24(Arc>), I32(Arc>), F32(Arc>), F64(Arc>), } impl AudioChunk { /// Crée un chunk de silence (I32 par défaut) pub fn silence(frames: usize, sample_rate: u32) -> Self { AudioChunk::I32(AudioChunkData::::silence(frames, sample_rate)) } /// Retourne le nombre de frames du chunk pub fn len(&self) -> usize { match self { 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::I16(d) => d.get_sample_rate(), AudioChunk::I24(d) => d.get_sample_rate(), AudioChunk::I32(d) => d.get_sample_rate(), AudioChunk::F32(d) => d.get_sample_rate(), AudioChunk::F64(d) => d.get_sample_rate(), } } /// Gain courant en décibels pub fn gain_db(&self) -> f64 { match self { AudioChunk::I16(d) => d.get_gain_db(), AudioChunk::I24(d) => d.get_gain_db(), AudioChunk::I32(d) => d.get_gain_db(), AudioChunk::F32(d) => d.get_gain_db(), AudioChunk::F64(d) => d.get_gain_db(), } } /// Gain sous forme linéaire pub fn gain_linear(&self) -> f64 { gain_linear_from_db(self.gain_db()) } /// Définit le gain en dB pub fn set_gain_db(&self, gain_db: f64) -> Self { match self { 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(gain_db_from_linear(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::I16(d) => { let gain_db = d.get_gain_db(); if gain_db.abs() < f64::EPSILON { return AudioChunk::I16(d); } let gain_linear = gain_linear_from_db(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.get_sample_rate(), 0.0)) } AudioChunk::I24(d) => { let gain_db = d.get_gain_db(); if gain_db.abs() < f64::EPSILON { return AudioChunk::I24(d); } let gain_linear = gain_linear_from_db(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.get_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::I16(_) => "i16", AudioChunk::I24(_) => "I24", AudioChunk::I32(_) => "i32", AudioChunk::F32(_) => "f32", AudioChunk::F64(_) => "f64", } } /// Tente de convertir vers AudioIntegerChunk (retourne None si float) pub fn try_as_integer(&self) -> Option { match self { AudioChunk::I16(d) => Some(AudioIntegerChunk::I16(d.clone())), AudioChunk::I24(d) => Some(AudioIntegerChunk::I24(d.clone())), AudioChunk::I32(d) => Some(AudioIntegerChunk::I32(d.clone())), AudioChunk::F32(_) | AudioChunk::F64(_) => None, } } /// Tente de convertir vers AudioFloatChunk (retourne None si integer) pub fn try_as_float(&self) -> Option { match self { AudioChunk::F32(d) => Some(AudioFloatChunk::F32(d.clone())), AudioChunk::F64(d) => Some(AudioFloatChunk::F64(d.clone())), AudioChunk::I16(_) | AudioChunk::I24(_) | AudioChunk::I32(_) => None, } } /// Vérifie si le chunk est de type entier pub fn is_integer(&self) -> bool { matches!( self, AudioChunk::I16(_) | AudioChunk::I24(_) | AudioChunk::I32(_) ) } /// Vérifie si le chunk est de type flottant pub fn is_float(&self) -> bool { matches!(self, AudioChunk::F32(_) | AudioChunk::F64(_)) } } #[derive(Debug, Clone)] pub enum AudioIntegerChunk { I16(Arc>), I24(Arc>), I32(Arc>), } impl AudioIntegerChunk { /// Retourne le nombre de frames du chunk pub fn len(&self) -> usize { match self { AudioIntegerChunk::I16(d) => d.len(), AudioIntegerChunk::I24(d) => d.len(), AudioIntegerChunk::I32(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 { AudioIntegerChunk::I16(d) => d.get_sample_rate(), AudioIntegerChunk::I24(d) => d.get_sample_rate(), AudioIntegerChunk::I32(d) => d.get_sample_rate(), } } /// Gain courant en décibels pub fn gain_db(&self) -> f64 { match self { AudioIntegerChunk::I16(d) => d.get_gain_db(), AudioIntegerChunk::I24(d) => d.get_gain_db(), AudioIntegerChunk::I32(d) => d.get_gain_db(), } } /// Gain sous forme linéaire pub fn gain_linear(&self) -> f64 { gain_linear_from_db(self.gain_db()) } /// Définit le gain en dB pub fn set_gain_db(&self, gain_db: f64) -> Self { match self { AudioIntegerChunk::I16(d) => AudioIntegerChunk::I16(d.set_gain_db(gain_db)), AudioIntegerChunk::I24(d) => AudioIntegerChunk::I24(d.set_gain_db(gain_db)), AudioIntegerChunk::I32(d) => AudioIntegerChunk::I32(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(gain_db_from_linear(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 { AudioIntegerChunk::I16(d) => { let gain_db = d.get_gain_db(); if gain_db.abs() < f64::EPSILON { return AudioIntegerChunk::I16(d); } let gain_linear = gain_linear_from_db(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; } AudioIntegerChunk::I16(AudioChunkData::new(stereo, d.get_sample_rate(), 0.0)) } AudioIntegerChunk::I24(d) => { let gain_db = d.get_gain_db(); if gain_db.abs() < f64::EPSILON { return AudioIntegerChunk::I24(d); } let gain_linear = gain_linear_from_db(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); } AudioIntegerChunk::I24(AudioChunkData::new(stereo, d.get_sample_rate(), 0.0)) } AudioIntegerChunk::I32(d) => AudioIntegerChunk::I32(d.apply_gain()), } } /// Retourne le nom du type de sample pub fn type_name(&self) -> &'static str { match self { AudioIntegerChunk::I16(_) => "i16", AudioIntegerChunk::I24(_) => "I24", AudioIntegerChunk::I32(_) => "i32", } } /// Vérifie si le chunk est de type I16 pub fn is_i16(&self) -> bool { matches!(self, AudioIntegerChunk::I16(_)) } /// Vérifie si le chunk est de type I24 pub fn is_i24(&self) -> bool { matches!(self, AudioIntegerChunk::I24(_)) } /// Vérifie si le chunk est de type I32 pub fn is_i32(&self) -> bool { matches!(self, AudioIntegerChunk::I32(_)) } /// Retourne la profondeur de bit du chunk pub fn bit_depth(&self) -> u8 { match self { AudioIntegerChunk::I16(_) => 16, AudioIntegerChunk::I24(_) => 24, AudioIntegerChunk::I32(_) => 32, } } /// Convertit vers AudioChunk pub fn as_audio_chunk(&self) -> AudioChunk { match self { AudioIntegerChunk::I16(d) => AudioChunk::I16(d.clone()), AudioIntegerChunk::I24(d) => AudioChunk::I24(d.clone()), AudioIntegerChunk::I32(d) => AudioChunk::I32(d.clone()), } } /// Convertit vers I16 (avec conversion si nécessaire) pub fn to_i16(&self) -> AudioIntegerChunk { match self { AudioIntegerChunk::I16(_) => self.clone(), AudioIntegerChunk::I24(d) => { // I24 -> I32 -> I16 let i32_chunk = crate::conversions::convert_i24_to_i32(d); let converted = crate::conversions::convert_i32_to_i16(&i32_chunk); AudioIntegerChunk::I16(converted) } AudioIntegerChunk::I32(d) => { let converted = crate::conversions::convert_i32_to_i16(d); AudioIntegerChunk::I16(converted) } } } /// Convertit vers I24 (avec conversion si nécessaire) pub fn to_i24(&self) -> AudioIntegerChunk { match self { AudioIntegerChunk::I16(d) => { // I16 -> I32 -> I24 let i32_chunk = crate::conversions::convert_i16_to_i32(d); let converted = crate::conversions::convert_i32_to_i24(&i32_chunk); AudioIntegerChunk::I24(converted) } AudioIntegerChunk::I24(_) => self.clone(), AudioIntegerChunk::I32(d) => { let converted = crate::conversions::convert_i32_to_i24(d); AudioIntegerChunk::I24(converted) } } } /// Convertit vers I32 (avec conversion si nécessaire) pub fn to_i32(&self) -> AudioIntegerChunk { match self { AudioIntegerChunk::I16(d) => { let converted = crate::conversions::convert_i16_to_i32(d); AudioIntegerChunk::I32(converted) } AudioIntegerChunk::I24(d) => { let converted = crate::conversions::convert_i24_to_i32(d); AudioIntegerChunk::I32(converted) } AudioIntegerChunk::I32(_) => self.clone(), } } /// Retourne un itérateur sur les frames pub fn frames(&self) -> Box + '_> { match self { AudioIntegerChunk::I16(d) => { Box::new(d.get_frames().iter().map(|f| [f[0] as i32, f[1] as i32])) } AudioIntegerChunk::I24(d) => Box::new( d.get_frames() .iter() .map(|f| [f[0].as_i32(), f[1].as_i32()]), ), AudioIntegerChunk::I32(d) => Box::new(d.get_frames().iter().map(|f| [f[0], f[1]])), } } } impl From for AudioIntegerChunk { /// Convertit depuis AudioChunk (panic si le chunk est float) fn from(chunk: AudioChunk) -> Self { match chunk { AudioChunk::I16(d) => AudioIntegerChunk::I16(d), AudioChunk::I24(d) => AudioIntegerChunk::I24(d), AudioChunk::I32(d) => AudioIntegerChunk::I32(d), AudioChunk::F32(_) | AudioChunk::F64(_) => { panic!("Cannot convert float AudioChunk to AudioIntegerChunk") } } } } #[derive(Debug, Clone)] pub enum AudioFloatChunk { F32(Arc>), F64(Arc>), } impl AudioFloatChunk { /// Retourne le nombre de frames du chunk pub fn len(&self) -> usize { match self { AudioFloatChunk::F32(d) => d.len(), AudioFloatChunk::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 { AudioFloatChunk::F32(d) => d.get_sample_rate(), AudioFloatChunk::F64(d) => d.get_sample_rate(), } } /// Gain courant en décibels pub fn gain_db(&self) -> f64 { match self { AudioFloatChunk::F32(d) => d.get_gain_db(), AudioFloatChunk::F64(d) => d.get_gain_db(), } } /// Gain sous forme linéaire pub fn gain_linear(&self) -> f64 { gain_linear_from_db(self.gain_db()) } /// Définit le gain en dB pub fn set_gain_db(&self, gain_db: f64) -> Self { match self { AudioFloatChunk::F32(d) => AudioFloatChunk::F32(d.set_gain_db(gain_db)), AudioFloatChunk::F64(d) => AudioFloatChunk::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(gain_db_from_linear(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 { AudioFloatChunk::F32(d) => AudioFloatChunk::F32(d.apply_gain()), AudioFloatChunk::F64(d) => AudioFloatChunk::F64(d.apply_gain()), } } /// Retourne le nom du type de sample pub fn type_name(&self) -> &'static str { match self { AudioFloatChunk::F32(_) => "f32", AudioFloatChunk::F64(_) => "f64", } } /// Vérifie si le chunk est de type F32 pub fn is_f32(&self) -> bool { matches!(self, AudioFloatChunk::F32(_)) } /// Vérifie si le chunk est de type F64 pub fn is_f64(&self) -> bool { matches!(self, AudioFloatChunk::F64(_)) } /// Retourne la profondeur de bit du chunk (32 ou 64) pub fn bit_depth(&self) -> u8 { match self { AudioFloatChunk::F32(_) => 32, AudioFloatChunk::F64(_) => 64, } } /// Convertit vers AudioChunk pub fn as_audio_chunk(&self) -> AudioChunk { match self { AudioFloatChunk::F32(d) => AudioChunk::F32(d.clone()), AudioFloatChunk::F64(d) => AudioChunk::F64(d.clone()), } } /// Convertit vers F32 (avec conversion si nécessaire) pub fn to_f32(&self) -> AudioFloatChunk { match self { AudioFloatChunk::F32(_) => self.clone(), AudioFloatChunk::F64(d) => { let converted = crate::conversions::convert_f64_to_f32(d); AudioFloatChunk::F32(converted) } } } /// Convertit vers F64 (avec conversion si nécessaire) pub fn to_f64(&self) -> AudioFloatChunk { match self { AudioFloatChunk::F32(d) => { let converted = crate::conversions::convert_f32_to_f64(d); AudioFloatChunk::F64(converted) } AudioFloatChunk::F64(_) => self.clone(), } } /// Retourne un itérateur sur les frames pub fn frames(&self) -> Box + '_> { match self { AudioFloatChunk::F32(d) => { Box::new(d.get_frames().iter().map(|f| [f[0] as f64, f[1] as f64])) } AudioFloatChunk::F64(d) => Box::new(d.get_frames().iter().map(|f| [f[0], f[1]])), } } } impl From for AudioFloatChunk { /// Convertit depuis AudioChunk (panic si le chunk est entier) fn from(chunk: AudioChunk) -> Self { match chunk { AudioChunk::F32(d) => AudioFloatChunk::F32(d), AudioChunk::F64(d) => AudioFloatChunk::F64(d), AudioChunk::I16(_) | AudioChunk::I24(_) | AudioChunk::I32(_) => { panic!("Cannot convert integer AudioChunk to AudioFloatChunk") } } } } // ============================================================================ // Fonctions utilitaires de conversion gain // ============================================================================ const MIN_GAIN_DB: f64 = -120.0; /// Convertit un gain linéaire (>0) en décibels #[inline] pub fn gain_db_from_linear(gain_linear: f64) -> f64 { if gain_linear <= 0.0 { MIN_GAIN_DB } else { (20.0 * gain_linear.log10()).max(MIN_GAIN_DB) } } /// Convertit un gain en décibels vers un gain linéaire #[inline] pub fn gain_linear_from_db(gain_db: f64) -> f64 { 10f64.powf(gain_db / 20.0) } // ============================================================================ // 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.get_sample_rate(), 48000); assert!(!chunk.is_empty()); assert_eq!(chunk.get_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.get_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 = gain_db_from_linear(linear); assert!((db - 6.0206).abs() < 0.01); // 2x ≈ +6dB let back = gain_linear_from_db(db); assert!((back - linear).abs() < 0.001); } }