Retour sur pmoaudio
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! 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
|
||||
//! - Entiers : i16, I24 (24-bit), i32
|
||||
//! - Flottants : f32, f64
|
||||
//!
|
||||
//! L'utilisation de génériques permet de factoriser le code tout en gardant
|
||||
@@ -18,7 +18,7 @@ use crate::{dsp, BitDepth, Sample, I24};
|
||||
/// 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 (i8, i16, I24, i32, f32, f64). Les données sont partagées via `Arc`
|
||||
/// 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
|
||||
@@ -118,7 +118,7 @@ impl<T: Sample> AudioChunkData<T> {
|
||||
/// Gain sous forme linéaire
|
||||
#[inline]
|
||||
pub fn gain_linear(&self) -> f64 {
|
||||
db_to_linear(self.gain_db)
|
||||
gain_linear_from_db(self.gain_db)
|
||||
}
|
||||
|
||||
/// Retourne une vue immuable sur les frames `[L, R]`
|
||||
@@ -146,7 +146,7 @@ impl<T: Sample> AudioChunkData<T> {
|
||||
|
||||
/// 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))
|
||||
self.set_gain_db(gain_db_from_linear(gain_linear))
|
||||
}
|
||||
|
||||
/// Modifie le gain de ce chunk (ajoute un delta en dB)
|
||||
@@ -156,11 +156,11 @@ impl<T: Sample> AudioChunkData<T> {
|
||||
|
||||
/// 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))
|
||||
self.with_modified_gain_db(gain_db_from_linear(gain_linear))
|
||||
}
|
||||
}
|
||||
|
||||
// Méthodes spécifiques pour les types entiers (i8, i16, I24, i32)
|
||||
// Méthodes spécifiques pour les types entiers (i16, I24, i32)
|
||||
impl AudioChunkData<i32> {
|
||||
/// Applique le gain et retourne un nouveau chunk avec les données modifiées
|
||||
///
|
||||
@@ -172,14 +172,18 @@ impl AudioChunkData<i32> {
|
||||
}
|
||||
|
||||
let mut stereo = self.clone_frames();
|
||||
dsp::apply_gain_stereo(&mut stereo, self.gain_db);
|
||||
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<i32>, right: Vec<i32>, sample_rate: u32) -> Arc<Self> {
|
||||
assert_eq!(left.len(), right.len(), "channels must have identical length");
|
||||
assert_eq!(
|
||||
left.len(),
|
||||
right.len(),
|
||||
"channels must have identical length"
|
||||
);
|
||||
let stereo = left
|
||||
.into_iter()
|
||||
.zip(right.into_iter())
|
||||
@@ -213,7 +217,7 @@ impl AudioChunkData<f32> {
|
||||
return self; // Pas de gain à appliquer
|
||||
}
|
||||
|
||||
let gain_linear = db_to_linear(self.gain_db) as f32;
|
||||
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;
|
||||
@@ -225,7 +229,11 @@ impl AudioChunkData<f32> {
|
||||
|
||||
/// 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");
|
||||
assert_eq!(
|
||||
left.len(),
|
||||
right.len(),
|
||||
"channels must have identical length"
|
||||
);
|
||||
let stereo = left
|
||||
.into_iter()
|
||||
.zip(right.into_iter())
|
||||
@@ -243,7 +251,7 @@ impl AudioChunkData<f64> {
|
||||
return self; // Pas de gain à appliquer
|
||||
}
|
||||
|
||||
let gain_linear = db_to_linear(self.gain_db);
|
||||
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;
|
||||
@@ -255,7 +263,11 @@ impl AudioChunkData<f64> {
|
||||
|
||||
/// 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");
|
||||
assert_eq!(
|
||||
left.len(),
|
||||
right.len(),
|
||||
"channels must have identical length"
|
||||
);
|
||||
let stereo = left
|
||||
.into_iter()
|
||||
.zip(right.into_iter())
|
||||
@@ -276,7 +288,6 @@ impl AudioChunkData<f64> {
|
||||
///
|
||||
/// # 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
|
||||
@@ -298,7 +309,6 @@ impl AudioChunkData<f64> {
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AudioChunk {
|
||||
I8(Arc<AudioChunkData<i8>>),
|
||||
I16(Arc<AudioChunkData<i16>>),
|
||||
I24(Arc<AudioChunkData<I24>>),
|
||||
I32(Arc<AudioChunkData<i32>>),
|
||||
@@ -310,7 +320,6 @@ 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(),
|
||||
@@ -327,7 +336,6 @@ impl AudioChunk {
|
||||
/// 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(),
|
||||
@@ -339,7 +347,6 @@ impl AudioChunk {
|
||||
/// 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(),
|
||||
@@ -350,13 +357,12 @@ impl AudioChunk {
|
||||
|
||||
/// Gain sous forme linéaire
|
||||
pub fn gain_linear(&self) -> f64 {
|
||||
db_to_linear(self.gain_db())
|
||||
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::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)),
|
||||
@@ -367,7 +373,7 @@ impl AudioChunk {
|
||||
|
||||
/// 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))
|
||||
self.set_gain_db(gain_db_from_linear(gain_linear))
|
||||
}
|
||||
|
||||
/// Modifie le gain (ajoute un delta en dB)
|
||||
@@ -380,31 +386,20 @@ impl AudioChunk {
|
||||
/// 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 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;
|
||||
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))
|
||||
}
|
||||
@@ -413,11 +408,15 @@ impl AudioChunk {
|
||||
if gain_db.abs() < f64::EPSILON {
|
||||
return AudioChunk::I24(d);
|
||||
}
|
||||
let gain_linear = db_to_linear(gain_db) as f32;
|
||||
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;
|
||||
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);
|
||||
}
|
||||
@@ -432,7 +431,6 @@ impl AudioChunk {
|
||||
/// 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",
|
||||
@@ -440,6 +438,415 @@ impl AudioChunk {
|
||||
AudioChunk::F64(_) => "f64",
|
||||
}
|
||||
}
|
||||
|
||||
/// Tente de convertir vers AudioIntegerChunk (retourne None si float)
|
||||
pub fn try_as_integer(&self) -> Option<AudioIntegerChunk> {
|
||||
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<AudioFloatChunk> {
|
||||
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<AudioChunkData<i16>>),
|
||||
I24(Arc<AudioChunkData<I24>>),
|
||||
I32(Arc<AudioChunkData<i32>>),
|
||||
}
|
||||
|
||||
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.sample_rate(),
|
||||
AudioIntegerChunk::I24(d) => d.sample_rate(),
|
||||
AudioIntegerChunk::I32(d) => d.sample_rate(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gain courant en décibels
|
||||
pub fn gain_db(&self) -> f64 {
|
||||
match self {
|
||||
AudioIntegerChunk::I16(d) => d.gain_db(),
|
||||
AudioIntegerChunk::I24(d) => d.gain_db(),
|
||||
AudioIntegerChunk::I32(d) => d.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.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.sample_rate(), 0.0))
|
||||
}
|
||||
AudioIntegerChunk::I24(d) => {
|
||||
let gain_db = d.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.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<dyn Iterator<Item = [i32; 2]> + '_> {
|
||||
match self {
|
||||
AudioIntegerChunk::I16(d) => {
|
||||
Box::new(d.frames().iter().map(|f| [f[0] as i32, f[1] as i32]))
|
||||
}
|
||||
AudioIntegerChunk::I24(d) => {
|
||||
Box::new(d.frames().iter().map(|f| [f[0].as_i32(), f[1].as_i32()]))
|
||||
}
|
||||
AudioIntegerChunk::I32(d) => Box::new(d.frames().iter().map(|f| [f[0], f[1]])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AudioChunk> 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<AudioChunkData<f32>>),
|
||||
F64(Arc<AudioChunkData<f64>>),
|
||||
}
|
||||
|
||||
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.sample_rate(),
|
||||
AudioFloatChunk::F64(d) => d.sample_rate(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gain courant en décibels
|
||||
pub fn gain_db(&self) -> f64 {
|
||||
match self {
|
||||
AudioFloatChunk::F32(d) => d.gain_db(),
|
||||
AudioFloatChunk::F64(d) => d.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<dyn Iterator<Item = [f64; 2]> + '_> {
|
||||
match self {
|
||||
AudioFloatChunk::F32(d) => {
|
||||
Box::new(d.frames().iter().map(|f| [f[0] as f64, f[1] as f64]))
|
||||
}
|
||||
AudioFloatChunk::F64(d) => Box::new(d.frames().iter().map(|f| [f[0], f[1]])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AudioChunk> 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -450,7 +857,7 @@ const MIN_GAIN_DB: f64 = -120.0;
|
||||
|
||||
/// Convertit un gain linéaire (>0) en décibels
|
||||
#[inline]
|
||||
pub fn linear_to_db(gain_linear: f64) -> f64 {
|
||||
pub fn gain_db_from_linear(gain_linear: f64) -> f64 {
|
||||
if gain_linear <= 0.0 {
|
||||
MIN_GAIN_DB
|
||||
} else {
|
||||
@@ -460,18 +867,8 @@ pub fn linear_to_db(gain_linear: 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)
|
||||
10f64.powf(gain_db / 20.0)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -515,10 +912,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_gain_conversion() {
|
||||
let linear = 2.0;
|
||||
let db = linear_to_db(linear);
|
||||
let db = gain_db_from_linear(linear);
|
||||
assert!((db - 6.0206).abs() < 0.01); // 2x ≈ +6dB
|
||||
|
||||
let back = db_to_linear(db);
|
||||
let back = gain_linear_from_db(db);
|
||||
assert!((back - linear).abs() < 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use pmometadata::TrackMetadata;
|
||||
|
||||
use crate::{AudioChunk, AudioChunkData, BitDepth, SyncMarker, linear_to_db};
|
||||
use crate::{gain_db_from_linear, AudioChunk, AudioChunkData, BitDepth, SyncMarker};
|
||||
|
||||
pub enum _AudioSegment {
|
||||
Chunk(Arc<AudioChunk>),
|
||||
@@ -60,7 +60,7 @@ impl AudioSegment {
|
||||
_bit_depth: BitDepth, // Conservé pour compatibilité API
|
||||
gain_linear: f64,
|
||||
) -> Arc<Self> {
|
||||
let chunk_data = AudioChunkData::new(stereo, sample_rate, linear_to_db(gain_linear));
|
||||
let chunk_data = AudioChunkData::new(stereo, sample_rate, gain_db_from_linear(gain_linear));
|
||||
let chunk = AudioChunk::I32(chunk_data);
|
||||
Arc::new(Self {
|
||||
order,
|
||||
@@ -98,7 +98,11 @@ impl AudioSegment {
|
||||
sample_rate: u32,
|
||||
bit_depth: BitDepth,
|
||||
) -> Arc<Self> {
|
||||
assert_eq!(left.len(), right.len(), "channels must have identical length");
|
||||
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();
|
||||
@@ -152,9 +156,10 @@ impl AudioSegment {
|
||||
}
|
||||
|
||||
pub fn new_track_boundary(
|
||||
order: u64,
|
||||
timestamp_sec: f64,
|
||||
metadata: Arc<dyn TrackMetadata>) -> Arc<Self> {
|
||||
order: u64,
|
||||
timestamp_sec: f64,
|
||||
metadata: Arc<dyn TrackMetadata>,
|
||||
) -> Arc<Self> {
|
||||
let marker = Arc::new(SyncMarker::TrackBoundary {
|
||||
metadata: Arc::clone(&metadata),
|
||||
});
|
||||
@@ -166,13 +171,13 @@ impl AudioSegment {
|
||||
}
|
||||
|
||||
pub fn new_stream_metadata(
|
||||
order: u64,
|
||||
order: u64,
|
||||
timestamp_sec: f64,
|
||||
key: String,
|
||||
value: String
|
||||
) -> Arc<Self> {
|
||||
value: String,
|
||||
) -> Arc<Self> {
|
||||
let marker = Arc::new(SyncMarker::StreamMetadata { key, value });
|
||||
|
||||
|
||||
Arc::new(Self {
|
||||
order,
|
||||
timestamp_sec,
|
||||
@@ -183,51 +188,41 @@ impl AudioSegment {
|
||||
pub fn new_top_zero_sync() -> Arc<Self> {
|
||||
let marker = Arc::new(SyncMarker::TopZeroSync);
|
||||
|
||||
Arc::new(Self{
|
||||
Arc::new(Self {
|
||||
order: 0,
|
||||
timestamp_sec: 0.0,
|
||||
segment: _AudioSegment::Sync(marker)
|
||||
segment: _AudioSegment::Sync(marker),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_hearbeat(
|
||||
order: u64,
|
||||
timestamp_sec: f64,
|
||||
) -> Arc<Self> {
|
||||
pub fn new_hearbeat(order: u64, timestamp_sec: f64) -> Arc<Self> {
|
||||
let marker = Arc::new(SyncMarker::Heartbeat);
|
||||
|
||||
Arc::new(Self{
|
||||
order: order,
|
||||
timestamp_sec: timestamp_sec,
|
||||
segment: _AudioSegment::Sync(marker)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_end_of_stream(
|
||||
order: u64,
|
||||
timestamp_sec: f64,
|
||||
) -> Arc<Self> {
|
||||
let marker = Arc::new(SyncMarker::EndOfStream);
|
||||
|
||||
Arc::new(Self{
|
||||
Arc::new(Self {
|
||||
order: order,
|
||||
timestamp_sec: timestamp_sec,
|
||||
segment: _AudioSegment::Sync(marker),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_error(
|
||||
order: u64,
|
||||
timestamp_sec: f64,
|
||||
error: String,
|
||||
) -> Arc<Self> {
|
||||
pub fn new_end_of_stream(order: u64, timestamp_sec: f64) -> Arc<Self> {
|
||||
let marker = Arc::new(SyncMarker::EndOfStream);
|
||||
|
||||
Arc::new(Self {
|
||||
order: order,
|
||||
timestamp_sec: timestamp_sec,
|
||||
segment: _AudioSegment::Sync(marker),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_error(order: u64, timestamp_sec: f64, error: String) -> Arc<Self> {
|
||||
let marker = Arc::new(SyncMarker::Error(error));
|
||||
|
||||
Arc::new(Self{
|
||||
order: order,
|
||||
timestamp_sec: timestamp_sec,
|
||||
segment: _AudioSegment::Sync(marker),
|
||||
})
|
||||
Arc::new(Self {
|
||||
order: order,
|
||||
timestamp_sec: timestamp_sec,
|
||||
segment: _AudioSegment::Sync(marker),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_audio_chunk(&self) -> bool {
|
||||
@@ -460,13 +455,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_audio_segment_gain_manipulation() {
|
||||
let segment = AudioSegment::new_chunk(
|
||||
0,
|
||||
0.0,
|
||||
vec![[100i32, 200i32]],
|
||||
44100,
|
||||
BitDepth::B32,
|
||||
);
|
||||
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();
|
||||
@@ -486,13 +475,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_audio_segment_conversions() {
|
||||
let segment = AudioSegment::new_chunk(
|
||||
0,
|
||||
0.0,
|
||||
vec![[1000000i32, 2000000i32]],
|
||||
44100,
|
||||
BitDepth::B32,
|
||||
);
|
||||
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();
|
||||
|
||||
@@ -14,22 +14,6 @@ use crate::{dsp, AudioChunk, AudioChunkData, BitDepth, I24};
|
||||
// Ces fonctions utilisent la fonction DSP optimisée SIMD `bitdepth_change_stereo`
|
||||
// pour les conversions i32 ↔ i32 avec différents bit depths.
|
||||
|
||||
/// Convertit i32 vers i8 (downsampling via bit depth change)
|
||||
pub fn convert_i32_to_i8(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<i8>> {
|
||||
let mut stereo = chunk.clone_frames();
|
||||
|
||||
// Utiliser la fonction DSP optimisée pour passer de B32 → B8
|
||||
dsp::bitdepth_change_stereo(&mut stereo, BitDepth::B32, BitDepth::B8);
|
||||
|
||||
// Convertir i32 → i8 (les valeurs sont maintenant dans la plage i8)
|
||||
let stereo_i8: Vec<[i8; 2]> = stereo
|
||||
.into_iter()
|
||||
.map(|[l, r]| [l as i8, r as i8])
|
||||
.collect();
|
||||
|
||||
AudioChunkData::new(stereo_i8, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit i32 vers i16 (downsampling via bit depth change)
|
||||
pub fn convert_i32_to_i16(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<i16>> {
|
||||
let mut stereo = chunk.clone_frames();
|
||||
@@ -62,21 +46,6 @@ pub fn convert_i32_to_i24(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<I24
|
||||
AudioChunkData::new(stereo_i24, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit i8 vers i32 (upsampling via bit depth change)
|
||||
pub fn convert_i8_to_i32(chunk: &AudioChunkData<i8>) -> Arc<AudioChunkData<i32>> {
|
||||
// Convertir i8 → i32 d'abord
|
||||
let mut stereo: Vec<[i32; 2]> = chunk
|
||||
.frames()
|
||||
.iter()
|
||||
.map(|[l, r]| [*l as i32, *r as i32])
|
||||
.collect();
|
||||
|
||||
// Utiliser la fonction DSP optimisée pour passer de B8 → B32
|
||||
dsp::bitdepth_change_stereo(&mut stereo, BitDepth::B8, BitDepth::B32);
|
||||
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit i16 vers i32 (upsampling via bit depth change)
|
||||
pub fn convert_i16_to_i32(chunk: &AudioChunkData<i16>) -> Arc<AudioChunkData<i32>> {
|
||||
// Convertir i16 → i32 d'abord
|
||||
@@ -142,21 +111,24 @@ pub fn convert_i32_to_f64(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<f64
|
||||
convert_f32_to_f64(&f32_chunk)
|
||||
}
|
||||
|
||||
/// Convertit I24 vers f32
|
||||
/// Convertit I24 vers f32 via les fonctions DSP optimisées SIMD
|
||||
pub fn convert_i24_to_f32(chunk: &AudioChunkData<I24>) -> Arc<AudioChunkData<f32>> {
|
||||
let frames = chunk.frames();
|
||||
let max_value = 8_388_608.0f32; // 2^23
|
||||
let len = frames.len();
|
||||
|
||||
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();
|
||||
// Séparer les canaux I24 en i32
|
||||
let mut left = Vec::with_capacity(len);
|
||||
let mut right = Vec::with_capacity(len);
|
||||
for [l, r] in frames {
|
||||
left.push(l.as_i32());
|
||||
right.push(r.as_i32());
|
||||
}
|
||||
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
// Utiliser la fonction SIMD optimisée du module DSP pour I24
|
||||
let mut out_pairs = vec![[0.0f32; 2]; len];
|
||||
dsp::i24_as_i32_stereo_to_pairs_f32(&left, &right, &mut out_pairs);
|
||||
|
||||
AudioChunkData::new(out_pairs, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit I24 vers f64
|
||||
@@ -176,21 +148,24 @@ pub fn convert_i24_to_f64(chunk: &AudioChunkData<I24>) -> Arc<AudioChunkData<f64
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit i16 vers f32
|
||||
/// Convertit i16 vers f32 via les fonctions DSP optimisées SIMD
|
||||
pub fn convert_i16_to_f32(chunk: &AudioChunkData<i16>) -> Arc<AudioChunkData<f32>> {
|
||||
let frames = chunk.frames();
|
||||
let max_value = 32_768.0f32; // 2^15
|
||||
let len = frames.len();
|
||||
|
||||
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();
|
||||
// Séparer les canaux
|
||||
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);
|
||||
}
|
||||
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
// Utiliser la fonction SIMD optimisée du module DSP
|
||||
let mut out_pairs = vec![[0.0f32; 2]; len];
|
||||
dsp::i16_stereo_to_pairs_f32(&left, &right, &mut out_pairs);
|
||||
|
||||
AudioChunkData::new(out_pairs, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit i16 vers f64
|
||||
@@ -210,40 +185,6 @@ pub fn convert_i16_to_f64(chunk: &AudioChunkData<i16>) -> Arc<AudioChunkData<f64
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit i8 vers f32
|
||||
pub fn convert_i8_to_f32(chunk: &AudioChunkData<i8>) -> Arc<AudioChunkData<f32>> {
|
||||
let frames = chunk.frames();
|
||||
let max_value = 128.0f32; // 2^7
|
||||
|
||||
let stereo: Vec<[f32; 2]> = frames
|
||||
.iter()
|
||||
.map(|[l, r]| {
|
||||
let lf = *l as f32 / max_value;
|
||||
let rf = *r as f32 / max_value;
|
||||
[lf, rf]
|
||||
})
|
||||
.collect();
|
||||
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit i8 vers f64
|
||||
pub fn convert_i8_to_f64(chunk: &AudioChunkData<i8>) -> Arc<AudioChunkData<f64>> {
|
||||
let frames = chunk.frames();
|
||||
let max_value = 128.0f64; // 2^7
|
||||
|
||||
let stereo: Vec<[f64; 2]> = frames
|
||||
.iter()
|
||||
.map(|[l, r]| {
|
||||
let lf = *l as f64 / max_value;
|
||||
let rf = *r as f64 / max_value;
|
||||
[lf, rf]
|
||||
})
|
||||
.collect();
|
||||
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Conversions float → int (quantization)
|
||||
// ============================================================================
|
||||
@@ -279,19 +220,21 @@ pub fn convert_f64_to_i32(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i32
|
||||
convert_f32_to_i32(&f32_chunk)
|
||||
}
|
||||
|
||||
/// Convertit f32 vers I24
|
||||
/// Convertit f32 vers I24 via les fonctions DSP optimisées SIMD
|
||||
pub fn convert_f32_to_i24(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<I24>> {
|
||||
let frames = chunk.frames();
|
||||
let max_value = 8_388_607.0f32; // 2^23 - 1
|
||||
let min_value = -8_388_608.0f32; // -2^23
|
||||
let len = frames.len();
|
||||
|
||||
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)]
|
||||
})
|
||||
// Utiliser la fonction SIMD optimisée du module DSP
|
||||
let mut left = vec![0i32; len];
|
||||
let mut right = vec![0i32; len];
|
||||
dsp::pairs_f32_to_i24_as_i32_stereo(frames, &mut left, &mut right);
|
||||
|
||||
// Recombiner en frames I24
|
||||
let stereo: Vec<[I24; 2]> = left
|
||||
.into_iter()
|
||||
.zip(right.into_iter())
|
||||
.map(|(l, r)| [I24::new_clamped(l), I24::new_clamped(r)])
|
||||
.collect();
|
||||
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
@@ -315,19 +258,21 @@ pub fn convert_f64_to_i24(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<I24
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit f32 vers i16
|
||||
/// Convertit f32 vers i16 via les fonctions DSP optimisées SIMD
|
||||
pub fn convert_f32_to_i16(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<i16>> {
|
||||
let frames = chunk.frames();
|
||||
let max_value = 32_767.0f32; // 2^15 - 1
|
||||
let min_value = -32_768.0f32; // -2^15
|
||||
let len = frames.len();
|
||||
|
||||
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]
|
||||
})
|
||||
// Utiliser la fonction SIMD optimisée du module DSP
|
||||
let mut left = vec![0i16; len];
|
||||
let mut right = vec![0i16; len];
|
||||
dsp::pairs_f32_to_i16_stereo(frames, &mut left, &mut right);
|
||||
|
||||
// Recombiner en frames
|
||||
let stereo: Vec<[i16; 2]> = left
|
||||
.into_iter()
|
||||
.zip(right.into_iter())
|
||||
.map(|(l, r)| [l, r])
|
||||
.collect();
|
||||
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
@@ -351,42 +296,6 @@ pub fn convert_f64_to_i16(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i16
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit f32 vers i8
|
||||
pub fn convert_f32_to_i8(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<i8>> {
|
||||
let frames = chunk.frames();
|
||||
let max_value = 127.0f32; // 2^7 - 1
|
||||
let min_value = -128.0f32; // -2^7
|
||||
|
||||
let stereo: Vec<[i8; 2]> = frames
|
||||
.iter()
|
||||
.map(|[l, r]| {
|
||||
let l8 = (l * max_value).clamp(min_value, max_value).round() as i8;
|
||||
let r8 = (r * max_value).clamp(min_value, max_value).round() as i8;
|
||||
[l8, r8]
|
||||
})
|
||||
.collect();
|
||||
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
/// Convertit f64 vers i8
|
||||
pub fn convert_f64_to_i8(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i8>> {
|
||||
let frames = chunk.frames();
|
||||
let max_value = 127.0f64; // 2^7 - 1
|
||||
let min_value = -128.0f64; // -2^7
|
||||
|
||||
let stereo: Vec<[i8; 2]> = frames
|
||||
.iter()
|
||||
.map(|[l, r]| {
|
||||
let l8 = (l * max_value).clamp(min_value, max_value).round() as i8;
|
||||
let r8 = (r * max_value).clamp(min_value, max_value).round() as i8;
|
||||
[l8, r8]
|
||||
})
|
||||
.collect();
|
||||
|
||||
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Conversions F32 ↔ F64
|
||||
// ============================================================================
|
||||
@@ -394,10 +303,7 @@ pub fn convert_f64_to_i8(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i8>>
|
||||
/// Convertit f32 vers f64 (upcast simple)
|
||||
pub fn convert_f32_to_f64(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<f64>> {
|
||||
let frames = chunk.frames();
|
||||
let stereo: Vec<[f64; 2]> = frames
|
||||
.iter()
|
||||
.map(|[l, r]| [*l as f64, *r as f64])
|
||||
.collect();
|
||||
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())
|
||||
}
|
||||
@@ -405,10 +311,7 @@ pub fn convert_f32_to_f64(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<f64
|
||||
/// Convertit f64 vers f32 (downcast simple)
|
||||
pub fn convert_f64_to_f32(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<f32>> {
|
||||
let frames = chunk.frames();
|
||||
let stereo: Vec<[f32; 2]> = frames
|
||||
.iter()
|
||||
.map(|[l, r]| [*l as f32, *r as f32])
|
||||
.collect();
|
||||
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())
|
||||
}
|
||||
@@ -420,10 +323,9 @@ pub fn convert_f64_to_f32(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<f32
|
||||
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)
|
||||
/// Chaque type utilise sa plage native (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)),
|
||||
@@ -434,10 +336,9 @@ impl AudioChunk {
|
||||
|
||||
/// Convertit ce chunk vers f64
|
||||
///
|
||||
/// Chaque type utilise sa plage native (I8=±2^7, I16=±2^15, I24=±2^23, I32=±2^31)
|
||||
/// Chaque type utilise sa plage native (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)),
|
||||
@@ -451,7 +352,6 @@ impl AudioChunk {
|
||||
/// 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()),
|
||||
@@ -463,11 +363,6 @@ impl AudioChunk {
|
||||
/// 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);
|
||||
@@ -483,11 +378,6 @@ impl AudioChunk {
|
||||
/// 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
|
||||
@@ -499,26 +389,6 @@ impl AudioChunk {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -527,12 +397,6 @@ impl AudioChunk {
|
||||
|
||||
// ---------- From<Arc<AudioChunkData<T>>> pour AudioChunk ----------
|
||||
|
||||
impl From<Arc<AudioChunkData<i8>>> for AudioChunk {
|
||||
fn from(data: Arc<AudioChunkData<i8>>) -> Self {
|
||||
AudioChunk::I8(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<AudioChunkData<i16>>> for AudioChunk {
|
||||
fn from(data: Arc<AudioChunkData<i16>>) -> Self {
|
||||
AudioChunk::I16(data)
|
||||
@@ -565,25 +429,6 @@ impl From<Arc<AudioChunkData<f64>>> for AudioChunk {
|
||||
|
||||
// ---------- From entre AudioChunkData types (sans BitDepth requis) ----------
|
||||
|
||||
// I8 conversions
|
||||
impl From<&AudioChunkData<i8>> for Arc<AudioChunkData<i32>> {
|
||||
fn from(chunk: &AudioChunkData<i8>) -> Self {
|
||||
convert_i8_to_i32(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AudioChunkData<i8>> for Arc<AudioChunkData<f32>> {
|
||||
fn from(chunk: &AudioChunkData<i8>) -> Self {
|
||||
convert_i8_to_f32(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AudioChunkData<i8>> for Arc<AudioChunkData<f64>> {
|
||||
fn from(chunk: &AudioChunkData<i8>) -> Self {
|
||||
convert_i8_to_f64(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
// I16 conversions
|
||||
impl From<&AudioChunkData<i16>> for Arc<AudioChunkData<i32>> {
|
||||
fn from(chunk: &AudioChunkData<i16>) -> Self {
|
||||
@@ -623,11 +468,6 @@ impl From<&AudioChunkData<I24>> for Arc<AudioChunkData<f64>> {
|
||||
}
|
||||
|
||||
// I32 conversions vers types int (downsampling)
|
||||
impl From<&AudioChunkData<i32>> for Arc<AudioChunkData<i8>> {
|
||||
fn from(chunk: &AudioChunkData<i32>) -> Self {
|
||||
convert_i32_to_i8(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AudioChunkData<i32>> for Arc<AudioChunkData<i16>> {
|
||||
fn from(chunk: &AudioChunkData<i32>) -> Self {
|
||||
@@ -661,12 +501,6 @@ impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<f64>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<i8>> {
|
||||
fn from(chunk: &AudioChunkData<f32>) -> Self {
|
||||
convert_f32_to_i8(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<i16>> {
|
||||
fn from(chunk: &AudioChunkData<f32>) -> Self {
|
||||
convert_f32_to_i16(chunk)
|
||||
@@ -692,12 +526,6 @@ impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<f32>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<i8>> {
|
||||
fn from(chunk: &AudioChunkData<f64>) -> Self {
|
||||
convert_f64_to_i8(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<i16>> {
|
||||
fn from(chunk: &AudioChunkData<f64>) -> Self {
|
||||
convert_f64_to_i16(chunk)
|
||||
@@ -785,10 +613,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_i24_conversions() {
|
||||
let stereo = vec![
|
||||
[I24::new(1_000_000).unwrap(), I24::new(-500_000).unwrap()];
|
||||
10
|
||||
];
|
||||
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
|
||||
@@ -855,10 +680,7 @@ mod tests {
|
||||
#[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 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
|
||||
@@ -892,10 +714,7 @@ mod tests {
|
||||
#[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 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
|
||||
|
||||
94
pmoaudio/src/dsp/gain_16bits.rs
Normal file
94
pmoaudio/src/dsp/gain_16bits.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
/// Applique un gain (en dB) sur des échantillons stéréo interleavés `[L,R]`
|
||||
/// codés sur 16 bits signés.
|
||||
pub fn apply_gain_stereo_i16(samples: &mut [[i16; 2]], gain_db: f64) {
|
||||
let gain = 10f64.powf(gain_db / 20.0);
|
||||
let g_q15 = (gain * (1u32 << 15) as f64).round() as i16;
|
||||
|
||||
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
|
||||
unsafe {
|
||||
apply_gain_stereo_i16_neon(samples, g_q15);
|
||||
return;
|
||||
}
|
||||
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
|
||||
unsafe {
|
||||
apply_gain_stereo_i16_avx2(samples, g_q15);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback scalaire
|
||||
#[cfg(not(any(
|
||||
all(target_arch = "aarch64", target_feature = "neon"),
|
||||
all(target_arch = "x86_64", target_feature = "avx2")
|
||||
)))]
|
||||
{
|
||||
apply_gain_stereo_i16_scalar(samples, g_q15);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(
|
||||
all(target_arch = "aarch64", target_feature = "neon"),
|
||||
all(target_arch = "x86_64", target_feature = "avx2")
|
||||
)))]
|
||||
#[inline(always)]
|
||||
fn apply_gain_stereo_i16_scalar(samples: &mut [[i16; 2]], g_q15: i16) {
|
||||
for frame in samples.iter_mut() {
|
||||
// L
|
||||
let prod_l = (frame[0] as i32 * g_q15 as i32 + (1 << 14)) >> 15;
|
||||
frame[0] = prod_l.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
|
||||
|
||||
// R
|
||||
let prod_r = (frame[1] as i32 * g_q15 as i32 + (1 << 14)) >> 15;
|
||||
frame[1] = prod_r.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
|
||||
#[inline(always)]
|
||||
unsafe fn apply_gain_stereo_i16_neon(samples: &mut [[i16; 2]], g_q15: i16) {
|
||||
use core::arch::aarch64::*;
|
||||
let gvec = vdupq_n_s16(g_q15);
|
||||
let mut i = 0;
|
||||
let n = samples.len() * 2;
|
||||
let ptr = samples.as_mut_ptr() as *mut i16;
|
||||
|
||||
while i + 8 <= n {
|
||||
let v = vld1q_s16(ptr.add(i));
|
||||
let res = vqdmulhq_s16(v, gvec); // Q15 multiply high
|
||||
vst1q_s16(ptr.add(i), res);
|
||||
i += 8;
|
||||
}
|
||||
|
||||
// reste scalaire
|
||||
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
|
||||
apply_gain_i16_scalar(slice, g_q15);
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
|
||||
#[inline(always)]
|
||||
unsafe fn apply_gain_stereo_i16_avx2(samples: &mut [[i16; 2]], g_q15: i16) {
|
||||
use core::arch::x86_64::*;
|
||||
let g = _mm256_set1_epi16(g_q15 as i16);
|
||||
let mut i = 0;
|
||||
let n = samples.len() * 2;
|
||||
let ptr = samples.as_mut_ptr() as *mut i16;
|
||||
|
||||
while i + 16 <= n {
|
||||
let x = _mm256_loadu_si256(ptr.add(i) as *const __m256i);
|
||||
let hi = _mm256_mulhi_epi16(x, g);
|
||||
_mm256_storeu_si256(ptr.add(i) as *mut __m256i, hi);
|
||||
i += 16;
|
||||
}
|
||||
|
||||
// reste scalaire
|
||||
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
|
||||
apply_gain_i16_scalar(slice, g_q15);
|
||||
}
|
||||
|
||||
/// version mono utilisée pour le reste scalaire
|
||||
#[inline(always)]
|
||||
fn apply_gain_i16_scalar(samples: &mut [i16], g_q15: i16) {
|
||||
for s in samples.iter_mut() {
|
||||
let prod = (*s as i32 * g_q15 as i32 + (1 << 14)) >> 15;
|
||||
*s = prod.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
|
||||
}
|
||||
}
|
||||
99
pmoaudio/src/dsp/gain_24bits.rs
Normal file
99
pmoaudio/src/dsp/gain_24bits.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use crate::I24;
|
||||
|
||||
/// Applique un gain (en dB) sur des échantillons stéréo interleavés `[L,R]`
|
||||
/// codés sur 24 bits signés (`I24`).
|
||||
pub fn apply_gain_stereo_i24(samples: &mut [[I24; 2]], gain_db: f64) {
|
||||
let gain = 10f64.powf(gain_db / 20.0);
|
||||
// Q23 scaling
|
||||
let g_q23 = (gain * (1u64 << 23) as f64).round() as i32;
|
||||
|
||||
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
|
||||
unsafe {
|
||||
apply_gain_stereo_i24_neon(samples, g_q23);
|
||||
return;
|
||||
}
|
||||
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
|
||||
unsafe {
|
||||
apply_gain_stereo_i24_avx2(samples, g_q23);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback scalaire
|
||||
#[cfg(not(any(
|
||||
all(target_arch = "aarch64", target_feature = "neon"),
|
||||
all(target_arch = "x86_64", target_feature = "avx2")
|
||||
)))]
|
||||
{
|
||||
apply_gain_stereo_i24_scalar(samples, g_q23);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(
|
||||
all(target_arch = "aarch64", target_feature = "neon"),
|
||||
all(target_arch = "x86_64", target_feature = "avx2")
|
||||
)))]
|
||||
#[inline(always)]
|
||||
fn apply_gain_stereo_i24_scalar(samples: &mut [[I24; 2]], g_q23: i32) {
|
||||
for frame in samples.iter_mut() {
|
||||
// L
|
||||
let prod_l = (frame[0].as_i32() as i64 * g_q23 as i64 + (1 << 22)) >> 23;
|
||||
let clamped_l = prod_l.clamp(I24::MIN_VALUE as i64, I24::MAX_VALUE as i64) as i32;
|
||||
frame[0] = I24::new_clamped(clamped_l);
|
||||
|
||||
// R
|
||||
let prod_r = (frame[1].as_i32() as i64 * g_q23 as i64 + (1 << 22)) >> 23;
|
||||
let clamped_r = prod_r.clamp(I24::MIN_VALUE as i64, I24::MAX_VALUE as i64) as i32;
|
||||
frame[1] = I24::new_clamped(clamped_r);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
|
||||
#[inline(always)]
|
||||
unsafe fn apply_gain_stereo_i24_neon(samples: &mut [[I24; 2]], g_q23: i32) {
|
||||
use core::arch::aarch64::*;
|
||||
let gvec = vdupq_n_s32(g_q23);
|
||||
let mut i = 0;
|
||||
let n = samples.len() * 2;
|
||||
let ptr = samples.as_mut_ptr() as *mut i32;
|
||||
|
||||
while i + 4 <= n {
|
||||
let v = vld1q_s32(ptr.add(i));
|
||||
let res = vqdmulhq_s32(v, gvec); // Q23 multiply high
|
||||
vst1q_s32(ptr.add(i), res);
|
||||
i += 4;
|
||||
}
|
||||
|
||||
// reste scalaire
|
||||
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
|
||||
apply_gain_i24_scalar(slice, g_q23);
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
|
||||
#[inline(always)]
|
||||
unsafe fn apply_gain_stereo_i24_avx2(samples: &mut [[I24; 2]], g_q23: i32) {
|
||||
use core::arch::x86_64::*;
|
||||
let g = _mm256_set1_epi32(g_q23);
|
||||
let mut i = 0;
|
||||
let n = samples.len() * 2;
|
||||
let ptr = samples.as_mut_ptr() as *mut i32;
|
||||
|
||||
while i + 8 <= n {
|
||||
let x = _mm256_loadu_si256(ptr.add(i) as *const __m256i);
|
||||
let hi = _mm256_mulhi_epi32(x, g);
|
||||
_mm256_storeu_si256(ptr.add(i) as *mut __m256i, hi);
|
||||
i += 8;
|
||||
}
|
||||
|
||||
// reste scalaire
|
||||
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
|
||||
apply_gain_i24_scalar(slice, g_q23);
|
||||
}
|
||||
|
||||
/// Version mono utilisée pour le reste scalaire.
|
||||
#[inline(always)]
|
||||
fn apply_gain_i24_scalar(samples: &mut [i32], g_q23: i32) {
|
||||
for s in samples.iter_mut() {
|
||||
let prod = (*s as i64 * g_q23 as i64 + (1 << 22)) >> 23;
|
||||
*s = prod.clamp(I24::MIN_VALUE as i64, I24::MAX_VALUE as i64) as i32;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
/// Applique un gain (en dB) sur des échantillons stéréo interleavés `[L,R]`.
|
||||
pub fn apply_gain_stereo(samples: &mut [[i32; 2]], gain_db: f64) {
|
||||
pub fn apply_gain_stereo_i32(samples: &mut [[i32; 2]], gain_db: f64) {
|
||||
let gain = 10f64.powf(gain_db / 20.0);
|
||||
let g_q31 = (gain * (1u64 << 31) as f64).round() as i32;
|
||||
|
||||
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
|
||||
unsafe {
|
||||
apply_gain_stereo_neon(samples, g_q31);
|
||||
apply_gain_stereo_i32_neon(samples, g_q31);
|
||||
return;
|
||||
}
|
||||
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
|
||||
unsafe {
|
||||
apply_gain_stereo_avx2(samples, g_q31);
|
||||
apply_gain_stereo_i32_avx2(samples, g_q31);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub fn apply_gain_stereo(samples: &mut [[i32; 2]], gain_db: f64) {
|
||||
all(target_arch = "x86_64", target_feature = "avx2")
|
||||
)))]
|
||||
{
|
||||
apply_gain_stereo_scalar(samples, g_q31);
|
||||
apply_gain_stereo_i32_scalar(samples, g_q31);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn apply_gain_stereo(samples: &mut [[i32; 2]], gain_db: f64) {
|
||||
all(target_arch = "x86_64", target_feature = "avx2")
|
||||
)))]
|
||||
#[inline(always)]
|
||||
fn apply_gain_stereo_scalar(samples: &mut [[i32; 2]], g_q31: i32) {
|
||||
fn apply_gain_stereo_i32_scalar(samples: &mut [[i32; 2]], g_q31: i32) {
|
||||
for frame in samples.iter_mut() {
|
||||
// L
|
||||
let prod_l = (frame[0] as i64 * g_q31 as i64 + (1 << 30)) >> 31;
|
||||
@@ -43,7 +43,7 @@ fn apply_gain_stereo_scalar(samples: &mut [[i32; 2]], g_q31: i32) {
|
||||
|
||||
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
|
||||
#[inline(always)]
|
||||
unsafe fn apply_gain_stereo_neon(samples: &mut [[i32; 2]], g_q31: i32) {
|
||||
unsafe fn apply_gain_stereo_i32_neon(samples: &mut [[i32; 2]], g_q31: i32) {
|
||||
use core::arch::aarch64::*;
|
||||
let gvec = vdupq_n_s32(g_q31);
|
||||
let mut i = 0;
|
||||
@@ -59,12 +59,12 @@ unsafe fn apply_gain_stereo_neon(samples: &mut [[i32; 2]], g_q31: i32) {
|
||||
|
||||
// reste scalaire
|
||||
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
|
||||
apply_gain_scalar(slice, g_q31);
|
||||
apply_gain_i32_scalar(slice, g_q31);
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
|
||||
#[inline(always)]
|
||||
unsafe fn apply_gain_stereo_avx2(samples: &mut [[i32; 2]], g_q31: i32) {
|
||||
unsafe fn apply_gain_stereo_i32_avx2(samples: &mut [[i32; 2]], g_q31: i32) {
|
||||
use core::arch::x86_64::*;
|
||||
let g = _mm256_set1_epi32(g_q31);
|
||||
let mut i = 0;
|
||||
@@ -80,12 +80,12 @@ unsafe fn apply_gain_stereo_avx2(samples: &mut [[i32; 2]], g_q31: i32) {
|
||||
|
||||
// reste scalaire
|
||||
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
|
||||
apply_gain_scalar(slice, g_q31);
|
||||
apply_gain_i32_scalar(slice, g_q31);
|
||||
}
|
||||
|
||||
/// version mono utilisée pour le reste scalaire
|
||||
#[inline(always)]
|
||||
fn apply_gain_scalar(samples: &mut [i32], g_q31: i32) {
|
||||
fn apply_gain_i32_scalar(samples: &mut [i32], g_q31: i32) {
|
||||
for s in samples.iter_mut() {
|
||||
let prod = (*s as i64 * g_q31 as i64 + (1 << 30)) >> 31;
|
||||
*s = prod.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
|
||||
@@ -1,5 +1,5 @@
|
||||
use bytemuck::{cast_slice, cast_slice_mut};
|
||||
use crate::BitDepth;
|
||||
use bytemuck::{cast_slice, cast_slice_mut};
|
||||
|
||||
#[cfg(feature = "simd")]
|
||||
use std::simd::num::{SimdFloat, SimdInt};
|
||||
@@ -183,3 +183,307 @@ pub fn interleaved_f32_to_i32_stereo(
|
||||
let input_pairs: &[[f32; 2]] = cast_slice(input_interleaved);
|
||||
pairs_f32_to_i32_stereo(input_pairs, left, right, bit_depth);
|
||||
}
|
||||
|
||||
/* ====================== CONVERSIONS I16 ↔ F32 SIMD ====================== */
|
||||
|
||||
/// Convertit deux canaux i16 (L/R) en pairs f32 normalisées [-1.0, 1.0]
|
||||
#[cfg(feature = "simd")]
|
||||
fn i16_stereo_to_pairs_f32_inner(
|
||||
left: &[i16],
|
||||
right: &[i16],
|
||||
out_pairs: &mut [[f32; 2]],
|
||||
max_value: f32,
|
||||
) {
|
||||
debug_assert_eq!(left.len(), right.len());
|
||||
debug_assert_eq!(out_pairs.len(), left.len());
|
||||
|
||||
const LANES: usize = 8;
|
||||
type Vf32 = Simd<f32, LANES>;
|
||||
type Vi32 = Simd<i32, LANES>;
|
||||
|
||||
let scale = Vf32::splat(1.0 / max_value);
|
||||
|
||||
let (l_chunks, l_tail) = left.as_chunks::<LANES>();
|
||||
let (r_chunks, r_tail) = right.as_chunks::<LANES>();
|
||||
let (o_chunks, o_tail) = out_pairs.as_chunks_mut::<LANES>();
|
||||
|
||||
for (k, o) in o_chunks.iter_mut().enumerate() {
|
||||
// Charger i16, caster en i32 puis en f32
|
||||
let l_arr: [i32; LANES] = std::array::from_fn(|i| l_chunks[k][i] as i32);
|
||||
let r_arr: [i32; LANES] = std::array::from_fn(|i| r_chunks[k][i] as i32);
|
||||
|
||||
let l = Vi32::from_array(l_arr).cast::<f32>() * scale;
|
||||
let r = Vi32::from_array(r_arr).cast::<f32>() * scale;
|
||||
|
||||
for j in 0..LANES {
|
||||
unsafe {
|
||||
*o.get_unchecked_mut(j) = [l[j], r[j]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 * scale_scalar;
|
||||
dst[1] = r as f32 * scale_scalar;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "simd"))]
|
||||
fn i16_stereo_to_pairs_f32_inner(
|
||||
left: &[i16],
|
||||
right: &[i16],
|
||||
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 / 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit deux canaux i16 (L/R) en pairs f32 normalisées [-1.0, 1.0]
|
||||
pub fn i16_stereo_to_pairs_f32(
|
||||
left: &[i16],
|
||||
right: &[i16],
|
||||
out_pairs: &mut [[f32; 2]],
|
||||
) {
|
||||
i16_stereo_to_pairs_f32_inner(left, right, out_pairs, 32768.0);
|
||||
}
|
||||
|
||||
/// Convertit pairs f32 normalisées [-1.0, 1.0] en deux canaux i16 (L/R)
|
||||
#[cfg(feature = "simd")]
|
||||
fn pairs_f32_to_i16_stereo_inner(
|
||||
input_pairs: &[[f32; 2]],
|
||||
left: &mut [i16],
|
||||
right: &mut [i16],
|
||||
max_value: f32,
|
||||
) {
|
||||
debug_assert_eq!(input_pairs.len(), left.len());
|
||||
debug_assert_eq!(input_pairs.len(), right.len());
|
||||
|
||||
const LANES: usize = 8;
|
||||
type Vf32 = Simd<f32, LANES>;
|
||||
|
||||
let vmin = -max_value;
|
||||
let vmax_clamp = max_value - 1.0;
|
||||
let vscale = Vf32::splat(max_value);
|
||||
let vminv = Vf32::splat(vmin);
|
||||
let vmaxv = Vf32::splat(vmax_clamp);
|
||||
|
||||
let (in_chunks, in_tail) = input_pairs.as_chunks::<LANES>();
|
||||
let (l_chunks, l_tail) = left.as_chunks_mut::<LANES>();
|
||||
let (r_chunks, r_tail) = right.as_chunks_mut::<LANES>();
|
||||
|
||||
for (k, blk) in in_chunks.iter().enumerate() {
|
||||
let mut l_arr = [0.0f32; LANES];
|
||||
let mut r_arr = [0.0f32; LANES];
|
||||
for j in 0..LANES {
|
||||
let p = blk[j];
|
||||
l_arr[j] = p[0];
|
||||
r_arr[j] = p[1];
|
||||
}
|
||||
|
||||
let lq = (Vf32::from_array(l_arr) * vscale)
|
||||
.simd_clamp(vminv, vmaxv)
|
||||
.round()
|
||||
.cast::<i32>();
|
||||
let rq = (Vf32::from_array(r_arr) * vscale)
|
||||
.simd_clamp(vminv, vmaxv)
|
||||
.round()
|
||||
.cast::<i32>();
|
||||
|
||||
for j in 0..LANES {
|
||||
l_chunks[k][j] = lq[j] as i16;
|
||||
r_chunks[k][j] = rq[j] as i16;
|
||||
}
|
||||
}
|
||||
|
||||
for (j, (l, r)) in in_tail.iter().zip(l_tail.iter_mut().zip(r_tail.iter_mut())) {
|
||||
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 i16;
|
||||
*r = rx as i16;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "simd"))]
|
||||
fn pairs_f32_to_i16_stereo_inner(
|
||||
input_pairs: &[[f32; 2]],
|
||||
left: &mut [i16],
|
||||
right: &mut [i16],
|
||||
max_value: f32,
|
||||
) {
|
||||
debug_assert_eq!(input_pairs.len(), left.len());
|
||||
debug_assert_eq!(input_pairs.len(), right.len());
|
||||
|
||||
let vmin = -max_value;
|
||||
let vmax_clamp = max_value - 1.0;
|
||||
for (i, pair) in input_pairs.iter().enumerate() {
|
||||
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 i16;
|
||||
right[i] = rx as i16;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit pairs f32 normalisées [-1.0, 1.0] en deux canaux i16 (L/R)
|
||||
pub fn pairs_f32_to_i16_stereo(
|
||||
input_pairs: &[[f32; 2]],
|
||||
left: &mut [i16],
|
||||
right: &mut [i16],
|
||||
) {
|
||||
pairs_f32_to_i16_stereo_inner(input_pairs, left, right, 32768.0);
|
||||
}
|
||||
|
||||
/* ====================== CONVERSIONS I24 ↔ F32 SIMD ====================== */
|
||||
|
||||
/// Convertit deux canaux i32 (contenant des valeurs I24) en pairs f32 normalisées
|
||||
#[cfg(feature = "simd")]
|
||||
fn i24_as_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());
|
||||
|
||||
const LANES: usize = 8;
|
||||
type Vf32 = Simd<f32, LANES>;
|
||||
type Vi32 = Simd<i32, LANES>;
|
||||
|
||||
let scale = Vf32::splat(1.0 / max_value);
|
||||
|
||||
let (l_chunks, l_tail) = left.as_chunks::<LANES>();
|
||||
let (r_chunks, r_tail) = right.as_chunks::<LANES>();
|
||||
let (o_chunks, o_tail) = out_pairs.as_chunks_mut::<LANES>();
|
||||
|
||||
for (k, o) in o_chunks.iter_mut().enumerate() {
|
||||
let l = Vi32::from_slice(&l_chunks[k]).cast::<f32>() * scale;
|
||||
let r = Vi32::from_slice(&r_chunks[k]).cast::<f32>() * scale;
|
||||
|
||||
for j in 0..LANES {
|
||||
unsafe {
|
||||
*o.get_unchecked_mut(j) = [l[j], r[j]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 * scale_scalar;
|
||||
dst[1] = r as f32 * scale_scalar;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "simd"))]
|
||||
fn i24_as_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 / 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit deux canaux i32 (contenant des valeurs I24) en pairs f32 normalisées
|
||||
pub fn i24_as_i32_stereo_to_pairs_f32(
|
||||
left: &[i32],
|
||||
right: &[i32],
|
||||
out_pairs: &mut [[f32; 2]],
|
||||
) {
|
||||
i24_as_i32_stereo_to_pairs_f32_inner(left, right, out_pairs, 8388608.0);
|
||||
}
|
||||
|
||||
/// Convertit pairs f32 normalisées en deux canaux i32 (valeurs I24 range)
|
||||
#[cfg(feature = "simd")]
|
||||
fn pairs_f32_to_i24_as_i32_stereo_inner(
|
||||
input_pairs: &[[f32; 2]],
|
||||
left: &mut [i32],
|
||||
right: &mut [i32],
|
||||
max_value: f32,
|
||||
) {
|
||||
debug_assert_eq!(input_pairs.len(), left.len());
|
||||
debug_assert_eq!(input_pairs.len(), right.len());
|
||||
|
||||
const LANES: usize = 8;
|
||||
type Vf32 = Simd<f32, LANES>;
|
||||
|
||||
let vmin = -max_value;
|
||||
let vmax_clamp = max_value - 1.0;
|
||||
let vscale = Vf32::splat(max_value);
|
||||
let vminv = Vf32::splat(vmin);
|
||||
let vmaxv = Vf32::splat(vmax_clamp);
|
||||
|
||||
let (in_chunks, in_tail) = input_pairs.as_chunks::<LANES>();
|
||||
let (l_chunks, l_tail) = left.as_chunks_mut::<LANES>();
|
||||
let (r_chunks, r_tail) = right.as_chunks_mut::<LANES>();
|
||||
|
||||
for (k, blk) in in_chunks.iter().enumerate() {
|
||||
let mut l_arr = [0.0f32; LANES];
|
||||
let mut r_arr = [0.0f32; LANES];
|
||||
for j in 0..LANES {
|
||||
let p = blk[j];
|
||||
l_arr[j] = p[0];
|
||||
r_arr[j] = p[1];
|
||||
}
|
||||
|
||||
let lq = (Vf32::from_array(l_arr) * vscale)
|
||||
.simd_clamp(vminv, vmaxv)
|
||||
.round();
|
||||
let rq = (Vf32::from_array(r_arr) * vscale)
|
||||
.simd_clamp(vminv, vmaxv)
|
||||
.round();
|
||||
|
||||
lq.cast::<i32>().copy_to_slice(&mut l_chunks[k]);
|
||||
rq.cast::<i32>().copy_to_slice(&mut r_chunks[k]);
|
||||
}
|
||||
|
||||
for (j, (l, r)) in in_tail.iter().zip(l_tail.iter_mut().zip(r_tail.iter_mut())) {
|
||||
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"))]
|
||||
fn pairs_f32_to_i24_as_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 vmin = -max_value;
|
||||
let vmax_clamp = max_value - 1.0;
|
||||
for (i, pair) in input_pairs.iter().enumerate() {
|
||||
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 en deux canaux i32 (valeurs I24 range)
|
||||
pub fn pairs_f32_to_i24_as_i32_stereo(
|
||||
input_pairs: &[[f32; 2]],
|
||||
left: &mut [i32],
|
||||
right: &mut [i32],
|
||||
) {
|
||||
pairs_f32_to_i24_as_i32_stereo_inner(input_pairs, left, right, 8388608.0);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
//! Module DSP pour les conversions et traitements audio optimisés (SIMD)
|
||||
|
||||
pub mod depth;
|
||||
pub mod gain;
|
||||
pub mod gain_16bits;
|
||||
pub mod gain_24bits;
|
||||
pub mod gain_32bits;
|
||||
pub mod int_float;
|
||||
pub mod resampling;
|
||||
|
||||
pub use depth::bitdepth_change_stereo;
|
||||
pub use gain::apply_gain_stereo;
|
||||
pub use gain_16bits::apply_gain_stereo_i16;
|
||||
pub use gain_24bits::apply_gain_stereo_i24;
|
||||
pub use gain_32bits::apply_gain_stereo_i32;
|
||||
|
||||
pub use int_float::{
|
||||
i32_stereo_to_interleaved_f32, i32_stereo_to_pairs_f32, interleaved_f32_to_i32_stereo,
|
||||
pairs_f32_to_i32_stereo,
|
||||
i16_stereo_to_pairs_f32, i24_as_i32_stereo_to_pairs_f32, i32_stereo_to_interleaved_f32,
|
||||
i32_stereo_to_pairs_f32, interleaved_f32_to_i32_stereo, pairs_f32_to_i16_stereo,
|
||||
pairs_f32_to_i24_as_i32_stereo, pairs_f32_to_i32_stereo,
|
||||
};
|
||||
|
||||
pub use resampling::resampling;
|
||||
|
||||
@@ -81,12 +81,13 @@ async fn main() {
|
||||
use std::simd::*;
|
||||
|
||||
mod audio_chunk;
|
||||
pub mod events;
|
||||
// mod nodes; // Temporairement déplacé hors du module
|
||||
mod sync_marker;
|
||||
mod audio_segment;
|
||||
mod sample_types;
|
||||
pub mod conversions;
|
||||
pub mod events;
|
||||
pub mod nodes;
|
||||
mod sample_types;
|
||||
mod sync_marker;
|
||||
pub mod type_constraints;
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
@@ -94,18 +95,32 @@ pub mod bit_depth;
|
||||
pub mod dsp;
|
||||
|
||||
pub use audio_segment::{AudioSegment, _AudioSegment};
|
||||
pub use sync_marker::{SyncMarker};
|
||||
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 audio_chunk::{
|
||||
gain_db_from_linear, gain_linear_from_db, AudioChunk, AudioChunkData, AudioFloatChunk,
|
||||
AudioIntegerChunk,
|
||||
};
|
||||
pub use bit_depth::{Bit16, Bit24, Bit32, Bit8, BitDepth};
|
||||
pub use sample_types::{I24, Sample};
|
||||
|
||||
pub use sample_types::{Sample, I24};
|
||||
pub use type_constraints::{
|
||||
check_compatibility, SampleType, TypeCategory, TypeMismatch, TypeRequirement,
|
||||
};
|
||||
|
||||
pub use events::{
|
||||
AudioDataEvent, EventPublisher, EventReceiver, NodeEvent, NodeListener, SourceNameUpdateEvent,
|
||||
VolumeChangeEvent,
|
||||
};
|
||||
|
||||
// Exports publics des nodes
|
||||
pub use nodes::{
|
||||
converter_nodes::{ToF32Node, ToF64Node, ToI16Node, ToI24Node, ToI32Node},
|
||||
file_source::FileSource,
|
||||
flac_file_sink::{FlacFileSink, FlacFileSinkStats},
|
||||
http_source::HttpSource,
|
||||
AudioError, AudioNode, MultiSubscriberNode, SingleSubscriberNode, TypedAudioNode,
|
||||
};
|
||||
|
||||
// Nodes temporairement désactivés
|
||||
/*
|
||||
pub use nodes::{
|
||||
@@ -114,13 +129,10 @@ pub use nodes::{
|
||||
decoder_node::DecoderNode,
|
||||
disk_sink::{AudioFileFormat, DiskSink, DiskSinkConfig, DiskSinkStats},
|
||||
dsp_node::DspNode,
|
||||
file_source::FileSource,
|
||||
flac_file_sink::{FlacFileSink, FlacFileSinkStats},
|
||||
mpd_sink::{MpdAudioFormat, MpdConfig, MpdHandle, MpdSink, MpdStats},
|
||||
sink_node::{SinkNode, SinkStats},
|
||||
source_node::SourceNode,
|
||||
timer_node::{TimerHandle, TimerNode},
|
||||
volume_node::{HardwareVolumeNode, VolumeHandle, VolumeNode},
|
||||
AudioError, AudioNode, MultiSubscriberNode, SingleSubscriberNode,
|
||||
};
|
||||
*/
|
||||
|
||||
@@ -14,12 +14,6 @@
|
||||
/// ```
|
||||
#[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),
|
||||
@@ -68,7 +62,6 @@ macro_rules! extract_chunk_data {
|
||||
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,
|
||||
@@ -94,24 +87,11 @@ macro_rules! match_chunk {
|
||||
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)
|
||||
}
|
||||
$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),
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -130,9 +110,6 @@ macro_rules! map_chunk {
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! is_chunk_type {
|
||||
($chunk:expr, I8) => {
|
||||
matches!($chunk, $crate::AudioChunk::I8(_))
|
||||
};
|
||||
($chunk:expr, I16) => {
|
||||
matches!($chunk, $crate::AudioChunk::I16(_))
|
||||
};
|
||||
@@ -280,13 +257,15 @@ mod tests {
|
||||
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);
|
||||
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 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()),
|
||||
|
||||
472
pmoaudio/src/nodes/converter_nodes.rs
Normal file
472
pmoaudio/src/nodes/converter_nodes.rs
Normal file
@@ -0,0 +1,472 @@
|
||||
//! Nodes de conversion de type pour AudioChunk
|
||||
//!
|
||||
//! Ces nodes permettent de convertir les chunks audio d'un type vers un autre
|
||||
//! (I16, I24, I32, F32, F64). Toutes les conversions utilisent les fonctions
|
||||
//! DSP optimisées SIMD du module `crate::conversions`.
|
||||
//!
|
||||
//! Le designer de pipeline doit insérer manuellement ces nodes pour gérer
|
||||
//! les incompatibilités de type entre producers et consumers.
|
||||
|
||||
use crate::{
|
||||
nodes::{AudioError, MultiSubscriberNode, TypedAudioNode},
|
||||
type_constraints::{SampleType, TypeRequirement},
|
||||
AudioSegment,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Node de conversion vers I16
|
||||
///
|
||||
/// Convertit n'importe quel type de chunk audio vers I16 (16-bit signed integer).
|
||||
/// Utilise les conversions DSP SIMD optimisées.
|
||||
pub struct ToI16Node {
|
||||
rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl ToI16Node {
|
||||
/// Crée un nouveau node de conversion vers I16
|
||||
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
Self::with_channel_size(16)
|
||||
}
|
||||
|
||||
/// Crée un nouveau node avec une taille de buffer spécifique
|
||||
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
};
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Ajoute un abonné qui recevra les segments audio convertis
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Lance le traitement de conversion
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(segment) = self.rx.recv().await {
|
||||
// Si c'est un syncmarker, passer directement
|
||||
if !segment.is_audio_chunk() {
|
||||
self.subscribers.push(segment).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convertir le chunk audio vers I16
|
||||
let converted_segment = if let Some(chunk) = segment.as_chunk() {
|
||||
let converted_chunk = chunk.to_i16();
|
||||
Arc::new(AudioSegment {
|
||||
order: segment.order,
|
||||
timestamp_sec: segment.timestamp_sec,
|
||||
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
|
||||
})
|
||||
} else {
|
||||
segment
|
||||
};
|
||||
|
||||
self.subscribers.push(converted_segment).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedAudioNode for ToI16Node {
|
||||
fn input_type(&self) -> Option<TypeRequirement> {
|
||||
// Accepte n'importe quel type
|
||||
Some(TypeRequirement::any())
|
||||
}
|
||||
|
||||
fn output_type(&self) -> Option<TypeRequirement> {
|
||||
// Produit uniquement I16
|
||||
Some(TypeRequirement::specific(SampleType::I16))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToI16Node {
|
||||
fn default() -> Self {
|
||||
Self::new().0
|
||||
}
|
||||
}
|
||||
|
||||
/// Node de conversion vers I24
|
||||
///
|
||||
/// Convertit n'importe quel type de chunk audio vers I24 (24-bit signed integer).
|
||||
/// Utilise les conversions DSP SIMD optimisées.
|
||||
pub struct ToI24Node {
|
||||
rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl ToI24Node {
|
||||
/// Crée un nouveau node de conversion vers I24
|
||||
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
Self::with_channel_size(16)
|
||||
}
|
||||
|
||||
/// Crée un nouveau node avec une taille de buffer spécifique
|
||||
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
};
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Ajoute un abonné qui recevra les segments audio convertis
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Lance le traitement de conversion
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(segment) = self.rx.recv().await {
|
||||
if !segment.is_audio_chunk() {
|
||||
self.subscribers.push(segment).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let converted_segment = if let Some(chunk) = segment.as_chunk() {
|
||||
let converted_chunk = chunk.to_i24();
|
||||
Arc::new(AudioSegment {
|
||||
order: segment.order,
|
||||
timestamp_sec: segment.timestamp_sec,
|
||||
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
|
||||
})
|
||||
} else {
|
||||
segment
|
||||
};
|
||||
|
||||
self.subscribers.push(converted_segment).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedAudioNode for ToI24Node {
|
||||
fn input_type(&self) -> Option<TypeRequirement> {
|
||||
Some(TypeRequirement::any())
|
||||
}
|
||||
|
||||
fn output_type(&self) -> Option<TypeRequirement> {
|
||||
Some(TypeRequirement::specific(SampleType::I24))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToI24Node {
|
||||
fn default() -> Self {
|
||||
Self::new().0
|
||||
}
|
||||
}
|
||||
|
||||
/// Node de conversion vers I32
|
||||
///
|
||||
/// Convertit n'importe quel type de chunk audio vers I32 (32-bit signed integer).
|
||||
/// Utilise les conversions DSP SIMD optimisées.
|
||||
pub struct ToI32Node {
|
||||
rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl ToI32Node {
|
||||
/// Crée un nouveau node de conversion vers I32
|
||||
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
Self::with_channel_size(16)
|
||||
}
|
||||
|
||||
/// Crée un nouveau node avec une taille de buffer spécifique
|
||||
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
};
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Ajoute un abonné qui recevra les segments audio convertis
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Lance le traitement de conversion
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(segment) = self.rx.recv().await {
|
||||
if !segment.is_audio_chunk() {
|
||||
self.subscribers.push(segment).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let converted_segment = if let Some(chunk) = segment.as_chunk() {
|
||||
let converted_chunk = chunk.to_i32();
|
||||
Arc::new(AudioSegment {
|
||||
order: segment.order,
|
||||
timestamp_sec: segment.timestamp_sec,
|
||||
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
|
||||
})
|
||||
} else {
|
||||
segment
|
||||
};
|
||||
|
||||
self.subscribers.push(converted_segment).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedAudioNode for ToI32Node {
|
||||
fn input_type(&self) -> Option<TypeRequirement> {
|
||||
Some(TypeRequirement::any())
|
||||
}
|
||||
|
||||
fn output_type(&self) -> Option<TypeRequirement> {
|
||||
Some(TypeRequirement::specific(SampleType::I32))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToI32Node {
|
||||
fn default() -> Self {
|
||||
Self::new().0
|
||||
}
|
||||
}
|
||||
|
||||
/// Node de conversion vers F32
|
||||
///
|
||||
/// Convertit n'importe quel type de chunk audio vers F32 (32-bit floating point).
|
||||
/// Utilise les conversions DSP SIMD optimisées.
|
||||
pub struct ToF32Node {
|
||||
rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl ToF32Node {
|
||||
/// Crée un nouveau node de conversion vers F32
|
||||
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
Self::with_channel_size(16)
|
||||
}
|
||||
|
||||
/// Crée un nouveau node avec une taille de buffer spécifique
|
||||
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
};
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Ajoute un abonné qui recevra les segments audio convertis
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Lance le traitement de conversion
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(segment) = self.rx.recv().await {
|
||||
if !segment.is_audio_chunk() {
|
||||
self.subscribers.push(segment).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let converted_segment = if let Some(chunk) = segment.as_chunk() {
|
||||
let converted_chunk = chunk.to_f32();
|
||||
Arc::new(AudioSegment {
|
||||
order: segment.order,
|
||||
timestamp_sec: segment.timestamp_sec,
|
||||
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
|
||||
})
|
||||
} else {
|
||||
segment
|
||||
};
|
||||
|
||||
self.subscribers.push(converted_segment).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedAudioNode for ToF32Node {
|
||||
fn input_type(&self) -> Option<TypeRequirement> {
|
||||
Some(TypeRequirement::any())
|
||||
}
|
||||
|
||||
fn output_type(&self) -> Option<TypeRequirement> {
|
||||
Some(TypeRequirement::specific(SampleType::F32))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToF32Node {
|
||||
fn default() -> Self {
|
||||
Self::new().0
|
||||
}
|
||||
}
|
||||
|
||||
/// Node de conversion vers F64
|
||||
///
|
||||
/// Convertit n'importe quel type de chunk audio vers F64 (64-bit floating point).
|
||||
/// Utilise les conversions DSP SIMD optimisées.
|
||||
pub struct ToF64Node {
|
||||
rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl ToF64Node {
|
||||
/// Crée un nouveau node de conversion vers F64
|
||||
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
Self::with_channel_size(16)
|
||||
}
|
||||
|
||||
/// Crée un nouveau node avec une taille de buffer spécifique
|
||||
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
};
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Ajoute un abonné qui recevra les segments audio convertis
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Lance le traitement de conversion
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(segment) = self.rx.recv().await {
|
||||
if !segment.is_audio_chunk() {
|
||||
self.subscribers.push(segment).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let converted_segment = if let Some(chunk) = segment.as_chunk() {
|
||||
let converted_chunk = chunk.to_f64();
|
||||
Arc::new(AudioSegment {
|
||||
order: segment.order,
|
||||
timestamp_sec: segment.timestamp_sec,
|
||||
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
|
||||
})
|
||||
} else {
|
||||
segment
|
||||
};
|
||||
|
||||
self.subscribers.push(converted_segment).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedAudioNode for ToF64Node {
|
||||
fn input_type(&self) -> Option<TypeRequirement> {
|
||||
Some(TypeRequirement::any())
|
||||
}
|
||||
|
||||
fn output_type(&self) -> Option<TypeRequirement> {
|
||||
Some(TypeRequirement::specific(SampleType::F64))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToF64Node {
|
||||
fn default() -> Self {
|
||||
Self::new().0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{AudioChunk, AudioChunkData};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_to_f32_node_type_requirements() {
|
||||
let (node, _tx) = ToF32Node::new();
|
||||
|
||||
// Vérifier les types d'entrée/sortie
|
||||
assert_eq!(
|
||||
node.input_type().unwrap().get_accepted_types().len(),
|
||||
5,
|
||||
"Should accept all 5 types"
|
||||
);
|
||||
assert_eq!(
|
||||
node.output_type()
|
||||
.unwrap()
|
||||
.get_accepted_types()
|
||||
.first()
|
||||
.copied(),
|
||||
Some(SampleType::F32),
|
||||
"Should output F32 only"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_to_i16_node_converts_from_i32() {
|
||||
let (mut node, tx) = ToI16Node::new();
|
||||
let (out_tx, mut out_rx) = mpsc::channel(16);
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
// Lancer le node dans une tâche
|
||||
let handle = tokio::spawn(async move { node.run().await });
|
||||
|
||||
// Créer et envoyer un chunk I32
|
||||
let stereo = vec![[1_000_000i32 << 16, -500_000i32 << 16]; 100];
|
||||
let chunk_data = AudioChunkData::new(stereo.clone(), 48_000, 0.0);
|
||||
let chunk = AudioChunk::I32(chunk_data);
|
||||
let segment = Arc::new(AudioSegment {
|
||||
order: 0,
|
||||
timestamp_sec: 0.0,
|
||||
segment: crate::_AudioSegment::Chunk(Arc::new(chunk)),
|
||||
});
|
||||
|
||||
tx.send(segment).await.unwrap();
|
||||
drop(tx);
|
||||
|
||||
// Recevoir le chunk converti
|
||||
let result = out_rx.recv().await.unwrap();
|
||||
assert!(result.is_audio_chunk());
|
||||
|
||||
if let Some(converted) = result.as_chunk() {
|
||||
assert_eq!(converted.type_name(), "i16");
|
||||
assert_eq!(converted.len(), 100);
|
||||
|
||||
// Vérifier la conversion (downsampling de I32 vers I16)
|
||||
if let AudioChunk::I16(data) = &**converted {
|
||||
for (orig, converted_frame) in stereo.iter().zip(data.frames().iter()) {
|
||||
let expected_l = (orig[0] >> 16) as i16;
|
||||
let expected_r = (orig[1] >> 16) as i16;
|
||||
assert_eq!(converted_frame[0], expected_l);
|
||||
assert_eq!(converted_frame[1], expected_r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_syncmarkers_passthrough() {
|
||||
let (mut node, tx) = ToI16Node::new();
|
||||
let (out_tx, mut out_rx) = mpsc::channel(16);
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un syncmarker
|
||||
let top_zero = AudioSegment::new_top_zero_sync();
|
||||
tx.send(top_zero.clone()).await.unwrap();
|
||||
drop(tx);
|
||||
|
||||
// Recevoir le syncmarker
|
||||
let result = out_rx.recv().await.unwrap();
|
||||
assert!(!result.is_audio_chunk());
|
||||
assert!(result.as_sync_marker().is_some());
|
||||
}
|
||||
}
|
||||
426
pmoaudio/src/nodes/file_source.rs
Normal file
426
pmoaudio/src/nodes/file_source.rs
Normal file
@@ -0,0 +1,426 @@
|
||||
use crate::{
|
||||
nodes::{AudioError, MultiSubscriberNode, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
||||
type_constraints::TypeRequirement,
|
||||
AudioChunk, AudioChunkData, AudioSegment, I24,
|
||||
};
|
||||
use pmoflac::{decode_audio_stream, AudioFileMetadata, StreamInfo};
|
||||
use pmometadata::{MemoryTrackMetadata, TrackMetadata};
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use tokio::{fs::File, io::AsyncReadExt, sync::mpsc};
|
||||
|
||||
/// FileSource - Lit un fichier audio et publie des `AudioSegment`
|
||||
///
|
||||
/// Cette source utilise `pmoflac` pour décoder le fichier (FLAC/MP3/OGG/WAV/AIFF)
|
||||
/// puis transforme les échantillons PCM en `AudioSegment` stéréo avec le type approprié
|
||||
/// (I16, I24, ou I32) selon la profondeur de bit du fichier source.
|
||||
///
|
||||
/// Le node émet trois types de syncmarkers :
|
||||
/// - `TopZeroSync` au début du flux
|
||||
/// - `TrackBoundary` avec les métadonnées du fichier
|
||||
/// - `EndOfStream` à la fin du flux
|
||||
pub struct FileSource {
|
||||
path: PathBuf,
|
||||
chunk_frames: usize,
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl FileSource {
|
||||
/// Crée une nouvelle source de fichier avec calcul automatique de la taille des chunks.
|
||||
///
|
||||
/// La taille des chunks sera calculée automatiquement pour obtenir environ 50ms
|
||||
/// de latence par chunk, en fonction du sample rate du fichier.
|
||||
///
|
||||
/// * `path` - chemin du fichier audio à lire
|
||||
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
|
||||
Self::with_chunk_size(path, 0) // 0 = auto-calculer
|
||||
}
|
||||
|
||||
/// Crée une nouvelle source de fichier avec une taille de chunk spécifique.
|
||||
///
|
||||
/// * `path` - chemin du fichier audio à lire
|
||||
/// * `chunk_frames` - nombre d'échantillons par canal par chunk (0 = auto)
|
||||
pub fn with_chunk_size<P: Into<PathBuf>>(path: P, chunk_frames: usize) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
chunk_frames,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ajoute un abonné qui recevra les segments audio.
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Lance la lecture du fichier et diffuse les segments audio.
|
||||
pub async fn run(self) -> Result<(), AudioError> {
|
||||
// Ouvrir le fichier
|
||||
let file = File::open(&self.path).await.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("Failed to open {:?}: {}", self.path, e))
|
||||
})?;
|
||||
|
||||
// Décoder le flux audio
|
||||
let mut stream = decode_audio_stream(file)
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Decode error: {}", e)))?;
|
||||
let stream_info = stream.info().clone();
|
||||
|
||||
validate_stream(&stream_info)?;
|
||||
|
||||
// Calculer la taille des chunks si non spécifiée (0 = auto)
|
||||
let chunk_frames = if self.chunk_frames == 0 {
|
||||
// Calculer pour obtenir DEFAULT_CHUNK_DURATION_MS millisecondes
|
||||
let frames =
|
||||
(stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize;
|
||||
// Arrondir à la puissance de 2 la plus proche pour optimiser les buffers
|
||||
frames.next_power_of_two().max(256)
|
||||
} else {
|
||||
self.chunk_frames.max(1)
|
||||
};
|
||||
|
||||
// Émettre TopZeroSync
|
||||
let top_zero = AudioSegment::new_top_zero_sync();
|
||||
self.subscribers.push(top_zero).await?;
|
||||
|
||||
// Extraire et émettre les métadonnées du fichier
|
||||
match AudioFileMetadata::from_file(&self.path) {
|
||||
Ok(file_metadata) => {
|
||||
let mut metadata = MemoryTrackMetadata::new();
|
||||
|
||||
// Convertir AudioFileMetadata vers MemoryTrackMetadata
|
||||
if let Some(title) = file_metadata.title {
|
||||
let _ = metadata.set_title(Some(title)).await;
|
||||
}
|
||||
if let Some(artist) = file_metadata.artist {
|
||||
let _ = metadata.set_artist(Some(artist)).await;
|
||||
}
|
||||
if let Some(album) = file_metadata.album {
|
||||
let _ = metadata.set_album(Some(album)).await;
|
||||
}
|
||||
if let Some(year) = file_metadata.year {
|
||||
let _ = metadata.set_year(Some(year)).await;
|
||||
}
|
||||
if let Some(duration_secs) = file_metadata.duration_secs {
|
||||
let _ = metadata
|
||||
.set_duration(Some(Duration::from_secs(duration_secs)))
|
||||
.await;
|
||||
}
|
||||
|
||||
// Émettre TrackBoundary
|
||||
let track_boundary = AudioSegment::new_track_boundary(0, 0.0, Arc::new(metadata));
|
||||
self.subscribers.push(track_boundary).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Failed to extract metadata from {:?}: {}",
|
||||
self.path, e
|
||||
);
|
||||
// Continuer sans métadonnées
|
||||
}
|
||||
}
|
||||
|
||||
// Préparer la lecture des chunks audio
|
||||
let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize;
|
||||
let chunk_byte_len = chunk_frames * frame_bytes;
|
||||
let mut pending = Vec::new();
|
||||
let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames)];
|
||||
let mut chunk_index = 0u64;
|
||||
let mut total_frames = 0u64;
|
||||
|
||||
// Lire et émettre les chunks audio
|
||||
loop {
|
||||
// Remplir le buffer
|
||||
if pending.len() < chunk_byte_len {
|
||||
let read = stream.read(&mut read_buf).await.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("I/O error while decoding: {}", e))
|
||||
})?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
pending.extend_from_slice(&read_buf[..read]);
|
||||
}
|
||||
|
||||
if pending.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Extraire un chunk
|
||||
let frames_in_pending = pending.len() / frame_bytes;
|
||||
let frames_to_emit = frames_in_pending.min(chunk_frames);
|
||||
let take_bytes = frames_to_emit * frame_bytes;
|
||||
let chunk_bytes = pending.drain(..take_bytes).collect::<Vec<u8>>();
|
||||
|
||||
// Calculer le timestamp
|
||||
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
|
||||
|
||||
// Créer le segment audio
|
||||
let segment = bytes_to_segment(
|
||||
&chunk_bytes,
|
||||
&stream_info,
|
||||
frames_to_emit,
|
||||
chunk_index,
|
||||
timestamp_sec,
|
||||
)?;
|
||||
self.subscribers.push(segment).await?;
|
||||
|
||||
chunk_index += 1;
|
||||
total_frames += frames_to_emit as u64;
|
||||
}
|
||||
|
||||
// Traiter le reste éventuel (moins qu'un chunk complet)
|
||||
if !pending.is_empty() {
|
||||
let frames = pending.len() / frame_bytes;
|
||||
if frames > 0 {
|
||||
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
|
||||
let segment =
|
||||
bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?;
|
||||
self.subscribers.push(segment).await?;
|
||||
total_frames += frames as u64;
|
||||
chunk_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Émettre EndOfStream
|
||||
let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64;
|
||||
let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp);
|
||||
self.subscribers.push(eos).await?;
|
||||
|
||||
// Attendre la fin du décodage
|
||||
stream
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_stream(info: &StreamInfo) -> Result<(), AudioError> {
|
||||
if !(1..=2).contains(&info.channels) {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported channel count: {}",
|
||||
info.channels
|
||||
)));
|
||||
}
|
||||
match info.bits_per_sample {
|
||||
8 | 16 | 24 | 32 => Ok(()),
|
||||
other => Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported bit depth: {}",
|
||||
other
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit des bytes PCM en AudioSegment avec le type approprié
|
||||
fn bytes_to_segment(
|
||||
chunk_bytes: &[u8],
|
||||
info: &StreamInfo,
|
||||
frames: usize,
|
||||
order: u64,
|
||||
timestamp_sec: f64,
|
||||
) -> Result<Arc<AudioSegment>, AudioError> {
|
||||
let bytes_per_sample = info.bytes_per_sample();
|
||||
let channels = info.channels as usize;
|
||||
let frame_bytes = bytes_per_sample * channels;
|
||||
|
||||
// Créer le chunk du bon type selon la profondeur de bit
|
||||
let chunk = match info.bits_per_sample {
|
||||
16 => {
|
||||
// Type I16
|
||||
let mut stereo = Vec::with_capacity(frames);
|
||||
for frame_idx in 0..frames {
|
||||
let base = frame_idx * frame_bytes;
|
||||
let l = i16::from_le_bytes(
|
||||
chunk_bytes[base..base + bytes_per_sample]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
let r = if channels == 1 {
|
||||
l
|
||||
} else {
|
||||
i16::from_le_bytes(
|
||||
chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
};
|
||||
stereo.push([l, r]);
|
||||
}
|
||||
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
|
||||
AudioChunk::I16(chunk_data)
|
||||
}
|
||||
24 => {
|
||||
// Type I24
|
||||
let mut stereo = Vec::with_capacity(frames);
|
||||
for frame_idx in 0..frames {
|
||||
let base = frame_idx * frame_bytes;
|
||||
let l_i32 = {
|
||||
let mut buf = [0u8; 4];
|
||||
buf[..3].copy_from_slice(&chunk_bytes[base..base + 3]);
|
||||
// Sign extend
|
||||
if chunk_bytes[base + 2] & 0x80 != 0 {
|
||||
buf[3] = 0xFF;
|
||||
}
|
||||
i32::from_le_bytes(buf)
|
||||
};
|
||||
let l = I24::new(l_i32).ok_or_else(|| {
|
||||
AudioError::ProcessingError(format!("Invalid I24 value: {}", l_i32))
|
||||
})?;
|
||||
|
||||
let r = if channels == 1 {
|
||||
l
|
||||
} else {
|
||||
let r_i32 = {
|
||||
let mut buf = [0u8; 4];
|
||||
buf[..3].copy_from_slice(
|
||||
&chunk_bytes[base + bytes_per_sample..base + bytes_per_sample + 3],
|
||||
);
|
||||
// Sign extend
|
||||
if chunk_bytes[base + bytes_per_sample + 2] & 0x80 != 0 {
|
||||
buf[3] = 0xFF;
|
||||
}
|
||||
i32::from_le_bytes(buf)
|
||||
};
|
||||
I24::new(r_i32).ok_or_else(|| {
|
||||
AudioError::ProcessingError(format!("Invalid I24 value: {}", r_i32))
|
||||
})?
|
||||
};
|
||||
stereo.push([l, r]);
|
||||
}
|
||||
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
|
||||
AudioChunk::I24(chunk_data)
|
||||
}
|
||||
32 => {
|
||||
// Type I32
|
||||
let mut stereo = Vec::with_capacity(frames);
|
||||
for frame_idx in 0..frames {
|
||||
let base = frame_idx * frame_bytes;
|
||||
let l = i32::from_le_bytes(
|
||||
chunk_bytes[base..base + bytes_per_sample]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
let r = if channels == 1 {
|
||||
l
|
||||
} else {
|
||||
i32::from_le_bytes(
|
||||
chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
};
|
||||
stereo.push([l, r]);
|
||||
}
|
||||
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
|
||||
AudioChunk::I32(chunk_data)
|
||||
}
|
||||
other => {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported bit depth: {}",
|
||||
other
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// Créer le segment audio
|
||||
Ok(Arc::new(AudioSegment {
|
||||
order,
|
||||
timestamp_sec,
|
||||
segment: crate::_AudioSegment::Chunk(Arc::new(chunk)),
|
||||
}))
|
||||
}
|
||||
|
||||
impl TypedAudioNode for FileSource {
|
||||
fn input_type(&self) -> Option<TypeRequirement> {
|
||||
// FileSource est une source, elle ne consomme pas d'audio
|
||||
None
|
||||
}
|
||||
|
||||
fn output_type(&self) -> Option<TypeRequirement> {
|
||||
// FileSource peut produire n'importe quel type entier (I16, I24, I32)
|
||||
// selon la profondeur de bit du fichier source
|
||||
Some(TypeRequirement::any_integer())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
|
||||
use std::io::Cursor;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_file_source_decodes_flac() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let flac_path = temp_dir.path().join("test.flac");
|
||||
|
||||
let sample_rate = 48_000;
|
||||
let frames = 256;
|
||||
let mut pcm = Vec::with_capacity(frames * 4);
|
||||
for i in 0..frames {
|
||||
let sample = ((i % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5; // simple ramp
|
||||
let sample_i16 = (sample * 32767.0) as i16;
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
}
|
||||
|
||||
let format = PcmFormat {
|
||||
sample_rate,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
};
|
||||
|
||||
let mut flac_stream =
|
||||
encode_flac_stream(Cursor::new(pcm.clone()), format, EncoderOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut file = File::create(&flac_path).await.expect("create flac file");
|
||||
tokio::io::copy(&mut flac_stream, &mut file)
|
||||
.await
|
||||
.expect("write flac");
|
||||
file.flush().await.expect("flush file");
|
||||
flac_stream.wait().await.unwrap();
|
||||
|
||||
let mut source = FileSource::with_chunk_size(&flac_path, 64);
|
||||
let (tx, mut rx) = mpsc::channel(16);
|
||||
source.add_subscriber(tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
source.run().await.unwrap();
|
||||
});
|
||||
|
||||
let mut received_frames = 0usize;
|
||||
let mut received_syncmarkers = 0usize;
|
||||
let mut seen_top_zero = false;
|
||||
let mut seen_eos = false;
|
||||
|
||||
while let Some(segment) = rx.recv().await {
|
||||
if segment.is_audio_chunk() {
|
||||
if let Some(chunk) = segment.as_chunk() {
|
||||
received_frames += chunk.len();
|
||||
assert_eq!(chunk.sample_rate(), sample_rate);
|
||||
}
|
||||
} else {
|
||||
received_syncmarkers += 1;
|
||||
if let Some(marker) = segment.as_sync_marker() {
|
||||
match **marker {
|
||||
crate::SyncMarker::TopZeroSync => seen_top_zero = true,
|
||||
crate::SyncMarker::EndOfStream => seen_eos = true,
|
||||
crate::SyncMarker::TrackBoundary { .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier que tous les frames ont été reçus
|
||||
assert_eq!(received_frames, frames);
|
||||
// Vérifier qu'on a bien reçu des syncmarkers
|
||||
assert!(received_syncmarkers >= 2); // Au moins TopZeroSync et EndOfStream
|
||||
assert!(seen_top_zero, "Should have received TopZeroSync");
|
||||
assert!(seen_eos, "Should have received EndOfStream");
|
||||
}
|
||||
}
|
||||
766
pmoaudio/src/nodes/flac_file_sink.rs
Normal file
766
pmoaudio/src/nodes/flac_file_sink.rs
Normal file
@@ -0,0 +1,766 @@
|
||||
use crate::{
|
||||
nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE},
|
||||
type_constraints::TypeRequirement,
|
||||
AudioChunk, AudioSegment, SyncMarker,
|
||||
};
|
||||
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{self, AsyncRead, AsyncWriteExt, ReadBuf},
|
||||
sync::mpsc,
|
||||
};
|
||||
|
||||
/// Sink qui encode les `AudioSegment` reçus au format FLAC.
|
||||
///
|
||||
/// Ce sink :
|
||||
/// - Filtre les chunks audio et ignore les autres syncmarkers (sauf TrackBoundary et EndOfStream)
|
||||
/// - Crée un nouveau fichier FLAC pour chaque TrackBoundary rencontré
|
||||
/// - Adapte automatiquement l'encodage FLAC selon la profondeur de bit du chunk (8/16/24/32-bit)
|
||||
/// - Termine l'encodage proprement quand il reçoit EndOfStream
|
||||
pub struct FlacFileSink {
|
||||
rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||
base_path: PathBuf,
|
||||
encoder_options: EncoderOptions,
|
||||
pcm_buffer_capacity: usize,
|
||||
}
|
||||
|
||||
impl FlacFileSink {
|
||||
/// Crée un sink FLAC avec les options par défaut (compression 5, buffer de 16 segments).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `base_path` - Chemin de base pour les fichiers FLAC. Si des TrackBoundary sont reçus,
|
||||
/// des fichiers seront créés avec des suffixes (_01, _02, etc.)
|
||||
pub fn new<P: Into<PathBuf>>(base_path: P) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
Self::with_channel_size(base_path, DEFAULT_CHANNEL_SIZE)
|
||||
}
|
||||
|
||||
/// Crée un sink FLAC avec une taille de buffer MPSC personnalisée.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `base_path` - Chemin de base pour les fichiers FLAC
|
||||
/// * `channel_size` - Taille du buffer MPSC (nombre de segments en attente avant backpressure)
|
||||
pub fn with_channel_size<P: Into<PathBuf>>(
|
||||
base_path: P,
|
||||
channel_size: usize,
|
||||
) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
Self::with_config(base_path, channel_size, EncoderOptions::default())
|
||||
}
|
||||
|
||||
/// Crée un sink FLAC avec une configuration complète.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `base_path` - Chemin de base pour les fichiers FLAC
|
||||
/// * `channel_size` - Taille du buffer MPSC
|
||||
/// * `encoder_options` - Options d'encodage FLAC (compression, etc.)
|
||||
pub fn with_config<P: Into<PathBuf>>(
|
||||
base_path: P,
|
||||
channel_size: usize,
|
||||
encoder_options: EncoderOptions,
|
||||
) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
let sink = Self {
|
||||
rx,
|
||||
base_path: base_path.into(),
|
||||
encoder_options,
|
||||
pcm_buffer_capacity: 8,
|
||||
};
|
||||
(sink, tx)
|
||||
}
|
||||
|
||||
/// Lance l'encodage vers le(s) fichier(s) cible(s).
|
||||
///
|
||||
/// Cette méthode crée un nouveau fichier FLAC pour chaque TrackBoundary rencontré.
|
||||
/// Les fichiers sont nommés selon la convention :
|
||||
/// - Track 0 : base_path.flac
|
||||
/// - Track 1 : base_path_01.flac
|
||||
/// - Track 2 : base_path_02.flac, etc.
|
||||
pub async fn run(self) -> Result<FlacFileSinkStats, AudioError> {
|
||||
let FlacFileSink {
|
||||
mut rx,
|
||||
base_path,
|
||||
encoder_options,
|
||||
pcm_buffer_capacity,
|
||||
} = self;
|
||||
|
||||
let mut all_tracks = Vec::new();
|
||||
let mut track_number = 0;
|
||||
|
||||
loop {
|
||||
// Attendre le premier chunk audio pour cette track, en capturant les métadonnées du TrackBoundary
|
||||
let (first_segment, track_metadata) = match wait_for_first_audio_chunk_with_metadata(&mut rx).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
// Plus d'audio disponible
|
||||
if all_tracks.is_empty() {
|
||||
return Err(AudioError::ProcessingError("No audio data received".into()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Extraire les informations du premier chunk
|
||||
let first_chunk = first_segment.as_chunk().unwrap();
|
||||
let sample_rate = first_chunk.sample_rate();
|
||||
let bits_per_sample = get_chunk_bit_depth(first_chunk);
|
||||
|
||||
let format = PcmFormat {
|
||||
sample_rate,
|
||||
channels: 2,
|
||||
bits_per_sample,
|
||||
};
|
||||
if let Err(err) = format.validate() {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Invalid PCM format: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
|
||||
// Générer le chemin du fichier pour cette track
|
||||
let track_path = generate_track_path(&base_path, track_number);
|
||||
|
||||
// Créer le pipeline d'encodage pour cette track
|
||||
let (pcm_tx, pcm_rx) = mpsc::channel::<Vec<u8>>(pcm_buffer_capacity);
|
||||
|
||||
// Préparer les options d'encodage avec les métadonnées du TrackBoundary
|
||||
let mut options_with_metadata = encoder_options.clone();
|
||||
options_with_metadata.metadata = track_metadata;
|
||||
|
||||
// Créer l'encoder et le fichier
|
||||
let reader = ByteStreamReader::new(pcm_rx);
|
||||
let mut flac_stream = encode_flac_stream(reader, format, options_with_metadata)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("FLAC encode init failed: {}", e))
|
||||
})?;
|
||||
|
||||
let mut output = File::create(&track_path).await.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("Failed to create {:?}: {}", track_path, e))
|
||||
})?;
|
||||
|
||||
// Exécuter pump et copy en parallèle avec tokio::select! en boucle
|
||||
let pump_future =
|
||||
pump_track_segments(first_segment, &mut rx, pcm_tx, bits_per_sample, sample_rate);
|
||||
let copy_future = async {
|
||||
let copy_result = tokio::io::copy(&mut flac_stream, &mut output).await;
|
||||
let flush_result = output.flush().await;
|
||||
let wait_result = flac_stream.wait().await;
|
||||
|
||||
copy_result.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("FLAC write failed: {}", e))
|
||||
})?;
|
||||
flush_result
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Failed to flush: {}", e)))?;
|
||||
wait_result
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Encoder failed: {}", e)))?;
|
||||
Ok::<_, AudioError>(())
|
||||
};
|
||||
|
||||
// Attendre les deux tâches en parallèle
|
||||
let (copy_result, pump_result) = tokio::join!(copy_future, pump_future);
|
||||
copy_result?;
|
||||
let (chunks, samples, duration_sec, stop_reason) = pump_result?;
|
||||
|
||||
// Ajouter les stats de cette track
|
||||
all_tracks.push(TrackStats {
|
||||
path: track_path,
|
||||
track_number,
|
||||
chunks_received: chunks,
|
||||
total_samples: samples,
|
||||
total_duration_sec: duration_sec,
|
||||
});
|
||||
|
||||
// Vérifier le stop_reason pour savoir si on continue
|
||||
match stop_reason {
|
||||
StopReason::TrackBoundary(_metadata) => {
|
||||
// Continuer avec la prochaine track
|
||||
track_number += 1;
|
||||
continue;
|
||||
}
|
||||
StopReason::EndOfStream | StopReason::ChannelClosed => {
|
||||
// Fin de l'encodage
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FlacFileSinkStats { tracks: all_tracks })
|
||||
}
|
||||
}
|
||||
|
||||
/// Génère le chemin de fichier pour une track donnée.
|
||||
/// - track 0 → base_path.flac
|
||||
/// - track 1 → base_path_01.flac
|
||||
/// - track 2 → base_path_02.flac, etc.
|
||||
fn generate_track_path(base_path: &Path, track_number: usize) -> PathBuf {
|
||||
if track_number == 0 {
|
||||
base_path.to_path_buf()
|
||||
} else {
|
||||
let stem = base_path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("output");
|
||||
let extension = base_path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("flac");
|
||||
let parent = base_path.parent().unwrap_or(Path::new("."));
|
||||
parent.join(format!("{}_{:02}.{}", stem, track_number, extension))
|
||||
}
|
||||
}
|
||||
|
||||
/// Signal retourné par pump_segments indiquant pourquoi l'encodage s'est arrêté.
|
||||
enum StopReason {
|
||||
TrackBoundary(Arc<dyn pmometadata::TrackMetadata + Send + Sync>),
|
||||
EndOfStream,
|
||||
ChannelClosed,
|
||||
}
|
||||
|
||||
/// Attend et retourne le premier chunk audio avec les métadonnées du TrackBoundary si présent.
|
||||
/// Retourne une erreur si EndOfStream est reçu avant tout audio.
|
||||
async fn wait_for_first_audio_chunk_with_metadata(
|
||||
rx: &mut mpsc::Receiver<Arc<AudioSegment>>,
|
||||
) -> Result<(Arc<AudioSegment>, Option<Arc<dyn pmometadata::TrackMetadata + Send + Sync>>), AudioError> {
|
||||
let mut track_metadata: Option<Arc<dyn pmometadata::TrackMetadata + Send + Sync>> = None;
|
||||
|
||||
loop {
|
||||
let segment = rx
|
||||
.recv()
|
||||
.await
|
||||
.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))?;
|
||||
|
||||
match &segment.segment {
|
||||
crate::_AudioSegment::Chunk(chunk) => {
|
||||
if chunk.len() == 0 {
|
||||
return Err(AudioError::ProcessingError("Received empty chunk".into()));
|
||||
}
|
||||
return Ok((segment, track_metadata));
|
||||
}
|
||||
crate::_AudioSegment::Sync(marker) => {
|
||||
match **marker {
|
||||
SyncMarker::TrackBoundary { ref metadata, .. } => {
|
||||
// Capturer les métadonnées du TrackBoundary
|
||||
track_metadata = Some(metadata.clone());
|
||||
continue;
|
||||
}
|
||||
SyncMarker::EndOfStream => {
|
||||
return Err(AudioError::ProcessingError(
|
||||
"EndOfStream received before any audio".into(),
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
// Ignorer TopZeroSync, Heartbeat, etc.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pompe les segments pour une seule track (s'arrête au TrackBoundary).
|
||||
async fn pump_track_segments(
|
||||
first_segment: Arc<AudioSegment>,
|
||||
rx: &mut mpsc::Receiver<Arc<AudioSegment>>,
|
||||
pcm_tx: mpsc::Sender<Vec<u8>>,
|
||||
bits_per_sample: u8,
|
||||
expected_rate: u32,
|
||||
) -> Result<(u64, u64, f64, StopReason), AudioError> {
|
||||
let mut chunks = 0u64;
|
||||
let mut samples = 0u64;
|
||||
let mut duration_sec = 0.0f64;
|
||||
|
||||
// Traiter le premier segment
|
||||
if let Some(chunk) = first_segment.as_chunk() {
|
||||
let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?;
|
||||
if !pcm_bytes.is_empty() {
|
||||
pcm_tx
|
||||
.send(pcm_bytes)
|
||||
.await
|
||||
.map_err(|_| AudioError::SendError)?;
|
||||
chunks += 1;
|
||||
samples += chunk.len() as u64;
|
||||
duration_sec += chunk.len() as f64 / expected_rate as f64;
|
||||
}
|
||||
}
|
||||
|
||||
// Boucle sur les segments suivants
|
||||
loop {
|
||||
let segment = match rx.recv().await {
|
||||
Some(seg) => seg,
|
||||
None => {
|
||||
drop(pcm_tx); // Fermer le channel PCM
|
||||
return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed));
|
||||
}
|
||||
};
|
||||
|
||||
match &segment.segment {
|
||||
crate::_AudioSegment::Chunk(chunk) => {
|
||||
// Vérifier la cohérence du sample rate
|
||||
if chunk.sample_rate() != expected_rate {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"FlacFileSink: inconsistent sample rate ({} vs {})",
|
||||
chunk.sample_rate(),
|
||||
expected_rate
|
||||
)));
|
||||
}
|
||||
|
||||
let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?;
|
||||
if pcm_bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
pcm_tx
|
||||
.send(pcm_bytes)
|
||||
.await
|
||||
.map_err(|_| AudioError::SendError)?;
|
||||
|
||||
chunks += 1;
|
||||
samples += chunk.len() as u64;
|
||||
duration_sec += chunk.len() as f64 / expected_rate as f64;
|
||||
}
|
||||
crate::_AudioSegment::Sync(marker) => {
|
||||
match &**marker {
|
||||
SyncMarker::TrackBoundary { metadata, .. } => {
|
||||
drop(pcm_tx); // Fermer le channel PCM
|
||||
return Ok((
|
||||
chunks,
|
||||
samples,
|
||||
duration_sec,
|
||||
StopReason::TrackBoundary(metadata.clone()),
|
||||
));
|
||||
}
|
||||
SyncMarker::EndOfStream => {
|
||||
drop(pcm_tx); // Fermer le channel PCM
|
||||
return Ok((chunks, samples, duration_sec, StopReason::EndOfStream));
|
||||
}
|
||||
_ => {} // Ignorer les autres syncmarkers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Détermine la profondeur de bit d'un chunk audio
|
||||
fn get_chunk_bit_depth(chunk: &AudioChunk) -> u8 {
|
||||
match chunk {
|
||||
AudioChunk::I16(_) => 16,
|
||||
AudioChunk::I24(_) => 24,
|
||||
AudioChunk::I32(_) => 32,
|
||||
AudioChunk::F32(_) => 32, // Les flottants seront convertis en 32-bit
|
||||
AudioChunk::F64(_) => 32, // Les flottants seront convertis en 32-bit
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit un chunk audio en bytes PCM avec la profondeur de bit spécifiée
|
||||
fn chunk_to_pcm_bytes(chunk: &AudioChunk, bits_per_sample: u8) -> Result<Vec<u8>, AudioError> {
|
||||
// Vérifier que le chunk est de type entier
|
||||
match chunk {
|
||||
AudioChunk::F32(_) | AudioChunk::F64(_) => {
|
||||
return Err(AudioError::ProcessingError(
|
||||
"FlacFileSink only supports integer audio chunks (I16, I24, I32)".into(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let len = chunk.len();
|
||||
let bytes_per_frame = (bits_per_sample / 8) as usize * 2; // 2 channels
|
||||
let mut bytes = Vec::with_capacity(len * bytes_per_frame);
|
||||
|
||||
// Convertir selon le type du chunk
|
||||
match (chunk, bits_per_sample) {
|
||||
// I16 source
|
||||
(AudioChunk::I16(data), 16) => {
|
||||
for frame in data.frames() {
|
||||
bytes.extend_from_slice(&frame[0].to_le_bytes());
|
||||
bytes.extend_from_slice(&frame[1].to_le_bytes());
|
||||
}
|
||||
}
|
||||
(AudioChunk::I16(data), 24) => {
|
||||
for frame in data.frames() {
|
||||
let left = (frame[0] as i32) << 8;
|
||||
let right = (frame[1] as i32) << 8;
|
||||
bytes.extend_from_slice(&left.to_le_bytes()[..3]);
|
||||
bytes.extend_from_slice(&right.to_le_bytes()[..3]);
|
||||
}
|
||||
}
|
||||
(AudioChunk::I16(data), 32) => {
|
||||
for frame in data.frames() {
|
||||
let left = (frame[0] as i32) << 16;
|
||||
let right = (frame[1] as i32) << 16;
|
||||
bytes.extend_from_slice(&left.to_le_bytes());
|
||||
bytes.extend_from_slice(&right.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
// I24 source
|
||||
(AudioChunk::I24(data), 16) => {
|
||||
for frame in data.frames() {
|
||||
let left = (frame[0].as_i32() >> 8) as i16;
|
||||
let right = (frame[1].as_i32() >> 8) as i16;
|
||||
bytes.extend_from_slice(&left.to_le_bytes());
|
||||
bytes.extend_from_slice(&right.to_le_bytes());
|
||||
}
|
||||
}
|
||||
(AudioChunk::I24(data), 24) => {
|
||||
for frame in data.frames() {
|
||||
bytes.extend_from_slice(&frame[0].as_i32().to_le_bytes()[..3]);
|
||||
bytes.extend_from_slice(&frame[1].as_i32().to_le_bytes()[..3]);
|
||||
}
|
||||
}
|
||||
(AudioChunk::I24(data), 32) => {
|
||||
for frame in data.frames() {
|
||||
let left = frame[0].as_i32() << 8;
|
||||
let right = frame[1].as_i32() << 8;
|
||||
bytes.extend_from_slice(&left.to_le_bytes());
|
||||
bytes.extend_from_slice(&right.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
// I32 source
|
||||
(AudioChunk::I32(data), 16) => {
|
||||
for frame in data.frames() {
|
||||
let left = (frame[0] >> 16) as i16;
|
||||
let right = (frame[1] >> 16) as i16;
|
||||
bytes.extend_from_slice(&left.to_le_bytes());
|
||||
bytes.extend_from_slice(&right.to_le_bytes());
|
||||
}
|
||||
}
|
||||
(AudioChunk::I32(data), 24) => {
|
||||
for frame in data.frames() {
|
||||
let left = frame[0] >> 8;
|
||||
let right = frame[1] >> 8;
|
||||
bytes.extend_from_slice(&left.to_le_bytes()[..3]);
|
||||
bytes.extend_from_slice(&right.to_le_bytes()[..3]);
|
||||
}
|
||||
}
|
||||
(AudioChunk::I32(data), 32) => {
|
||||
for frame in data.frames() {
|
||||
bytes.extend_from_slice(&frame[0].to_le_bytes());
|
||||
bytes.extend_from_slice(&frame[1].to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported bits_per_sample: {}",
|
||||
bits_per_sample
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
struct ByteStreamReader {
|
||||
rx: mpsc::Receiver<Vec<u8>>,
|
||||
buffer: VecDeque<u8>,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl ByteStreamReader {
|
||||
fn new(rx: mpsc::Receiver<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
rx,
|
||||
buffer: VecDeque::new(),
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for ByteStreamReader {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
loop {
|
||||
if !self.buffer.is_empty() {
|
||||
let to_copy = self.buffer.len().min(buf.remaining());
|
||||
if to_copy == 0 {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// VecDeque::make_contiguous pour copier efficacement
|
||||
let slice = self.buffer.make_contiguous();
|
||||
buf.put_slice(&slice[..to_copy]);
|
||||
self.buffer.drain(..to_copy);
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if self.finished {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
match Pin::new(&mut self.rx).poll_recv(cx) {
|
||||
Poll::Ready(Some(bytes)) => {
|
||||
if bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
self.buffer.extend(bytes);
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
self.finished = true;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques pour une track individuelle.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrackStats {
|
||||
pub path: PathBuf,
|
||||
pub track_number: usize,
|
||||
pub chunks_received: u64,
|
||||
pub total_samples: u64,
|
||||
pub total_duration_sec: f64,
|
||||
}
|
||||
|
||||
/// Statistiques produites par le `FlacFileSink`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FlacFileSinkStats {
|
||||
pub tracks: Vec<TrackStats>,
|
||||
}
|
||||
|
||||
impl TypedAudioNode for FlacFileSink {
|
||||
fn input_type(&self) -> Option<TypeRequirement> {
|
||||
// FlacFileSink accepte n'importe quel type entier (I16, I24, I32)
|
||||
// mais rejette les chunks flottants
|
||||
Some(TypeRequirement::any_integer())
|
||||
}
|
||||
|
||||
fn output_type(&self) -> Option<TypeRequirement> {
|
||||
// FlacFileSink est un sink, il ne produit pas d'audio
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pmoflac::{decode_flac_stream, AudioFileMetadata};
|
||||
use pmometadata::{MemoryTrackMetadata, TrackMetadata};
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_flac_file_sink_writes_metadata() {
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let output_path = temp_dir.path().join("output_with_metadata.flac");
|
||||
|
||||
let sample_rate = 44_100;
|
||||
let frames = 256;
|
||||
|
||||
// Créer le sink
|
||||
let (sink, tx) = FlacFileSink::with_channel_size(&output_path, 16);
|
||||
let sink_handle = tokio::spawn(async move { sink.run().await.unwrap() });
|
||||
|
||||
// Envoyer des segments avec métadonnées
|
||||
tokio::spawn(async move {
|
||||
// TopZeroSync
|
||||
tx.send(crate::AudioSegment::new_top_zero_sync())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TrackBoundary avec métadonnées
|
||||
let mut metadata = MemoryTrackMetadata::new();
|
||||
metadata.set_title(Some("Test Track Title".to_string())).await.unwrap();
|
||||
metadata.set_artist(Some("Test Artist".to_string())).await.unwrap();
|
||||
metadata.set_album(Some("Test Album".to_string())).await.unwrap();
|
||||
metadata.set_year(Some(2024)).await.unwrap();
|
||||
|
||||
let track_boundary =
|
||||
crate::AudioSegment::new_track_boundary(0, 0.0, std::sync::Arc::new(metadata));
|
||||
tx.send(track_boundary).await.unwrap();
|
||||
|
||||
// Générer et envoyer des chunks audio
|
||||
let chunk_frames = 64;
|
||||
let mut order = 0u64;
|
||||
let mut total_frames = 0u64;
|
||||
|
||||
for chunk_start in (0..frames).step_by(chunk_frames) {
|
||||
let chunk_len = (frames - chunk_start).min(chunk_frames);
|
||||
let mut stereo = Vec::with_capacity(chunk_len);
|
||||
|
||||
for i in 0..chunk_len {
|
||||
let frame_idx = chunk_start + i;
|
||||
let sample = ((frame_idx % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5;
|
||||
let sample_i16 = (sample * 32767.0) as i16;
|
||||
stereo.push([sample_i16, sample_i16]);
|
||||
}
|
||||
|
||||
let timestamp = total_frames as f64 / sample_rate as f64;
|
||||
let chunk_data = crate::AudioChunkData::new(stereo, sample_rate, 0.0);
|
||||
let chunk = crate::AudioChunk::I16(chunk_data);
|
||||
let segment = crate::AudioSegment {
|
||||
order,
|
||||
timestamp_sec: timestamp,
|
||||
segment: crate::_AudioSegment::Chunk(std::sync::Arc::new(chunk)),
|
||||
};
|
||||
|
||||
tx.send(std::sync::Arc::new(segment)).await.unwrap();
|
||||
total_frames += chunk_len as u64;
|
||||
order += 1;
|
||||
}
|
||||
|
||||
// EndOfStream
|
||||
let final_timestamp = total_frames as f64 / sample_rate as f64;
|
||||
tx.send(crate::AudioSegment::new_end_of_stream(
|
||||
order,
|
||||
final_timestamp,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
drop(tx);
|
||||
});
|
||||
|
||||
sink_handle.await.unwrap();
|
||||
|
||||
// Vérifier que le fichier a été créé et contient les métadonnées
|
||||
assert!(output_path.exists(), "Output file should exist");
|
||||
|
||||
// Lire les métadonnées du fichier FLAC généré
|
||||
let file_metadata = AudioFileMetadata::from_file(&output_path).unwrap();
|
||||
|
||||
// Vérifier que les métadonnées ont été correctement écrites
|
||||
assert_eq!(file_metadata.title, Some("Test Track Title".to_string()));
|
||||
assert_eq!(file_metadata.artist, Some("Test Artist".to_string()));
|
||||
assert_eq!(file_metadata.album, Some("Test Album".to_string()));
|
||||
assert_eq!(file_metadata.year, Some(2024));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_flac_file_sink_writes_audio() {
|
||||
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
|
||||
use std::io::Cursor;
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let input_path = temp_dir.path().join("input.flac");
|
||||
let output_path = temp_dir.path().join("output.flac");
|
||||
|
||||
// Créer un petit fichier FLAC de test (comme dans file_source test)
|
||||
let sample_rate = 44_100;
|
||||
let frames = 512;
|
||||
let mut pcm = Vec::with_capacity(frames * 4);
|
||||
for i in 0..frames {
|
||||
let sample = ((i % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5;
|
||||
let sample_i16 = (sample * 32767.0) as i16;
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
}
|
||||
|
||||
let format = PcmFormat {
|
||||
sample_rate,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
};
|
||||
|
||||
let mut flac_stream =
|
||||
encode_flac_stream(Cursor::new(pcm.clone()), format, EncoderOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut input_file = File::create(&input_path).await.unwrap();
|
||||
tokio::io::copy(&mut flac_stream, &mut input_file)
|
||||
.await
|
||||
.unwrap();
|
||||
input_file.flush().await.unwrap();
|
||||
flac_stream.wait().await.unwrap();
|
||||
|
||||
// Maintenant utiliser FlacFileSink pour réécrire le fichier
|
||||
let (sink, tx) = FlacFileSink::with_channel_size(&output_path, 16);
|
||||
let sink_handle = tokio::spawn(async move { sink.run().await.unwrap() });
|
||||
|
||||
// Lire le fichier input et envoyer les segments au sink
|
||||
tokio::spawn(async move {
|
||||
let source_file = File::open(&input_path).await.unwrap();
|
||||
let mut decode_stream = pmoflac::decode_audio_stream(source_file).await.unwrap();
|
||||
let info = decode_stream.info().clone();
|
||||
|
||||
// TopZeroSync
|
||||
tx.send(crate::AudioSegment::new_top_zero_sync())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Lire et envoyer les chunks
|
||||
let mut buffer = vec![0u8; info.bytes_per_sample() * info.channels as usize * 256];
|
||||
let mut total_frames = 0u64;
|
||||
let mut order = 0u64;
|
||||
|
||||
loop {
|
||||
let read = decode_stream.read(&mut buffer).await.unwrap();
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let chunk_frames = read / (info.bytes_per_sample() * info.channels as usize);
|
||||
let timestamp = total_frames as f64 / info.sample_rate as f64;
|
||||
|
||||
// Créer un segment I16
|
||||
let mut stereo = Vec::with_capacity(chunk_frames);
|
||||
for i in 0..chunk_frames {
|
||||
let offset = i * info.bytes_per_sample() * info.channels as usize;
|
||||
let l = i16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
|
||||
let r = i16::from_le_bytes([buffer[offset + 2], buffer[offset + 3]]);
|
||||
stereo.push([l, r]);
|
||||
}
|
||||
|
||||
let chunk_data = crate::AudioChunkData::new(stereo, info.sample_rate, 0.0);
|
||||
let chunk = crate::AudioChunk::I16(chunk_data);
|
||||
let segment = crate::AudioSegment {
|
||||
order,
|
||||
timestamp_sec: timestamp,
|
||||
segment: crate::_AudioSegment::Chunk(std::sync::Arc::new(chunk)),
|
||||
};
|
||||
|
||||
tx.send(std::sync::Arc::new(segment)).await.unwrap();
|
||||
total_frames += chunk_frames as u64;
|
||||
order += 1;
|
||||
}
|
||||
|
||||
// EndOfStream
|
||||
let final_timestamp = total_frames as f64 / info.sample_rate as f64;
|
||||
tx.send(crate::AudioSegment::new_end_of_stream(
|
||||
order,
|
||||
final_timestamp,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
drop(tx);
|
||||
decode_stream.wait().await.unwrap();
|
||||
});
|
||||
|
||||
let stats = sink_handle.await.unwrap();
|
||||
assert_eq!(stats.tracks.len(), 1);
|
||||
assert!(stats.tracks[0].chunks_received > 0);
|
||||
assert_eq!(stats.tracks[0].total_samples, frames as u64);
|
||||
|
||||
// Vérifier que le fichier de sortie est valide
|
||||
let file = File::open(&output_path).await.unwrap();
|
||||
let mut stream = decode_flac_stream(file).await.unwrap();
|
||||
let info = stream.info().clone();
|
||||
assert_eq!(info.channels, 2);
|
||||
assert_eq!(info.sample_rate, sample_rate);
|
||||
assert_eq!(info.bits_per_sample, 16);
|
||||
|
||||
let mut decoded = Vec::new();
|
||||
stream.read_to_end(&mut decoded).await.unwrap();
|
||||
stream.wait().await.unwrap();
|
||||
assert!(decoded.len() > 0);
|
||||
}
|
||||
}
|
||||
827
pmoaudio/src/nodes/http_source.rs
Normal file
827
pmoaudio/src/nodes/http_source.rs
Normal file
@@ -0,0 +1,827 @@
|
||||
use crate::{
|
||||
nodes::{AudioError, MultiSubscriberNode, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
||||
type_constraints::TypeRequirement,
|
||||
AudioChunk, AudioChunkData, AudioSegment, I24,
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
use pmoflac::{decode_audio_stream, StreamInfo};
|
||||
use pmometadata::{MemoryTrackMetadata, TrackMetadata};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::io::StreamReader;
|
||||
|
||||
/// HttpSource - Récupère un fichier audio via HTTP et publie des `AudioSegment`
|
||||
///
|
||||
/// Cette source télécharge un fichier audio depuis une URL HTTP/HTTPS,
|
||||
/// utilise `pmoflac` pour le décoder (FLAC/MP3/OGG/WAV/AIFF) puis transforme
|
||||
/// les échantillons PCM en `AudioSegment` stéréo avec le type approprié.
|
||||
///
|
||||
/// Le node émet trois types de syncmarkers :
|
||||
/// - `TopZeroSync` au début du flux
|
||||
/// - `TrackBoundary` avec les métadonnées extraites des headers HTTP
|
||||
/// - `EndOfStream` à la fin du flux
|
||||
///
|
||||
/// # Métadonnées HTTP
|
||||
///
|
||||
/// Les métadonnées suivantes sont extraites des headers HTTP lorsqu'elles sont disponibles:
|
||||
/// - `icy-name`: nom du stream (Icecast/Shoutcast) → utilisé comme titre
|
||||
/// - `icy-url`: URL du stream source
|
||||
/// - `content-type`: type MIME du contenu (ex: audio/flac, audio/mpeg)
|
||||
///
|
||||
/// Si aucun header `icy-name` n'est présent, le nom du fichier est extrait de l'URL
|
||||
/// et utilisé comme titre.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ## Lecture d'un fichier FLAC distant
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::HttpSource;
|
||||
/// use tokio::sync::mpsc;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let mut source = HttpSource::new("http://example.com/audio.flac");
|
||||
/// let (tx, mut rx) = mpsc::channel(16);
|
||||
/// source.add_subscriber(tx);
|
||||
///
|
||||
/// // Lancer la lecture dans une tâche séparée
|
||||
/// tokio::spawn(async move {
|
||||
/// source.run().await.unwrap();
|
||||
/// });
|
||||
///
|
||||
/// // Recevoir et traiter les segments audio
|
||||
/// while let Some(segment) = rx.recv().await {
|
||||
/// if segment.is_audio_chunk() {
|
||||
/// println!("Chunk reçu à {}s", segment.timestamp_sec);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Stream Icecast/Shoutcast
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::HttpSource;
|
||||
/// use tokio::sync::mpsc;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// // Les métadonnées icy-name seront extraites automatiquement
|
||||
/// let mut source = HttpSource::new("http://stream.example.com:8000/stream");
|
||||
/// let (tx, rx) = mpsc::channel(32);
|
||||
/// source.add_subscriber(tx);
|
||||
///
|
||||
/// tokio::spawn(async move {
|
||||
/// source.run().await.unwrap();
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Gestion des erreurs
|
||||
///
|
||||
/// La méthode `run()` peut retourner les erreurs suivantes:
|
||||
/// - `AudioError::ProcessingError`: échec de connexion HTTP, status code non-200,
|
||||
/// erreur de décodage audio, ou format non supporté
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// - Le téléchargement et le décodage sont effectués en streaming
|
||||
/// - Pas de buffering complet du fichier en mémoire
|
||||
/// - La taille des chunks audio est calculée automatiquement pour ~50ms de latence
|
||||
/// - Compatible avec les streams infinis (radios web, etc.)
|
||||
pub struct HttpSource {
|
||||
url: String,
|
||||
chunk_frames: usize,
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl HttpSource {
|
||||
/// Crée une nouvelle source HTTP avec calcul automatique de la taille des chunks.
|
||||
///
|
||||
/// La taille des chunks sera calculée automatiquement pour obtenir environ 50ms
|
||||
/// de latence par chunk, en fonction du sample rate du fichier distant.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL HTTP ou HTTPS du fichier audio à télécharger
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::HttpSource;
|
||||
///
|
||||
/// let source = HttpSource::new("http://example.com/music.flac");
|
||||
/// ```
|
||||
pub fn new<S: Into<String>>(url: S) -> Self {
|
||||
Self::with_chunk_size(url, 0)
|
||||
}
|
||||
|
||||
/// Crée une nouvelle source HTTP avec une taille de chunk spécifique.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL HTTP ou HTTPS du fichier audio à télécharger
|
||||
/// * `chunk_frames` - nombre d'échantillons par canal par chunk (0 = auto-calcul)
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::HttpSource;
|
||||
///
|
||||
/// // Utiliser des chunks de 2048 frames
|
||||
/// let source = HttpSource::with_chunk_size("http://example.com/music.mp3", 2048);
|
||||
/// ```
|
||||
pub fn with_chunk_size<S: Into<String>>(url: S, chunk_frames: usize) -> Self {
|
||||
Self {
|
||||
url: url.into(),
|
||||
chunk_frames,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ajoute un abonné qui recevra les segments audio.
|
||||
///
|
||||
/// Chaque abonné recevra une copie (via `Arc`) de tous les segments audio
|
||||
/// produits par cette source, y compris les syncmarkers.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tx` - Channel sender pour recevoir les `AudioSegment`
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::HttpSource;
|
||||
/// use tokio::sync::mpsc;
|
||||
///
|
||||
/// let mut source = HttpSource::new("http://example.com/audio.flac");
|
||||
/// let (tx, rx) = mpsc::channel(16);
|
||||
/// source.add_subscriber(tx);
|
||||
/// ```
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Lance le téléchargement et la lecture du flux audio.
|
||||
///
|
||||
/// Cette méthode consomme `self` et exécute le pipeline complet:
|
||||
/// 1. Effectue la requête HTTP GET vers l'URL spécifiée
|
||||
/// 2. Vérifie le status HTTP (doit être 2xx)
|
||||
/// 3. Extrait les métadonnées des headers HTTP
|
||||
/// 4. Décode le flux audio en streaming
|
||||
/// 5. Émet les syncmarkers et chunks audio vers les abonnés
|
||||
///
|
||||
/// La méthode se termine quand le flux est complètement lu ou en cas d'erreur.
|
||||
///
|
||||
/// # Erreurs
|
||||
///
|
||||
/// Retourne `AudioError::ProcessingError` si:
|
||||
/// - La requête HTTP échoue (réseau, DNS, etc.)
|
||||
/// - Le serveur retourne un status code non-2xx
|
||||
/// - Le format audio n'est pas supporté
|
||||
/// - Le décodage échoue
|
||||
/// - Le fichier a un nombre de canaux non supporté (doit être 1 ou 2)
|
||||
/// - La profondeur de bit n'est pas supportée (doit être 8, 16, 24 ou 32 bits)
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::HttpSource;
|
||||
/// use tokio::sync::mpsc;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut source = HttpSource::new("http://example.com/audio.flac");
|
||||
/// let (tx, mut rx) = mpsc::channel(16);
|
||||
/// source.add_subscriber(tx);
|
||||
///
|
||||
/// let handle = tokio::spawn(async move {
|
||||
/// source.run().await
|
||||
/// });
|
||||
///
|
||||
/// // Traiter les segments
|
||||
/// while let Some(segment) = rx.recv().await {
|
||||
/// println!("Segment reçu: order={}", segment.order);
|
||||
/// }
|
||||
///
|
||||
/// handle.await??;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn run(self) -> Result<(), AudioError> {
|
||||
// Effectuer la requête HTTP
|
||||
let response = reqwest::get(&self.url)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("HTTP request failed for {}: {}", self.url, e))
|
||||
})?;
|
||||
|
||||
// Vérifier le status
|
||||
if !response.status().is_success() {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"HTTP request returned status {}: {}",
|
||||
response.status(),
|
||||
self.url
|
||||
)));
|
||||
}
|
||||
|
||||
// Extraire les métadonnées depuis les headers HTTP
|
||||
let metadata = extract_metadata_from_headers(&response, &self.url).await;
|
||||
|
||||
// Convertir le stream de bytes en AsyncRead
|
||||
let bytes_stream = response.bytes_stream();
|
||||
let stream_reader = StreamReader::new(bytes_stream.map(|result| {
|
||||
result.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
}));
|
||||
|
||||
// Décoder le flux audio
|
||||
let mut stream = decode_audio_stream(stream_reader)
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Decode error: {}", e)))?;
|
||||
let stream_info = stream.info().clone();
|
||||
|
||||
validate_stream(&stream_info)?;
|
||||
|
||||
// Calculer la taille des chunks si non spécifiée (0 = auto)
|
||||
let chunk_frames = if self.chunk_frames == 0 {
|
||||
let frames =
|
||||
(stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize;
|
||||
frames.next_power_of_two().max(256)
|
||||
} else {
|
||||
self.chunk_frames.max(1)
|
||||
};
|
||||
|
||||
// Émettre TopZeroSync
|
||||
let top_zero = AudioSegment::new_top_zero_sync();
|
||||
self.subscribers.push(top_zero).await?;
|
||||
|
||||
// Émettre TrackBoundary avec les métadonnées HTTP
|
||||
let track_boundary = AudioSegment::new_track_boundary(0, 0.0, Arc::new(metadata));
|
||||
self.subscribers.push(track_boundary).await?;
|
||||
|
||||
// Préparer la lecture des chunks audio
|
||||
let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize;
|
||||
let chunk_byte_len = chunk_frames * frame_bytes;
|
||||
let mut pending = Vec::new();
|
||||
let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames)];
|
||||
let mut chunk_index = 0u64;
|
||||
let mut total_frames = 0u64;
|
||||
|
||||
// Lire et émettre les chunks audio
|
||||
loop {
|
||||
// Remplir le buffer
|
||||
if pending.len() < chunk_byte_len {
|
||||
use tokio::io::AsyncReadExt;
|
||||
let read = stream.read(&mut read_buf).await.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("I/O error while decoding: {}", e))
|
||||
})?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
pending.extend_from_slice(&read_buf[..read]);
|
||||
}
|
||||
|
||||
if pending.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Extraire un chunk
|
||||
let frames_in_pending = pending.len() / frame_bytes;
|
||||
let frames_to_emit = frames_in_pending.min(chunk_frames);
|
||||
let take_bytes = frames_to_emit * frame_bytes;
|
||||
let chunk_bytes = pending.drain(..take_bytes).collect::<Vec<u8>>();
|
||||
|
||||
// Calculer le timestamp
|
||||
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
|
||||
|
||||
// Créer le segment audio
|
||||
let segment = bytes_to_segment(
|
||||
&chunk_bytes,
|
||||
&stream_info,
|
||||
frames_to_emit,
|
||||
chunk_index,
|
||||
timestamp_sec,
|
||||
)?;
|
||||
self.subscribers.push(segment).await?;
|
||||
|
||||
chunk_index += 1;
|
||||
total_frames += frames_to_emit as u64;
|
||||
}
|
||||
|
||||
// Traiter le reste éventuel
|
||||
if !pending.is_empty() {
|
||||
let frames = pending.len() / frame_bytes;
|
||||
if frames > 0 {
|
||||
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
|
||||
let segment =
|
||||
bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?;
|
||||
self.subscribers.push(segment).await?;
|
||||
total_frames += frames as u64;
|
||||
chunk_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Émettre EndOfStream
|
||||
let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64;
|
||||
let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp);
|
||||
self.subscribers.push(eos).await?;
|
||||
|
||||
// Attendre la fin du décodage
|
||||
stream
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrait les métadonnées disponibles depuis les headers HTTP
|
||||
async fn extract_metadata_from_headers(
|
||||
response: &reqwest::Response,
|
||||
url: &str,
|
||||
) -> MemoryTrackMetadata {
|
||||
let mut metadata = MemoryTrackMetadata::new();
|
||||
let headers = response.headers();
|
||||
|
||||
// Icecast/Shoutcast stream name
|
||||
if let Some(name) = headers.get("icy-name").and_then(|v| v.to_str().ok()) {
|
||||
let _ = metadata.set_title(Some(name.to_string())).await;
|
||||
}
|
||||
|
||||
// Icecast/Shoutcast stream URL (peut être utilisé comme source)
|
||||
if let Some(stream_url) = headers.get("icy-url").and_then(|v| v.to_str().ok()) {
|
||||
// On pourrait stocker ça dans un champ custom si nécessaire
|
||||
eprintln!("Stream URL: {}", stream_url);
|
||||
}
|
||||
|
||||
// Content-Type pour déterminer le format
|
||||
if let Some(content_type) = headers.get("content-type").and_then(|v| v.to_str().ok()) {
|
||||
eprintln!("Content-Type: {}", content_type);
|
||||
// On pourrait utiliser ça pour valider le format attendu
|
||||
}
|
||||
|
||||
// Si aucune métadonnée spécifique n'est trouvée, utiliser l'URL comme titre
|
||||
if metadata.get_title().await.ok().flatten().is_none() {
|
||||
// Extraire le nom du fichier depuis l'URL
|
||||
if let Some(filename) = url.rsplit('/').next() {
|
||||
if !filename.is_empty() {
|
||||
let _ = metadata.set_title(Some(filename.to_string())).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadata
|
||||
}
|
||||
|
||||
fn validate_stream(info: &StreamInfo) -> Result<(), AudioError> {
|
||||
if !(1..=2).contains(&info.channels) {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported channel count: {}",
|
||||
info.channels
|
||||
)));
|
||||
}
|
||||
match info.bits_per_sample {
|
||||
8 | 16 | 24 | 32 => Ok(()),
|
||||
other => Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported bit depth: {}",
|
||||
other
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit des bytes PCM en AudioSegment avec le type approprié
|
||||
fn bytes_to_segment(
|
||||
chunk_bytes: &[u8],
|
||||
info: &StreamInfo,
|
||||
frames: usize,
|
||||
order: u64,
|
||||
timestamp_sec: f64,
|
||||
) -> Result<Arc<AudioSegment>, AudioError> {
|
||||
let bytes_per_sample = info.bytes_per_sample();
|
||||
let channels = info.channels as usize;
|
||||
let frame_bytes = bytes_per_sample * channels;
|
||||
|
||||
// Créer le chunk du bon type selon la profondeur de bit
|
||||
let chunk = match info.bits_per_sample {
|
||||
16 => {
|
||||
// Type I16
|
||||
let mut stereo = Vec::with_capacity(frames);
|
||||
for frame_idx in 0..frames {
|
||||
let base = frame_idx * frame_bytes;
|
||||
let l = i16::from_le_bytes(
|
||||
chunk_bytes[base..base + bytes_per_sample]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
let r = if channels == 1 {
|
||||
l
|
||||
} else {
|
||||
i16::from_le_bytes(
|
||||
chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
};
|
||||
stereo.push([l, r]);
|
||||
}
|
||||
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
|
||||
AudioChunk::I16(chunk_data)
|
||||
}
|
||||
24 => {
|
||||
// Type I24
|
||||
let mut stereo = Vec::with_capacity(frames);
|
||||
for frame_idx in 0..frames {
|
||||
let base = frame_idx * frame_bytes;
|
||||
let l_i32 = {
|
||||
let mut buf = [0u8; 4];
|
||||
buf[..3].copy_from_slice(&chunk_bytes[base..base + 3]);
|
||||
// Sign extend
|
||||
if chunk_bytes[base + 2] & 0x80 != 0 {
|
||||
buf[3] = 0xFF;
|
||||
}
|
||||
i32::from_le_bytes(buf)
|
||||
};
|
||||
let l = I24::new(l_i32).ok_or_else(|| {
|
||||
AudioError::ProcessingError(format!("Invalid I24 value: {}", l_i32))
|
||||
})?;
|
||||
|
||||
let r = if channels == 1 {
|
||||
l
|
||||
} else {
|
||||
let r_i32 = {
|
||||
let mut buf = [0u8; 4];
|
||||
buf[..3].copy_from_slice(
|
||||
&chunk_bytes[base + bytes_per_sample..base + bytes_per_sample + 3],
|
||||
);
|
||||
// Sign extend
|
||||
if chunk_bytes[base + bytes_per_sample + 2] & 0x80 != 0 {
|
||||
buf[3] = 0xFF;
|
||||
}
|
||||
i32::from_le_bytes(buf)
|
||||
};
|
||||
I24::new(r_i32).ok_or_else(|| {
|
||||
AudioError::ProcessingError(format!("Invalid I24 value: {}", r_i32))
|
||||
})?
|
||||
};
|
||||
stereo.push([l, r]);
|
||||
}
|
||||
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
|
||||
AudioChunk::I24(chunk_data)
|
||||
}
|
||||
32 => {
|
||||
// Type I32
|
||||
let mut stereo = Vec::with_capacity(frames);
|
||||
for frame_idx in 0..frames {
|
||||
let base = frame_idx * frame_bytes;
|
||||
let l = i32::from_le_bytes(
|
||||
chunk_bytes[base..base + bytes_per_sample]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
let r = if channels == 1 {
|
||||
l
|
||||
} else {
|
||||
i32::from_le_bytes(
|
||||
chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
};
|
||||
stereo.push([l, r]);
|
||||
}
|
||||
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
|
||||
AudioChunk::I32(chunk_data)
|
||||
}
|
||||
other => {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported bit depth: {}",
|
||||
other
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// Créer le segment audio
|
||||
Ok(Arc::new(AudioSegment {
|
||||
order,
|
||||
timestamp_sec,
|
||||
segment: crate::_AudioSegment::Chunk(Arc::new(chunk)),
|
||||
}))
|
||||
}
|
||||
|
||||
impl TypedAudioNode for HttpSource {
|
||||
fn input_type(&self) -> Option<TypeRequirement> {
|
||||
// HttpSource est une source, elle ne consomme pas d'audio
|
||||
None
|
||||
}
|
||||
|
||||
fn output_type(&self) -> Option<TypeRequirement> {
|
||||
// HttpSource peut produire n'importe quel type entier (I16, I24, I32)
|
||||
// selon la profondeur de bit du fichier source
|
||||
Some(TypeRequirement::any_integer())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
|
||||
use std::io::Cursor;
|
||||
use tokio::sync::mpsc;
|
||||
use wiremock::{
|
||||
matchers::{method, path},
|
||||
Mock, MockServer, ResponseTemplate,
|
||||
};
|
||||
|
||||
/// Test de création basique de HttpSource
|
||||
#[test]
|
||||
fn test_http_source_creation() {
|
||||
let source = HttpSource::new("http://example.com/audio.flac");
|
||||
assert_eq!(source.url, "http://example.com/audio.flac");
|
||||
assert_eq!(source.chunk_frames, 0);
|
||||
}
|
||||
|
||||
/// Test de création avec taille de chunk personnalisée
|
||||
#[test]
|
||||
fn test_http_source_with_chunk_size() {
|
||||
let source = HttpSource::with_chunk_size("http://example.com/audio.mp3", 1024);
|
||||
assert_eq!(source.url, "http://example.com/audio.mp3");
|
||||
assert_eq!(source.chunk_frames, 1024);
|
||||
}
|
||||
|
||||
/// Test de téléchargement et décodage d'un fichier FLAC via HTTP
|
||||
#[tokio::test]
|
||||
async fn test_http_source_downloads_and_decodes_flac() {
|
||||
// Créer un serveur HTTP mock
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
// Générer un petit fichier FLAC de test
|
||||
let sample_rate = 48_000;
|
||||
let frames = 256;
|
||||
let mut pcm = Vec::with_capacity(frames * 4);
|
||||
for i in 0..frames {
|
||||
let sample = ((i % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5;
|
||||
let sample_i16 = (sample * 32767.0) as i16;
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
}
|
||||
|
||||
let format = PcmFormat {
|
||||
sample_rate,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
};
|
||||
|
||||
let mut flac_stream =
|
||||
encode_flac_stream(Cursor::new(pcm.clone()), format, EncoderOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Lire le FLAC encodé dans un buffer
|
||||
let mut flac_data = Vec::new();
|
||||
tokio::io::copy(&mut flac_stream, &mut flac_data)
|
||||
.await
|
||||
.unwrap();
|
||||
flac_stream.wait().await.unwrap();
|
||||
|
||||
// Configurer le mock pour servir le fichier FLAC
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/test.flac"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_bytes(flac_data)
|
||||
.insert_header("content-type", "audio/flac"),
|
||||
)
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
// Créer la source HTTP pointant vers le mock
|
||||
let url = format!("{}/test.flac", mock_server.uri());
|
||||
let mut source = HttpSource::with_chunk_size(&url, 64);
|
||||
let (tx, mut rx) = mpsc::channel(16);
|
||||
source.add_subscriber(tx);
|
||||
|
||||
// Lancer le téléchargement et le décodage
|
||||
tokio::spawn(async move {
|
||||
source.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Vérifier les segments reçus
|
||||
let mut received_frames = 0usize;
|
||||
let mut seen_top_zero = false;
|
||||
let mut seen_track_boundary = false;
|
||||
let mut seen_eos = false;
|
||||
|
||||
while let Some(segment) = rx.recv().await {
|
||||
if segment.is_audio_chunk() {
|
||||
if let Some(chunk) = segment.as_chunk() {
|
||||
received_frames += chunk.len();
|
||||
assert_eq!(chunk.sample_rate(), sample_rate);
|
||||
}
|
||||
} else if let Some(marker) = segment.as_sync_marker() {
|
||||
match **marker {
|
||||
crate::SyncMarker::TopZeroSync => seen_top_zero = true,
|
||||
crate::SyncMarker::TrackBoundary { .. } => seen_track_boundary = true,
|
||||
crate::SyncMarker::EndOfStream => seen_eos = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifications
|
||||
assert_eq!(received_frames, frames, "Tous les frames doivent être reçus");
|
||||
assert!(seen_top_zero, "TopZeroSync doit être émis");
|
||||
assert!(seen_track_boundary, "TrackBoundary doit être émis");
|
||||
assert!(seen_eos, "EndOfStream doit être émis");
|
||||
}
|
||||
|
||||
/// Test de l'extraction des métadonnées depuis les headers HTTP
|
||||
#[tokio::test]
|
||||
async fn test_http_source_extracts_icy_metadata() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
// Créer un fichier FLAC minimal
|
||||
let sample_rate = 48_000;
|
||||
let frames = 128;
|
||||
let mut pcm = Vec::with_capacity(frames * 4);
|
||||
for i in 0..frames {
|
||||
let sample_i16 = ((i % 100) as i16) * 100;
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
}
|
||||
|
||||
let format = PcmFormat {
|
||||
sample_rate,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
};
|
||||
|
||||
let mut flac_stream = encode_flac_stream(Cursor::new(pcm), format, EncoderOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut flac_data = Vec::new();
|
||||
tokio::io::copy(&mut flac_stream, &mut flac_data)
|
||||
.await
|
||||
.unwrap();
|
||||
flac_stream.wait().await.unwrap();
|
||||
|
||||
// Configurer le mock avec headers Icecast
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/stream"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_bytes(flac_data)
|
||||
.insert_header("content-type", "audio/flac")
|
||||
.insert_header("icy-name", "Test Radio Stream")
|
||||
.insert_header("icy-url", "http://example.com/radio"),
|
||||
)
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let url = format!("{}/stream", mock_server.uri());
|
||||
let mut source = HttpSource::new(&url);
|
||||
let (tx, mut rx) = mpsc::channel(16);
|
||||
source.add_subscriber(tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
source.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Chercher le TrackBoundary pour vérifier les métadonnées
|
||||
let mut found_metadata = false;
|
||||
while let Some(segment) = rx.recv().await {
|
||||
if let Some(marker) = segment.as_sync_marker() {
|
||||
if let crate::SyncMarker::TrackBoundary { metadata, .. } = &**marker {
|
||||
// Vérifier que le titre extrait est "Test Radio Stream"
|
||||
if let Some(title) = metadata.get_title().await.ok().flatten() {
|
||||
assert_eq!(title, "Test Radio Stream");
|
||||
found_metadata = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(found_metadata, "Les métadonnées ICY doivent être extraites");
|
||||
}
|
||||
|
||||
/// Test du comportement en cas d'erreur HTTP 404
|
||||
#[tokio::test]
|
||||
async fn test_http_source_handles_404_error() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/notfound.flac"))
|
||||
.respond_with(ResponseTemplate::new(404))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let url = format!("{}/notfound.flac", mock_server.uri());
|
||||
let mut source = HttpSource::new(&url);
|
||||
let (tx, _rx) = mpsc::channel(16);
|
||||
source.add_subscriber(tx);
|
||||
|
||||
let result = source.run().await;
|
||||
assert!(result.is_err(), "Doit retourner une erreur pour HTTP 404");
|
||||
|
||||
if let Err(AudioError::ProcessingError(msg)) = result {
|
||||
assert!(msg.contains("404"), "Le message d'erreur doit mentionner le code 404");
|
||||
} else {
|
||||
panic!("Le type d'erreur doit être ProcessingError");
|
||||
}
|
||||
}
|
||||
|
||||
/// Test du comportement avec un format audio invalide
|
||||
#[tokio::test]
|
||||
async fn test_http_source_handles_invalid_audio_format() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
// Envoyer des données invalides (pas un fichier audio)
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/invalid.flac"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_bytes(b"This is not a valid audio file")
|
||||
.insert_header("content-type", "audio/flac"),
|
||||
)
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let url = format!("{}/invalid.flac", mock_server.uri());
|
||||
let mut source = HttpSource::new(&url);
|
||||
let (tx, _rx) = mpsc::channel(16);
|
||||
source.add_subscriber(tx);
|
||||
|
||||
let result = source.run().await;
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Doit retourner une erreur pour un format invalide"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test de l'extraction du nom de fichier depuis l'URL quand pas de header icy-name
|
||||
#[tokio::test]
|
||||
async fn test_http_source_uses_filename_as_title() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
let sample_rate = 48_000;
|
||||
let frames = 128;
|
||||
let mut pcm = Vec::with_capacity(frames * 4);
|
||||
for i in 0..frames {
|
||||
let sample_i16 = (i % 100) as i16;
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
}
|
||||
|
||||
let format = PcmFormat {
|
||||
sample_rate,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
};
|
||||
|
||||
let mut flac_stream = encode_flac_stream(Cursor::new(pcm), format, EncoderOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut flac_data = Vec::new();
|
||||
tokio::io::copy(&mut flac_stream, &mut flac_data)
|
||||
.await
|
||||
.unwrap();
|
||||
flac_stream.wait().await.unwrap();
|
||||
|
||||
// Sans header icy-name
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/my-song.flac"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_bytes(flac_data)
|
||||
.insert_header("content-type", "audio/flac"),
|
||||
)
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let url = format!("{}/my-song.flac", mock_server.uri());
|
||||
let mut source = HttpSource::new(&url);
|
||||
let (tx, mut rx) = mpsc::channel(16);
|
||||
source.add_subscriber(tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
source.run().await.unwrap();
|
||||
});
|
||||
|
||||
let mut found_title = false;
|
||||
while let Some(segment) = rx.recv().await {
|
||||
if let Some(marker) = segment.as_sync_marker() {
|
||||
if let crate::SyncMarker::TrackBoundary { metadata, .. } = &**marker {
|
||||
if let Some(title) = metadata.get_title().await.ok().flatten() {
|
||||
assert_eq!(title, "my-song.flac");
|
||||
found_title = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(found_title, "Le nom du fichier doit être utilisé comme titre");
|
||||
}
|
||||
}
|
||||
224
pmoaudio/src/nodes/mod.rs
Normal file
224
pmoaudio/src/nodes/mod.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
//! Nodes du pipeline audio
|
||||
//!
|
||||
//! Ce module contient tous les types de nodes disponibles pour construire
|
||||
//! un pipeline audio, ainsi que les traits et structures de support.
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::type_constraints::{TypeMismatch, TypeRequirement};
|
||||
use crate::AudioSegment;
|
||||
|
||||
/// Taille par défaut du buffer de channel MPSC pour les nodes
|
||||
/// Cette valeur détermine combien de segments audio peuvent être mis en attente
|
||||
/// avant que le producteur soit bloqué (backpressure).
|
||||
pub const DEFAULT_CHANNEL_SIZE: usize = 16;
|
||||
|
||||
/// Durée par défaut des chunks audio en millisecondes
|
||||
/// Cette valeur détermine la latence de traitement et le compromis efficacité/réactivité.
|
||||
/// 50ms offre un bon équilibre pour la plupart des applications de lecture audio.
|
||||
pub const DEFAULT_CHUNK_DURATION_MS: f64 = 50.0;
|
||||
|
||||
// Modules actifs
|
||||
pub mod converter_nodes;
|
||||
pub mod file_source;
|
||||
pub mod flac_file_sink;
|
||||
pub mod http_source;
|
||||
|
||||
// Modules temporairement désactivés
|
||||
/*
|
||||
pub mod buffer_node;
|
||||
pub mod chromecast_sink;
|
||||
pub mod decoder_node;
|
||||
pub mod disk_sink;
|
||||
pub mod dsp_node;
|
||||
pub mod mpd_sink;
|
||||
pub mod sink_node;
|
||||
pub mod source_node;
|
||||
pub mod timer_node;
|
||||
pub mod volume_node;
|
||||
*/
|
||||
|
||||
/// Trait de base pour tous les nodes audio
|
||||
///
|
||||
/// Tous les nodes du pipeline implémentent ce trait pour permettre
|
||||
/// une interface uniforme de traitement des chunks audio.
|
||||
#[async_trait::async_trait]
|
||||
pub trait AudioNode: Send + Sync {
|
||||
/// Push un chunk vers ce node
|
||||
///
|
||||
/// # Erreurs
|
||||
///
|
||||
/// Retourne `AudioError::SendError` si l'envoi échoue
|
||||
async fn push(&mut self, chunk: Arc<AudioSegment>) -> Result<(), AudioError>;
|
||||
|
||||
/// Ferme le node proprement
|
||||
async fn close(&mut self);
|
||||
}
|
||||
|
||||
/// Trait pour les nodes qui déclarent leurs types acceptés/produits
|
||||
///
|
||||
/// Ce trait permet de vérifier la compatibilité des types entre nodes
|
||||
/// avant de les connecter dans un pipeline.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::{FileSource, FlacFileSink, TypedAudioNode};
|
||||
/// use pmoaudio::type_constraints::check_compatibility;
|
||||
///
|
||||
/// // Vérifier la compatibilité avant de connecter
|
||||
/// let source = FileSource::new("input.flac");
|
||||
/// let (sink, tx) = FlacFileSink::new("output.flac");
|
||||
///
|
||||
/// let source_output = source.output_type();
|
||||
/// let sink_input = sink.input_type();
|
||||
///
|
||||
/// match check_compatibility(&source_output, &sink_input) {
|
||||
/// Ok(()) => println!("Types compatibles!"),
|
||||
/// Err(e) => eprintln!("Types incompatibles: {}", e),
|
||||
/// }
|
||||
/// ```
|
||||
pub trait TypedAudioNode {
|
||||
/// Retourne les types que ce node peut accepter en entrée
|
||||
///
|
||||
/// Pour les sources (qui ne consomment rien), retourne `None`.
|
||||
fn input_type(&self) -> Option<TypeRequirement>;
|
||||
|
||||
/// Retourne les types que ce node peut produire en sortie
|
||||
///
|
||||
/// Pour les sinks (qui ne produisent rien), retourne `None`.
|
||||
fn output_type(&self) -> Option<TypeRequirement>;
|
||||
|
||||
/// Vérifie si ce node peut accepter les chunks d'un producer donné
|
||||
///
|
||||
/// # Erreurs
|
||||
///
|
||||
/// Retourne `AudioError::TypeMismatch` si les types sont incompatibles
|
||||
fn can_accept_from(&self, producer: &dyn TypedAudioNode) -> Result<(), AudioError> {
|
||||
match (producer.output_type(), self.input_type()) {
|
||||
(Some(prod), Some(cons)) => crate::type_constraints::check_compatibility(&prod, &cons)
|
||||
.map_err(|e| AudioError::TypeMismatch(e)),
|
||||
(None, Some(_)) => Err(AudioError::TypeMismatch(TypeMismatch {
|
||||
producer: TypeRequirement::any(), // Placeholder
|
||||
consumer: self.input_type().unwrap(),
|
||||
incompatible_type: None,
|
||||
})),
|
||||
_ => Ok(()), // Si pas de contrainte, toujours compatible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Node avec un seul abonné (pas de clone inutile)
|
||||
///
|
||||
/// Optimisé pour les cas où un node n'a qu'un seul destinataire.
|
||||
/// Le Arc du chunk est simplement transféré sans clonage supplémentaire.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoaudio::SingleSubscriberNode;
|
||||
/// use tokio::sync::mpsc;
|
||||
///
|
||||
/// let (tx, rx) = mpsc::channel(10);
|
||||
/// let node = SingleSubscriberNode::new(tx);
|
||||
/// ```
|
||||
pub struct SingleSubscriberNode {
|
||||
tx: mpsc::Sender<Arc<AudioSegment>>,
|
||||
}
|
||||
|
||||
impl SingleSubscriberNode {
|
||||
pub fn new(tx: mpsc::Sender<Arc<AudioSegment>>) -> Self {
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
pub async fn push(&self, chunk: Arc<AudioSegment>) -> Result<(), AudioError> {
|
||||
self.tx.send(chunk).await.map_err(|_| AudioError::SendError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Node avec plusieurs abonnés (partage le même Arc)
|
||||
///
|
||||
/// Permet de broadcaster un chunk à plusieurs destinations.
|
||||
/// Tous les abonnés reçoivent le même `Arc<AudioSegment>`, donc pas de copie
|
||||
/// des données audio - seul le compteur de référence Arc est incrémenté.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoaudio::MultiSubscriberNode;
|
||||
/// use tokio::sync::mpsc;
|
||||
///
|
||||
/// let mut node = MultiSubscriberNode::new();
|
||||
/// let (tx1, rx1) = mpsc::channel(10);
|
||||
/// let (tx2, rx2) = mpsc::channel(10);
|
||||
///
|
||||
/// node.add_subscriber(tx1);
|
||||
/// node.add_subscriber(tx2);
|
||||
/// // Les deux abonnés recevront les mêmes chunks
|
||||
/// ```
|
||||
pub struct MultiSubscriberNode {
|
||||
subscribers: Vec<mpsc::Sender<Arc<AudioSegment>>>,
|
||||
}
|
||||
|
||||
impl MultiSubscriberNode {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.subscribers.push(tx);
|
||||
}
|
||||
|
||||
pub async fn push(&self, chunk: Arc<AudioSegment>) -> Result<(), AudioError> {
|
||||
for tx in &self.subscribers {
|
||||
// On partage le même Arc avec tous les abonnés
|
||||
tx.send(chunk.clone())
|
||||
.await
|
||||
.map_err(|_| AudioError::SendError)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn try_push(&self, chunk: Arc<AudioSegment>) -> Result<(), AudioError> {
|
||||
for tx in &self.subscribers {
|
||||
// try_send non-bloquant, ignore si saturé
|
||||
let _ = tx.try_send(chunk.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MultiSubscriberNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Erreurs possibles dans le pipeline audio
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AudioError {
|
||||
/// Échec d'envoi d'un chunk à travers un channel
|
||||
SendError,
|
||||
/// Échec de réception d'un chunk depuis un channel
|
||||
ReceiveError,
|
||||
/// Erreur de traitement avec message descriptif
|
||||
ProcessingError(String),
|
||||
/// Incompatibilité de types entre nodes
|
||||
TypeMismatch(TypeMismatch),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AudioError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AudioError::SendError => write!(f, "Failed to send audio chunk"),
|
||||
AudioError::ReceiveError => write!(f, "Failed to receive audio chunk"),
|
||||
AudioError::ProcessingError(msg) => write!(f, "Processing error: {}", msg),
|
||||
AudioError::TypeMismatch(tm) => write!(f, "{}", tm),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AudioError {}
|
||||
@@ -175,23 +175,6 @@ impl TryFrom<i32> for I24 {
|
||||
// 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;
|
||||
@@ -222,7 +205,9 @@ impl Sample for I24 {
|
||||
|
||||
#[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;
|
||||
let scaled = (value * 8_388_607.0)
|
||||
.clamp(-8_388_608.0, 8_388_607.0)
|
||||
.round() as i32;
|
||||
I24(scaled)
|
||||
}
|
||||
}
|
||||
@@ -240,7 +225,9 @@ impl Sample for i32 {
|
||||
|
||||
#[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
|
||||
(value * 2_147_483_647.0)
|
||||
.clamp(-2_147_483_648.0, 2_147_483_647.0)
|
||||
.round() as i32
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
385
pmoaudio/src/type_constraints.rs
Normal file
385
pmoaudio/src/type_constraints.rs
Normal file
@@ -0,0 +1,385 @@
|
||||
//! Système de contraintes de types pour les nodes audio
|
||||
//!
|
||||
//! Ce module définit les types et structures permettant de vérifier la compatibilité
|
||||
//! entre les producers et consumers de chunks audio dans un pipeline.
|
||||
|
||||
use crate::AudioChunk;
|
||||
use std::fmt;
|
||||
|
||||
/// Type d'échantillon supporté
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SampleType {
|
||||
/// Entier 16-bit
|
||||
I16,
|
||||
/// Entier 24-bit (I24)
|
||||
I24,
|
||||
/// Entier 32-bit
|
||||
I32,
|
||||
/// Flottant 32-bit
|
||||
F32,
|
||||
/// Flottant 64-bit
|
||||
F64,
|
||||
}
|
||||
|
||||
impl SampleType {
|
||||
/// Vérifie si le type est un entier
|
||||
pub fn is_integer(&self) -> bool {
|
||||
matches!(self, SampleType::I16 | SampleType::I24 | SampleType::I32)
|
||||
}
|
||||
|
||||
/// Vérifie si le type est un flottant
|
||||
pub fn is_float(&self) -> bool {
|
||||
matches!(self, SampleType::F32 | SampleType::F64)
|
||||
}
|
||||
|
||||
/// Retourne la profondeur de bit
|
||||
pub fn bit_depth(&self) -> u8 {
|
||||
match self {
|
||||
SampleType::I16 => 16,
|
||||
SampleType::I24 => 24,
|
||||
SampleType::I32 | SampleType::F32 => 32,
|
||||
SampleType::F64 => 64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrait le type d'un AudioChunk
|
||||
pub fn from_audio_chunk(chunk: &AudioChunk) -> Self {
|
||||
match chunk {
|
||||
AudioChunk::I16(_) => SampleType::I16,
|
||||
AudioChunk::I24(_) => SampleType::I24,
|
||||
AudioChunk::I32(_) => SampleType::I32,
|
||||
AudioChunk::F32(_) => SampleType::F32,
|
||||
AudioChunk::F64(_) => SampleType::F64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SampleType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SampleType::I16 => write!(f, "I16"),
|
||||
SampleType::I24 => write!(f, "I24"),
|
||||
SampleType::I32 => write!(f, "I32"),
|
||||
SampleType::F32 => write!(f, "F32"),
|
||||
SampleType::F64 => write!(f, "F64"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Catégorie de type acceptée
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TypeCategory {
|
||||
/// N'importe quel type entier (I16, I24, I32)
|
||||
AnyInteger,
|
||||
/// N'importe quel type flottant (F32, F64)
|
||||
AnyFloat,
|
||||
/// Un type spécifique uniquement
|
||||
Specific(SampleType),
|
||||
/// N'importe quel type (entier ou flottant)
|
||||
Any,
|
||||
}
|
||||
|
||||
impl TypeCategory {
|
||||
/// Vérifie si cette catégorie accepte le type donné
|
||||
pub fn accepts(&self, sample_type: SampleType) -> bool {
|
||||
match self {
|
||||
TypeCategory::AnyInteger => sample_type.is_integer(),
|
||||
TypeCategory::AnyFloat => sample_type.is_float(),
|
||||
TypeCategory::Specific(t) => *t == sample_type,
|
||||
TypeCategory::Any => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne tous les types possibles pour cette catégorie
|
||||
pub fn possible_types(&self) -> Vec<SampleType> {
|
||||
match self {
|
||||
TypeCategory::AnyInteger => vec![SampleType::I16, SampleType::I24, SampleType::I32],
|
||||
TypeCategory::AnyFloat => vec![SampleType::F32, SampleType::F64],
|
||||
TypeCategory::Specific(t) => vec![*t],
|
||||
TypeCategory::Any => vec![
|
||||
SampleType::I16,
|
||||
SampleType::I24,
|
||||
SampleType::I32,
|
||||
SampleType::F32,
|
||||
SampleType::F64,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TypeCategory {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TypeCategory::AnyInteger => write!(f, "AnyInteger (I16|I24|I32)"),
|
||||
TypeCategory::AnyFloat => write!(f, "AnyFloat (F32|F64)"),
|
||||
TypeCategory::Specific(t) => write!(f, "{}", t),
|
||||
TypeCategory::Any => write!(f, "Any"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contrainte de type pour un node
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TypeRequirement {
|
||||
/// Catégorie de type acceptée
|
||||
pub category: TypeCategory,
|
||||
/// Types spécifiques acceptés (pour contraintes plus fines)
|
||||
/// Si vide, utilise category.possible_types()
|
||||
pub accepted_types: Vec<SampleType>,
|
||||
}
|
||||
|
||||
impl TypeRequirement {
|
||||
/// Crée une contrainte pour n'importe quel type
|
||||
pub fn any() -> Self {
|
||||
Self {
|
||||
category: TypeCategory::Any,
|
||||
accepted_types: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée une contrainte pour n'importe quel entier
|
||||
pub fn any_integer() -> Self {
|
||||
Self {
|
||||
category: TypeCategory::AnyInteger,
|
||||
accepted_types: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée une contrainte pour n'importe quel flottant
|
||||
pub fn any_float() -> Self {
|
||||
Self {
|
||||
category: TypeCategory::AnyFloat,
|
||||
accepted_types: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée une contrainte pour un type spécifique
|
||||
pub fn specific(sample_type: SampleType) -> Self {
|
||||
Self {
|
||||
category: TypeCategory::Specific(sample_type),
|
||||
accepted_types: vec![sample_type],
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée une contrainte avec une liste explicite de types acceptés
|
||||
pub fn from_list(types: Vec<SampleType>) -> Self {
|
||||
// Déterminer la catégorie la plus appropriée
|
||||
let all_integer = types.iter().all(|t| t.is_integer());
|
||||
let all_float = types.iter().all(|t| t.is_float());
|
||||
|
||||
let category = if types.len() == 1 {
|
||||
TypeCategory::Specific(types[0])
|
||||
} else if all_integer && types.len() == 3 {
|
||||
TypeCategory::AnyInteger
|
||||
} else if all_float && types.len() == 2 {
|
||||
TypeCategory::AnyFloat
|
||||
} else if types.len() == 5 {
|
||||
TypeCategory::Any
|
||||
} else {
|
||||
// Catégorie personnalisée - on garde la liste explicite
|
||||
TypeCategory::Any
|
||||
};
|
||||
|
||||
Self {
|
||||
category,
|
||||
accepted_types: types,
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie si cette contrainte accepte le type donné
|
||||
pub fn accepts(&self, sample_type: SampleType) -> bool {
|
||||
if !self.accepted_types.is_empty() {
|
||||
// Si une liste explicite est fournie, utiliser celle-ci
|
||||
self.accepted_types.contains(&sample_type)
|
||||
} else {
|
||||
// Sinon, utiliser la catégorie
|
||||
self.category.accepts(sample_type)
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne tous les types acceptés par cette contrainte
|
||||
pub fn get_accepted_types(&self) -> Vec<SampleType> {
|
||||
if !self.accepted_types.is_empty() {
|
||||
self.accepted_types.clone()
|
||||
} else {
|
||||
self.category.possible_types()
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie si cette contrainte est plus restrictive qu'une autre
|
||||
pub fn is_more_restrictive_than(&self, other: &TypeRequirement) -> bool {
|
||||
let my_types = self.get_accepted_types();
|
||||
let other_types = other.get_accepted_types();
|
||||
|
||||
// Je suis plus restrictif si tous mes types sont dans other_types
|
||||
// et que j'en ai moins
|
||||
my_types.iter().all(|t| other_types.contains(t)) && my_types.len() < other_types.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TypeRequirement {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if !self.accepted_types.is_empty() && self.accepted_types.len() < 5 {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
self.accepted_types
|
||||
.iter()
|
||||
.map(|t| t.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("|")
|
||||
)
|
||||
} else {
|
||||
write!(f, "{}", self.category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie la compatibilité entre un producer et un consumer
|
||||
///
|
||||
/// # Règles de compatibilité :
|
||||
///
|
||||
/// 1. Si le producer produit un type spécifique et que le consumer l'accepte → compatible
|
||||
/// 2. Si le producer peut produire plusieurs types (ex: AnyInteger) et que le consumer
|
||||
/// accepte un type spécifique (ex: I24), le producer POURRAIT produire un type
|
||||
/// incompatible → **incompatible** (nécessite conversion explicite)
|
||||
/// 3. Si le producer produit un type spécifique et que le consumer accepte une catégorie
|
||||
/// contenant ce type → compatible
|
||||
///
|
||||
/// # Exemples :
|
||||
///
|
||||
/// - Producer(Specific(I24)) + Consumer(AnyInteger) → Compatible ✓
|
||||
/// - Producer(AnyInteger) + Consumer(Specific(I24)) → Incompatible ✗ (producer peut produire I16)
|
||||
/// - Producer(Specific(I24)) + Consumer(Specific(I24)) → Compatible ✓
|
||||
/// - Producer(AnyInteger) + Consumer(AnyInteger) → Compatible ✓
|
||||
pub fn check_compatibility(
|
||||
producer: &TypeRequirement,
|
||||
consumer: &TypeRequirement,
|
||||
) -> Result<(), TypeMismatch> {
|
||||
let producer_types = producer.get_accepted_types();
|
||||
let consumer_types = consumer.get_accepted_types();
|
||||
|
||||
// Vérifier si tous les types que le producer peut produire sont acceptés par le consumer
|
||||
for prod_type in &producer_types {
|
||||
if !consumer_types.contains(prod_type) {
|
||||
return Err(TypeMismatch {
|
||||
producer: producer.clone(),
|
||||
consumer: consumer.clone(),
|
||||
incompatible_type: Some(*prod_type),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Erreur de compatibilité de types
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TypeMismatch {
|
||||
/// Type requirement du producer
|
||||
pub producer: TypeRequirement,
|
||||
/// Type requirement du consumer
|
||||
pub consumer: TypeRequirement,
|
||||
/// Type spécifique incompatible (si identifié)
|
||||
pub incompatible_type: Option<SampleType>,
|
||||
}
|
||||
|
||||
impl fmt::Display for TypeMismatch {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Type mismatch: producer produces {} but consumer only accepts {}",
|
||||
self.producer, self.consumer
|
||||
)?;
|
||||
if let Some(incomp_type) = self.incompatible_type {
|
||||
write!(f, " (incompatible type: {})", incomp_type)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TypeMismatch {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sample_type_is_integer() {
|
||||
assert!(SampleType::I16.is_integer());
|
||||
assert!(SampleType::I24.is_integer());
|
||||
assert!(SampleType::I32.is_integer());
|
||||
assert!(!SampleType::F32.is_integer());
|
||||
assert!(!SampleType::F64.is_integer());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_type_is_float() {
|
||||
assert!(!SampleType::I16.is_float());
|
||||
assert!(SampleType::F32.is_float());
|
||||
assert!(SampleType::F64.is_float());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_category_accepts() {
|
||||
let any_int = TypeCategory::AnyInteger;
|
||||
assert!(any_int.accepts(SampleType::I16));
|
||||
assert!(any_int.accepts(SampleType::I24));
|
||||
assert!(any_int.accepts(SampleType::I32));
|
||||
assert!(!any_int.accepts(SampleType::F32));
|
||||
|
||||
let specific = TypeCategory::Specific(SampleType::I24);
|
||||
assert!(!specific.accepts(SampleType::I16));
|
||||
assert!(specific.accepts(SampleType::I24));
|
||||
assert!(!specific.accepts(SampleType::I32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compatibility_specific_to_category() {
|
||||
// Producer(Specific(I24)) + Consumer(AnyInteger) → Compatible
|
||||
let producer = TypeRequirement::specific(SampleType::I24);
|
||||
let consumer = TypeRequirement::any_integer();
|
||||
assert!(check_compatibility(&producer, &consumer).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compatibility_category_to_specific() {
|
||||
// Producer(AnyInteger) + Consumer(Specific(I24)) → Incompatible
|
||||
let producer = TypeRequirement::any_integer();
|
||||
let consumer = TypeRequirement::specific(SampleType::I24);
|
||||
assert!(check_compatibility(&producer, &consumer).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compatibility_same_specific() {
|
||||
// Producer(Specific(I24)) + Consumer(Specific(I24)) → Compatible
|
||||
let producer = TypeRequirement::specific(SampleType::I24);
|
||||
let consumer = TypeRequirement::specific(SampleType::I24);
|
||||
assert!(check_compatibility(&producer, &consumer).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compatibility_same_category() {
|
||||
// Producer(AnyInteger) + Consumer(AnyInteger) → Compatible
|
||||
let producer = TypeRequirement::any_integer();
|
||||
let consumer = TypeRequirement::any_integer();
|
||||
assert!(check_compatibility(&producer, &consumer).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compatibility_integer_to_float() {
|
||||
// Producer(AnyInteger) + Consumer(AnyFloat) → Incompatible
|
||||
let producer = TypeRequirement::any_integer();
|
||||
let consumer = TypeRequirement::any_float();
|
||||
assert!(check_compatibility(&producer, &consumer).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_requirement_from_list() {
|
||||
let req = TypeRequirement::from_list(vec![SampleType::I24, SampleType::I32]);
|
||||
assert!(req.accepts(SampleType::I24));
|
||||
assert!(req.accepts(SampleType::I32));
|
||||
assert!(!req.accepts(SampleType::I16));
|
||||
assert!(!req.accepts(SampleType::F32));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user