Restructuration de pmoaudio avec ajout des messages de synchro

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

View File

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