Passons en stéreo

reprise du module DSP pmoaudio
This commit is contained in:
2025-10-28 18:41:34 +01:00
parent 68b092673b
commit c31596fdb5
29 changed files with 2036 additions and 415 deletions

BIN
.DS_Store vendored

Binary file not shown.

10
.vscode/settings.json vendored
View File

@@ -3,5 +3,13 @@
"git.enabled": false, "git.enabled": false,
"claude-code.environmentVariables": [ "claude-code.environmentVariables": [
] ],
// Exclusions via VS Code
"files.exclude": {
"target": true,
"**/target": true,
"node_modules": true
},
"rust-analyzer.procMacro.enable": true,
"rust-analyzer.numThreads": 4
} }

33
Cargo.lock generated
View File

@@ -522,18 +522,18 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.23.2" version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
dependencies = [ dependencies = [
"bytemuck_derive", "bytemuck_derive",
] ]
[[package]] [[package]]
name = "bytemuck_derive" name = "bytemuck_derive"
version = "1.10.1" version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2025,6 +2025,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "libsoxr-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cffdc8b3b64759d2e95e3236fa6bf85fef226bf57023fa9273ec60edea8fbce"
dependencies = [
"pkg-config",
]
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.35.0" version = "0.35.0"
@@ -2656,6 +2665,11 @@ name = "pmoaudio"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytemuck",
"paste",
"pmoflac",
"soxr",
"tempfile",
"tokio", "tokio",
"tokio-test", "tokio-test",
] ]
@@ -3816,6 +3830,17 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "soxr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3ca6bc65602daad89c3217255ff89c4abbe489b9dd3e56c66aa16d1addac979"
dependencies = [
"bitflags 2.9.4",
"bytemuck",
"libsoxr-sys",
]
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.10.0" version = "0.10.0"

View File

@@ -1,6 +1,8 @@
# Makefile pour projet Rust + Vue.js # Makefile pour projet Rust + Vue.js
# Variables de configuration # Variables de configuration
CARGO = cargo CARGO = cargo
CARGO_NIGHTLY = rustup run nightly cargo
FEATURES ?=
NPM = npm NPM = npm
WEBAPP_DIR = pmoapp/webapp WEBAPP_DIR = pmoapp/webapp
DIST_DIR = $(WEBAPP_DIR)/dist DIST_DIR = $(WEBAPP_DIR)/dist
@@ -14,7 +16,9 @@ YELLOW = \033[1;33m
RED = \033[0;31m RED = \033[0;31m
NC = \033[0m # No Color NC = \033[0m # No Color
.PHONY: all help build release debug test doc webapp clean install dev check fmt clippy watch .DEFAULT_GOAL := simd
.PHONY: all help build release debug test doc webapp clean install dev check fmt clippy watch simd scalar
# Cible par défaut # Cible par défaut
all: build all: build
@@ -31,13 +35,13 @@ build: webapp release
## release: Compile le binaire Rust en mode release ## release: Compile le binaire Rust en mode release
release: webapp release: webapp
@echo "$(YELLOW)→ Compilation Rust (release)...$(NC)" @echo "$(YELLOW)→ Compilation Rust (release)...$(NC)"
$(CARGO) build --release $(CARGO) build --release $(FEATURES)
@echo "$(GREEN)✓ Binaire disponible : $(RUST_TARGET)/$(BINARY_NAME)$(NC)" @echo "$(GREEN)✓ Binaire disponible : $(RUST_TARGET)/$(BINARY_NAME)$(NC)"
## debug: Compile le binaire Rust en mode debug ## debug: Compile le binaire Rust en mode debug
debug: webapp debug: webapp
@echo "$(YELLOW)→ Compilation Rust (debug)...$(NC)" @echo "$(YELLOW)→ Compilation Rust (debug)...$(NC)"
$(CARGO) build $(CARGO) build $(FEATURES)
@echo "$(GREEN)✓ Binaire disponible : target/debug/$(BINARY_NAME)$(NC)" @echo "$(GREEN)✓ Binaire disponible : target/debug/$(BINARY_NAME)$(NC)"
## test: Exécute tous les tests Rust ## test: Exécute tous les tests Rust
@@ -46,6 +50,18 @@ test:
$(CARGO) test --all $(CARGO) test --all
@echo "$(GREEN)✓ Tests terminés$(NC)" @echo "$(GREEN)✓ Tests terminés$(NC)"
## simd: Compile l'application en mode SIMD (nightly requis)
simd:
@echo "$(YELLOW)→ Build SIMD (nightly)...$(NC)"
$(MAKE) release CARGO="$(CARGO_NIGHTLY)" FEATURES="--features simd"
@echo "$(GREEN)✓ Build SIMD terminé$(NC)"
## scalar: Compile l'application en mode scalaire
scalar:
@echo "$(YELLOW)→ Build scalaire...$(NC)"
$(MAKE) release FEATURES=""
@echo "$(GREEN)✓ Build scalaire terminé$(NC)"
## test-doc: Teste les exemples dans la documentation ## test-doc: Teste les exemples dans la documentation
test-doc: test-doc:
@echo "$(YELLOW)→ Test des exemples de documentation...$(NC)" @echo "$(YELLOW)→ Test des exemples de documentation...$(NC)"
@@ -187,4 +203,4 @@ coverage:
@echo "$(YELLOW)→ Génération du rapport de couverture...$(NC)" @echo "$(YELLOW)→ Génération du rapport de couverture...$(NC)"
$(CARGO) tarpaulin --out Html --output-dir target/coverage $(CARGO) tarpaulin --out Html --output-dir target/coverage
@echo "$(GREEN)✓ Rapport disponible dans target/coverage/index.html$(NC)" @echo "$(GREEN)✓ Rapport disponible dans target/coverage/index.html$(NC)"

View File

@@ -98,3 +98,10 @@ jj rebase --continue
pour résoudre les conflits pour résoudre les conflits
## Installer rust sur mac
```bash
brew install rustup-init
rustup-init
rustup default stable
```

View File

@@ -3,9 +3,18 @@ name = "pmoaudio"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[features]
default = []
simd = []
[dependencies] [dependencies]
tokio = { version = "1.42", features = ["full"] } tokio = { version = "1.42", features = ["full"] }
async-trait = "0.1" async-trait = "0.1"
pmoflac = { path = "../pmoflac" }
paste = "1"
soxr = "0.6.0"
bytemuck = "1.24.0"
[dev-dependencies] [dev-dependencies]
tokio-test = "0.4" tokio-test = "0.4"
tempfile = "3"

View File

@@ -1,5 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use crate::{dsp, BitDepth};
/// Représente un chunk audio stéréo avec données partagées via Arc /// Représente un chunk audio stéréo avec données partagées via Arc
/// ///
/// Cette structure encapsule des données audio stéréo (canaux gauche et droit) /// Cette structure encapsule des données audio stéréo (canaux gauche et droit)
@@ -16,43 +18,43 @@ use std::sync::Arc;
/// # Exemples /// # Exemples
/// ///
/// ``` /// ```
/// use pmoaudio::AudioChunk; /// use pmoaudio::{AudioChunk, BitDepth};
/// ///
/// // Créer un chunk avec des données générées /// // Créer un chunk avec des données générées
/// let left = vec![0.0, 0.1, 0.2, 0.3]; /// let stereo = vec![[0, 100], [200, 300], [400, 500]];
/// let right = vec![0.0, 0.1, 0.2, 0.3]; /// let chunk = AudioChunk::new(0, stereo, 48_000, BitDepth::B24);
/// let chunk = AudioChunk::new(0, left, right, 48000);
/// ///
/// assert_eq!(chunk.len(), 4); /// assert_eq!(chunk.len(), 3);
/// assert_eq!(chunk.sample_rate, 48000); /// assert_eq!(chunk.sample_rate(), 48_000);
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AudioChunk { pub struct AudioChunk {
/// Numéro d'ordre du chunk dans le flux /// Numéro dordre dans le flux.
/// /// Sert à conserver la séquence et détecter déventuelles pertes.
/// Permet de suivre l'ordre des chunks et détecter les pertes éventuelles order: u64,
pub order: u64,
/// Canal gauche (partagé via Arc pour éviter les clonages) /// Canal gauche, partagé et immuable.
/// /// Toute transformation doit créer un nouveau `AudioChunk`.
/// Les samples sont en format float 32-bit, normalement entre -1.0 et 1.0 stereo: Arc<[[i32; 2]]>,
pub left: Arc<Vec<f32>>,
/// Canal droit (partagé via Arc pour éviter les clonages) /// Taux déchantillonnage (Hz).
/// /// Exemples : 44 100, 48 000, 96 000, 192 000.
/// Les samples sont en format float 32-bit, normalement entre -1.0 et 1.0 sample_rate: u32,
pub right: Arc<Vec<f32>>,
/// Taux d'échantillonnage en Hz /// Profondeur de bits des échantillons audio effectifs.
/// ///
/// Valeurs typiques: 44100, 48000, 96000, 192000 /// Indique la résolution utile des valeurs dans les buffers.
pub sample_rate: u32, /// Exemples : `16` pour un flux PCM 16 bits, `24` pour du PCM 24 bits, `32` pour du plein i32.
/// Ce champ permet dadapter les traitements DSP (normalisation, conversion, etc.).
bit_depth: BitDepth,
/// Gain multiplicatif appliqué au flux audio /// Gain appliqué au flux audio, en décibels (dB).
/// ///
/// Valeur par défaut: 1.0 (aucun changement) /// Conversion : `gain_linear = 10^(gain_db / 20)`
/// Valeurs typiques: 0.0 (silence) à 1.0 (volume max) /// Valeur par défaut : `0.0 dB` (aucune modification).
pub gain: f32, /// Exemples : `-6 dB` ≈ moitié du volume ; `+6 dB` ≈ double.
gain: f64,
} }
impl AudioChunk { impl AudioChunk {
@@ -63,82 +65,120 @@ impl AudioChunk {
/// # Arguments /// # Arguments
/// ///
/// * `order` - Numéro d'ordre du chunk dans le flux /// * `order` - Numéro d'ordre du chunk dans le flux
/// * `left` - Samples du canal gauche /// * `stereo` - Samples interleavés par frame `[L, R]`
/// * `right` - Samples du canal droit
/// * `sample_rate` - Taux d'échantillonnage en Hz /// * `sample_rate` - Taux d'échantillonnage en Hz
/// * `bit_depth` - Profondeur de bits des échantillons
/// ///
/// # Exemples /// # Exemples
/// ///
/// ``` /// ```
/// use pmoaudio::AudioChunk; /// use pmoaudio::{AudioChunk, BitDepth};
/// ///
/// let chunk = AudioChunk::new( /// let chunk = AudioChunk::new(
/// 0, /// 0,
/// vec![0.0, 0.5, 1.0], /// vec![[0, 0], [1_000_000, 1_000_000]],
/// vec![0.0, 0.5, 1.0], /// 48_000,
/// 48000 /// BitDepth::B24,
/// ); /// );
/// ``` /// ```
pub fn new(order: u64, left: Vec<f32>, right: Vec<f32>, sample_rate: u32) -> Self { pub fn new(
Self { order: u64,
stereo: Vec<[i32; 2]>,
sample_rate: u32,
bit_depth: BitDepth,
) -> Arc<Self> {
Arc::new(Self {
order, order,
left: Arc::new(left), stereo: Arc::from(stereo),
right: Arc::new(right),
sample_rate, sample_rate,
gain: 1.0, bit_depth,
} gain: 0.0,
})
} }
/// Crée un nouveau chunk audio avec un gain spécifique /// Crée un chunk avec un gain spécifique (en dB)
pub fn with_gain( pub fn with_gain_db(
order: u64,
stereo: Vec<[i32; 2]>,
sample_rate: u32,
bit_depth: BitDepth,
gain_db: f64,
) -> Arc<Self> {
Self::new(order, stereo, sample_rate, bit_depth).set_gain_db(gain_db)
}
/// Crée un chunk avec un gain spécifique (en gain linéaire).
///
/// Le gain linéaire sera converti en décibels.
pub fn with_gain_linear(
order: u64,
stereo: Vec<[i32; 2]>,
sample_rate: u32,
bit_depth: BitDepth,
gain_linear: f64,
) -> Arc<Self> {
Self::new(order, stereo, sample_rate, bit_depth).set_gain_linear(gain_linear)
}
/// Construit un chunk à partir de deux vecteurs `i32` séparés (L/R).
pub fn from_channels_i32(
order: u64,
left: Vec<i32>,
right: Vec<i32>,
sample_rate: u32,
bit_depth: BitDepth,
) -> Arc<Self> {
assert_eq!(
left.len(),
right.len(),
"channels must have identical length"
);
let stereo = left
.into_iter()
.zip(right.into_iter())
.map(|(l, r)| [l, r])
.collect();
Self::new(order, stereo, sample_rate, bit_depth)
}
/// Construit un chunk à partir de vecteurs `f32` normalisés dans [-1.0, 1.0].
pub fn from_channels_f32(
order: u64, order: u64,
left: Vec<f32>, left: Vec<f32>,
right: Vec<f32>, right: Vec<f32>,
sample_rate: u32, sample_rate: u32,
gain: f32, bit_depth: BitDepth,
) -> Self { ) -> Arc<Self> {
Self { assert_eq!(
order, left.len(),
left: Arc::new(left), right.len(),
right: Arc::new(right), "channels must have identical length"
sample_rate, );
gain, let stereo = left
} .into_iter()
.zip(right.into_iter())
.map(|(l, r)| [quantize_sample(l, bit_depth), quantize_sample(r, bit_depth)])
.collect();
Self::new(order, stereo, sample_rate, bit_depth)
} }
/// Crée un chunk à partir de données déjà wrappées dans Arc /// Construit un chunk à partir de frames stéréo normalisées [-1.0, 1.0].
/// pub fn from_pairs_f32(
/// Utile pour éviter un double wrapping si les données sont déjà dans Arc.
pub fn from_arc(
order: u64, order: u64,
left: Arc<Vec<f32>>, pairs: Vec<[f32; 2]>,
right: Arc<Vec<f32>>,
sample_rate: u32, sample_rate: u32,
) -> Self { bit_depth: BitDepth,
Self { ) -> Arc<Self> {
order, let stereo = pairs
left, .into_iter()
right, .map(|p| {
sample_rate, [
gain: 1.0, quantize_sample(p[0], bit_depth),
} quantize_sample(p[1], bit_depth),
} ]
})
/// Crée un chunk à partir de données déjà wrappées dans Arc avec gain .collect();
pub fn from_arc_with_gain( Self::new(order, stereo, sample_rate, bit_depth)
order: u64,
left: Arc<Vec<f32>>,
right: Arc<Vec<f32>>,
sample_rate: u32,
gain: f32,
) -> Self {
Self {
order,
left,
right,
sample_rate,
gain,
}
} }
/// Retourne le nombre d'échantillons par canal /// Retourne le nombre d'échantillons par canal
@@ -146,41 +186,98 @@ impl AudioChunk {
/// # Exemples /// # Exemples
/// ///
/// ``` /// ```
/// use pmoaudio::AudioChunk; /// use pmoaudio::{AudioChunk, BitDepth};
/// ///
/// let chunk = AudioChunk::new(0, vec![0.0; 1000], vec![0.0; 1000], 48000); /// let chunk = AudioChunk::new(0, vec![[0i32; 2]; 1000], 48_000, BitDepth::B24);
/// assert_eq!(chunk.len(), 1000); /// assert_eq!(chunk.len(), 1000);
/// ``` /// ```
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.left.len() self.stereo.len()
} }
/// Vérifie si le chunk est vide /// Vérifie si le chunk est vide
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.left.is_empty() self.stereo.is_empty()
}
/// Numéro de séquence du chunk dans le flux.
pub fn order(&self) -> u64 {
self.order
}
/// Taux d'échantillonnage (Hz).
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
/// Profondeur de bits effective.
pub fn bit_depth(&self) -> BitDepth {
self.bit_depth
}
/// Gain courant en décibels.
pub fn gain_db(&self) -> f64 {
self.gain
}
/// Gain sous forme linéaire.
pub fn gain_linear(&self) -> f64 {
db_to_linear(self.gain)
}
/// Convertit un gain linéaire (>0) en décibels.
pub fn gain_db_from_linear(gain_linear: f64) -> f64 {
linear_to_db(gain_linear)
}
/// Convertit un gain en décibels vers un gain linéaire.
pub fn gain_linear_from_db(gain_db: f64) -> f64 {
db_to_linear(gain_db)
}
/// Retourne une vue immuable sur les frames `[L,R]`.
pub fn frames(&self) -> &[[i32; 2]] {
&self.stereo
}
/// Clone les frames stéréo dans un `Vec`.
pub fn clone_frames(&self) -> Vec<[i32; 2]> {
self.stereo.to_vec()
}
/// Convertit les frames au format `f32` normalisé [-1.0, 1.0].
pub fn to_pairs_f32(&self) -> Vec<[f32; 2]> {
self.stereo
.iter()
.map(|frame| {
[
dequantize_sample(frame[0], self.bit_depth),
dequantize_sample(frame[1], self.bit_depth),
]
})
.collect()
} }
/// Clone les données pour permettre une modification (Copy-on-Write) /// Clone les données pour permettre une modification (Copy-on-Write)
/// ///
/// Cette méthode doit être appelée uniquement si vous avez besoin de modifier /// Cette méthode doit être appelée uniquement si vous avez besoin de modifier
/// les données audio. Pour une simple lecture, utilisez directement les champs /// les échantillons. Pour une simple lecture, utilisez [`frames`](Self::frames).
/// `left` et `right`.
/// ///
/// # Exemples /// # Exemples
/// ///
/// ``` /// ```
/// use pmoaudio::AudioChunk; /// use pmoaudio::{AudioChunk, BitDepth};
/// ///
/// let chunk = AudioChunk::new(0, vec![1.0, 2.0], vec![3.0, 4.0], 48000); /// let chunk = AudioChunk::new(0, vec![[1, 2], [3, 4]], 48_000, BitDepth::B24);
/// let (mut left, mut right) = chunk.clone_data(); /// let mut frames = chunk.clone_data();
/// /// frames[0][0] /= 2;
/// // Modifier les données
/// for sample in &mut left {
/// *sample *= 0.5;
/// }
/// ``` /// ```
pub fn clone_data(&self) -> (Vec<f32>, Vec<f32>) { pub fn clone_data(&self) -> Vec<[i32; 2]> {
((*self.left).clone(), (*self.right).clone()) self.stereo.to_vec()
}
pub fn set_data(&mut self, stereo: Vec<[i32; 2]>) {
self.stereo = Arc::from(stereo);
} }
/// Applique le gain et retourne un nouveau chunk avec les données modifiées /// Applique le gain et retourne un nouveau chunk avec les données modifiées
@@ -191,38 +288,78 @@ impl AudioChunk {
/// # Exemples /// # Exemples
/// ///
/// ``` /// ```
/// use pmoaudio::AudioChunk; /// use pmoaudio::{AudioChunk, BitDepth};
/// ///
/// let chunk = AudioChunk::with_gain(0, vec![1.0, 2.0], vec![3.0, 4.0], 48000, 0.5); /// let chunk = AudioChunk::from_pairs_f32(
/// 0,
/// vec![[0.5, 0.25], [0.25, 0.125]],
/// 48_000,
/// BitDepth::B24,
/// );
/// let chunk = chunk.set_gain_linear(0.5);
/// let applied = chunk.apply_gain(); /// let applied = chunk.apply_gain();
/// let frames = applied.to_pairs_f32();
/// ///
/// assert_eq!(applied.left[0], 0.5); /// assert!((frames[0][0] - 0.25).abs() < 1e-3);
/// assert_eq!(applied.left[1], 1.0); /// assert!((applied.gain_db()).abs() < f64::EPSILON); // Gain réinitialisé après application
/// assert_eq!(applied.gain, 1.0); // Gain réinitialisé après application
/// ``` /// ```
pub fn apply_gain(&self) -> Self { pub fn apply_gain(self: Arc<Self>) -> Arc<Self> {
if (self.gain - 1.0).abs() < f32::EPSILON { if self.gain.abs() < f64::EPSILON {
// Pas de gain à appliquer, retourner un clone // Pas de gain à appliquer, retourner la même instance
return self.clone(); return self;
} }
let left: Vec<f32> = self.left.iter().map(|&s| s * self.gain).collect(); let mut stereo = self.clone_data();
let right: Vec<f32> = self.right.iter().map(|&s| s * self.gain).collect(); dsp::apply_gain_stereo(&mut stereo, self.gain);
Self::new(self.order, left, right, self.sample_rate) Self::new(self.order, stereo, self.sample_rate, self.bit_depth)
}
pub fn set_gain_db(&self, gain: f64) -> Arc<Self> {
Arc::new(Self {
order: self.order,
stereo: self.stereo.clone(),
sample_rate: self.sample_rate,
bit_depth: self.bit_depth,
gain,
})
}
/// Définit le gain à l'aide d'un facteur linéaire (>0).
pub fn set_gain_linear(&self, gain_linear: f64) -> Arc<Self> {
self.set_gain_db(linear_to_db(gain_linear))
} }
/// Modifie le gain de ce chunk (retourne un nouveau chunk avec le même Arc mais gain différent) /// Modifie le gain de ce chunk (retourne un nouveau chunk avec le même Arc mais gain différent)
/// ///
/// Cette méthode est très peu coûteuse car elle ne clone que la structure, pas les données audio. /// Cette méthode est très peu coûteuse car elle ne clone que la structure, pas les données audio.
pub fn with_modified_gain(&self, new_gain: f32) -> Self { pub fn with_modified_gain_db(&self, delta_gain_db: f64) -> Arc<Self> {
Self { self.set_gain_db(self.gain + delta_gain_db)
order: self.order, }
left: self.left.clone(),
right: self.right.clone(), /// Modifie le gain via un facteur linéaire multiplié au gain courant.
sample_rate: self.sample_rate, pub fn with_modified_gain_linear(&self, gain_linear: f64) -> Arc<Self> {
gain: self.gain * new_gain, // Multiplication des gains self.with_modified_gain_db(linear_to_db(gain_linear))
}
pub fn get_bit_depth(&self) -> BitDepth {
self.bit_depth
}
pub fn set_bit_depth(self: Arc<Self>, new_depth: BitDepth) -> Arc<Self> {
if self.bit_depth == new_depth {
return self;
} }
let mut stereo = self.clone_data();
dsp::bitdepth_change_stereo(&mut stereo, self.bit_depth, new_depth);
Arc::new(Self {
order: self.order,
stereo: Arc::from(stereo),
sample_rate: self.sample_rate,
bit_depth: new_depth,
gain: self.gain,
})
} }
} }
@@ -232,26 +369,43 @@ mod tests {
#[test] #[test]
fn test_audio_chunk_creation() { fn test_audio_chunk_creation() {
let left = vec![0.0, 0.1, 0.2]; let stereo: Vec<[i32; 2]> = vec![
let right = vec![0.0, 0.1, 0.2]; [0, 10], // frame 0 : L=0, R=10
let chunk = AudioChunk::new(0, left, right, 48000); [20, 30], // frame 1 : L=20, R=30
[40, 50], // frame 2 : L=40, R=50
];
let chunk = AudioChunk::new(0, stereo, 48000, BitDepth::B24);
assert_eq!(chunk.order, 0); assert_eq!(chunk.order(), 0);
assert_eq!(chunk.len(), 3); assert_eq!(chunk.len(), 3);
assert_eq!(chunk.sample_rate, 48000); assert_eq!(chunk.sample_rate(), 48000);
assert!(!chunk.is_empty()); assert!(!chunk.is_empty());
} }
}
#[test] fn quantize_sample(sample: f32, bit_depth: BitDepth) -> i32 {
fn test_audio_chunk_arc_sharing() { let max_value = bit_depth.max_value() as f64;
let left = Arc::new(vec![0.0, 0.1, 0.2]); let upper = max_value - 1.0;
let right = Arc::new(vec![0.0, 0.1, 0.2]); let lower = -max_value;
let scaled = (sample as f64 * upper).round();
scaled.clamp(lower, upper) as i32
}
let chunk1 = AudioChunk::from_arc(0, left.clone(), right.clone(), 48000); fn dequantize_sample(sample: i32, bit_depth: BitDepth) -> f32 {
let chunk2 = chunk1.clone(); let max_value = bit_depth.max_value();
sample as f32 / max_value
}
// Vérifier que les Arc pointent vers les mêmes données const MIN_GAIN_DB: f64 = -120.0;
assert!(Arc::ptr_eq(&chunk1.left, &chunk2.left));
assert!(Arc::ptr_eq(&chunk1.right, &chunk2.right)); fn linear_to_db(gain_linear: f64) -> f64 {
if gain_linear <= 0.0 {
MIN_GAIN_DB
} else {
(20.0 * gain_linear.log10()).max(MIN_GAIN_DB)
} }
} }
fn db_to_linear(gain_db: f64) -> f64 {
10f64.powf(gain_db / 20.0)
}

162
pmoaudio/src/bit_depth.rs Normal file
View File

@@ -0,0 +1,162 @@
//! Bit depth abstraction for audio processing.
//!
//! Provides both compile-time generic types (`Bit8`, `Bit16`, …)
//! and a dynamic `BitDepth` enum for runtime selection.
use std::fmt;
/// Trait implemented by compile-time bit-depth marker types.
pub trait BitDepthType {
const BITS: u32;
const MAX_VALUE: f32;
}
/// Compile-time bit depth markers
#[derive(Clone, Copy, Debug)]
pub struct Bit8;
#[derive(Clone, Copy, Debug)]
pub struct Bit16;
#[derive(Clone, Copy, Debug)]
pub struct Bit24;
#[derive(Clone, Copy, Debug)]
pub struct Bit32;
impl BitDepthType for Bit8 {
const BITS: u32 = 8;
const MAX_VALUE: f32 = 128.0;
}
impl BitDepthType for Bit16 {
const BITS: u32 = 16;
const MAX_VALUE: f32 = 32_768.0;
}
impl BitDepthType for Bit24 {
const BITS: u32 = 24;
const MAX_VALUE: f32 = 8_388_608.0;
}
impl BitDepthType for Bit32 {
const BITS: u32 = 32;
const MAX_VALUE: f32 = 2_147_483_648.0;
}
/// Runtime bit-depth descriptor.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BitDepth {
B8,
B16,
B24,
B32,
}
impl BitDepth {
/// Returns the number of bits.
#[inline(always)]
pub const fn bits(self) -> u32 {
match self {
BitDepth::B8 => 8,
BitDepth::B16 => 16,
BitDepth::B24 => 24,
BitDepth::B32 => 32,
}
}
/// Returns the full-scale signed maximum value as `f32`.
#[inline(always)]
pub const fn max_value(self) -> f32 {
match self {
BitDepth::B8 => 128.0,
BitDepth::B16 => 32_768.0,
BitDepth::B24 => 8_388_608.0,
BitDepth::B32 => 2_147_483_648.0,
}
}
/// Create from bit count, returning `None` if unsupported.
#[inline(always)]
pub const fn from_u32(bits: u32) -> Option<Self> {
match bits {
8 => Some(Self::B8),
16 => Some(Self::B16),
24 => Some(Self::B24),
32 => Some(Self::B32),
_ => None,
}
}
/// Create from bit count, panicking if unsupported (non-const).
#[inline(always)]
pub fn from_u32_strict(bits: u32) -> Self {
Self::from_u32(bits).unwrap_or_else(|| panic!("Unsupported bit depth: {}", bits))
}
}
impl fmt::Display for BitDepth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-bit", self.bits())
}
}
/// Comparaisons dordre fondées sur la valeur en bits.
impl PartialOrd for BitDepth {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BitDepth {
#[inline(always)]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.bits().cmp(&other.bits())
}
}
/// Bridge between dynamic [`BitDepth`] and static [`BitDepthType`] markers.
///
/// Example:
/// ```
/// use pmoaudio::{
/// bit_depth::dispatch_by_bitdepth, Bit8, Bit16, Bit24, Bit32, BitDepth,
/// };
/// use pmoaudio::bit_depth::BitDepthType;
///
/// fn type_bits<B: BitDepthType>() -> u32 {
/// B::BITS
/// }
///
/// let depth = BitDepth::B16;
/// let bits = dispatch_by_bitdepth(
/// depth,
/// || type_bits::<Bit8>(),
/// || type_bits::<Bit16>(),
/// || type_bits::<Bit24>(),
/// || type_bits::<Bit32>(),
/// );
/// assert_eq!(bits, 16);
/// ```
#[inline(always)]
pub fn dispatch_by_bitdepth<R, F8, F16, F24, F32>(
depth: BitDepth,
f8: F8,
f16: F16,
f24: F24,
f32: F32,
) -> R
where
F8: FnOnce() -> R,
F16: FnOnce() -> R,
F24: FnOnce() -> R,
F32: FnOnce() -> R,
{
match depth {
BitDepth::B8 => f8(),
BitDepth::B16 => f16(),
BitDepth::B24 => f24(),
BitDepth::B32 => f32(),
}
}
/// Conversion helper from a runtime [`BitDepth`] to a compile-time constant.
#[inline(always)]
pub fn max_value_for(depth: BitDepth) -> f32 {
depth.max_value()
}

111
pmoaudio/src/dsp/depth.rs Normal file
View File

@@ -0,0 +1,111 @@
#[cfg(feature = "simd")]
use std::simd::prelude::*;
#[cfg(feature = "simd")]
use std::simd::Simd;
use crate::BitDepth;
#[inline(always)]
pub fn bitdepth_change_stereo(data: &mut [[i32; 2]], source_bits: BitDepth, dest_bits: BitDepth) {
use std::cmp::Ordering::*;
let obits = dest_bits.bits();
let ibits = source_bits.bits();
match source_bits.cmp(&dest_bits) {
Less => bitdepth_up_stereo(data, (obits - ibits) as i32),
Greater => bitdepth_down_stereo(data, (ibits - obits) as i32, obits),
Equal => (),
}
}
#[inline(always)]
#[cfg(feature = "simd")]
fn bitdepth_up_stereo(data: &mut [[i32; 2]], shift: i32) {
const LANES: usize = 8;
let shift_vec = Simd::<i32, LANES>::splat(shift);
// On traite 8 frames stéréo à la fois
let (chunks, remainder) = data.as_chunks_mut::<LANES>();
for blk in chunks {
// Séparer L et R localement (petit tableau sur la pile)
let mut l = [0i32; LANES];
let mut r = [0i32; LANES];
for j in 0..LANES {
let s = blk[j];
l[j] = s[0];
r[j] = s[1];
}
// SIMD
let vl = Simd::<i32, LANES>::from_array(l) << shift_vec;
let vr = Simd::<i32, LANES>::from_array(r) << shift_vec;
// Écrire
for j in 0..LANES {
blk[j] = [vl[j], vr[j]];
}
}
// Reste scalaire
for f in remainder {
f[0] <<= shift;
f[1] <<= shift;
}
}
#[inline(always)]
#[cfg(not(feature = "simd"))]
fn bitdepth_up_stereo(data: &mut [[i32; 2]], shift: i32) {
for frame in data.iter_mut() {
frame[0] <<= shift;
frame[1] <<= shift;
}
}
#[inline(always)]
#[cfg(feature = "simd")]
fn bitdepth_down_stereo(data: &mut [[i32; 2]], shift: i32, dest_bits: u32) {
const LANES: usize = 8;
let shift_vec = Simd::<i32, LANES>::splat(shift);
let maxv = Simd::<i32, LANES>::splat(((1i64 << (dest_bits - 1)) - 1) as i32);
let minv = Simd::<i32, LANES>::splat((-(1i64 << (dest_bits - 1))) as i32);
let (chunks, remainder) = data.as_chunks_mut::<LANES>();
for blk in chunks {
let mut l = [0i32; LANES];
let mut r = [0i32; LANES];
for j in 0..LANES {
l[j] = blk[j][0];
r[j] = blk[j][1];
}
let vl = Simd::<i32, LANES>::from_array(l);
let vr = Simd::<i32, LANES>::from_array(r);
let lq = (vl >> shift_vec).simd_clamp(minv, maxv);
let rq = (vr >> shift_vec).simd_clamp(minv, maxv);
for j in 0..LANES {
blk[j] = [lq[j], rq[j]];
}
}
// Reste scalaire
for f in remainder {
f[0] = ((*f)[0] as i64 >> shift)
.clamp(-(1i64 << (dest_bits - 1)), (1i64 << (dest_bits - 1)) - 1) as i32;
f[1] = ((*f)[1] as i64 >> shift)
.clamp(-(1i64 << (dest_bits - 1)), (1i64 << (dest_bits - 1)) - 1) as i32;
}
}
#[inline(always)]
#[cfg(not(feature = "simd"))]
fn bitdepth_down_stereo(data: &mut [[i32; 2]], shift: i32, dest_bits: u32) {
let maxv = (1i64 << (dest_bits - 1)) - 1;
let minv = -(1i64 << (dest_bits - 1));
for frame in data.iter_mut() {
frame[0] = ((frame[0] as i64 >> shift).clamp(minv, maxv)) as i32;
frame[1] = ((frame[1] as i64 >> shift).clamp(minv, maxv)) as i32;
}
}

93
pmoaudio/src/dsp/gain.rs Normal file
View File

@@ -0,0 +1,93 @@
/// 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) {
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);
return;
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
unsafe {
apply_gain_stereo_avx2(samples, g_q31);
return;
}
// Fallback scalar
#[cfg(not(any(
all(target_arch = "aarch64", target_feature = "neon"),
all(target_arch = "x86_64", target_feature = "avx2")
)))]
{
apply_gain_stereo_scalar(samples, g_q31);
}
}
#[cfg(not(any(
all(target_arch = "aarch64", target_feature = "neon"),
all(target_arch = "x86_64", target_feature = "avx2")
)))]
#[inline(always)]
fn apply_gain_stereo_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;
frame[0] = prod_l.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
// R
let prod_r = (frame[1] as i64 * g_q31 as i64 + (1 << 30)) >> 31;
frame[1] = prod_r.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
}
}
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
#[inline(always)]
unsafe fn apply_gain_stereo_neon(samples: &mut [[i32; 2]], g_q31: i32) {
use core::arch::aarch64::*;
let gvec = vdupq_n_s32(g_q31);
let mut i = 0;
let n = samples.len() * 2; // total d'échantillons (L+R)
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); // Q31 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_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) {
use core::arch::x86_64::*;
let g = _mm256_set1_epi32(g_q31);
let mut i = 0;
let n = samples.len() * 2; // total d'échantillons
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_scalar(slice, g_q31);
}
/// version mono utilisée pour le reste scalaire
#[inline(always)]
fn apply_gain_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;
}
}

View File

@@ -0,0 +1,188 @@
use bytemuck::{cast_slice, cast_slice_mut};
#[cfg(feature = "simd")]
use std::simd::num::{SimdFloat, SimdInt};
#[cfg(feature = "simd")]
use std::simd::{Simd, StdFloat};
/// Génère une implémentation de `BitDepth` pour une profondeur donnée.
/// Exemple :
/// ```ignore
/// use pmoaudio::dsp::int_float::{BitDepth, BitMax};
///
/// BitMax!(8);
/// assert_eq!(<Bit8 as BitDepth>::MAX_VALUE, 127.0);
/// ```
macro_rules! BitMax {
($bits:literal) => {
paste::paste! {
pub struct [<Bit $bits>];
impl BitDepth for [<Bit $bits>] {
const MAX_VALUE: f32 = ((1u32 << ($bits - 1)) as f32) - 1.0;
}
}
};
}
pub trait BitDepth {
const MAX_VALUE: f32; // Valeur max pour normaliser vers [-1.0, +1.0]
}
// Définir automatiquement les bit-depths
BitMax!(8);
BitMax!(16);
BitMax!(24);
BitMax!(32);
/* ====================== CŒURS CANONIQUES EN AoS ====================== */
// i32 L/R -> [[f32;2]]
#[cfg(feature = "simd")]
pub fn i32_stereo_to_pairs_f32<B: BitDepth>(
left: &[i32],
right: &[i32],
out_pairs: &mut [[f32; 2]],
) {
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 / B::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 {
// AoS direct
unsafe {
*o.get_unchecked_mut(j) = [l[j], r[j]];
}
}
}
for (dst, (&l, &r)) in o_tail.iter_mut().zip(l_tail.iter().zip(r_tail.iter())) {
dst[0] = l as f32 * (1.0 / B::MAX_VALUE);
dst[1] = r as f32 * (1.0 / B::MAX_VALUE);
}
}
#[cfg(not(feature = "simd"))]
pub fn i32_stereo_to_pairs_f32<B: BitDepth>(
left: &[i32],
right: &[i32],
out_pairs: &mut [[f32; 2]],
) {
debug_assert_eq!(left.len(), right.len());
debug_assert_eq!(out_pairs.len(), left.len());
let scale = 1.0 / B::MAX_VALUE;
for ((out, &l), &r) in out_pairs.iter_mut().zip(left).zip(right) {
out[0] = l as f32 * scale;
out[1] = r as f32 * scale;
}
}
// [[f32;2]] -> i32 L/R
#[cfg(feature = "simd")]
pub fn pairs_f32_to_i32_stereo<B: BitDepth>(
input_pairs: &[[f32; 2]],
left: &mut [i32],
right: &mut [i32],
) {
debug_assert_eq!(input_pairs.len(), left.len());
debug_assert_eq!(input_pairs.len(), right.len());
const LANES: usize = 8;
type Vf32 = Simd<f32, LANES>;
type Vi32 = Simd<i32, LANES>;
let vmax = B::MAX_VALUE;
let vmin = -B::MAX_VALUE;
let vscale = Vf32::splat(vmax);
let vminv = Vf32::splat(vmin);
let vmaxv = Vf32::splat(vmax - 1.0); // évite loverflow après round→cast
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() {
// AoS → deux vecteurs f32
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] * vmax).clamp(vmin, vmax - 1.0).round();
let rx = (j[1] * vmax).clamp(vmin, vmax - 1.0).round();
*l = lx as i32;
*r = rx as i32;
}
}
#[cfg(not(feature = "simd"))]
pub fn pairs_f32_to_i32_stereo<B: BitDepth>(
input_pairs: &[[f32; 2]],
left: &mut [i32],
right: &mut [i32],
) {
debug_assert_eq!(input_pairs.len(), left.len());
debug_assert_eq!(input_pairs.len(), right.len());
let vmax = B::MAX_VALUE;
let vmin = -B::MAX_VALUE;
for (i, pair) in input_pairs.iter().enumerate() {
let lx = (pair[0] * vmax).clamp(vmin, vmax - 1.0).round();
let rx = (pair[1] * vmax).clamp(vmin, vmax - 1.0).round();
left[i] = lx as i32;
right[i] = rx as i32;
}
}
/* ====================== WRAPPERS INTERLEAVÉS ====================== */
// i32 L/R -> interleaved [f32]
pub fn i32_stereo_to_interleaved_f32<B: BitDepth>(
left: &[i32],
right: &[i32],
out_interleaved: &mut [f32],
) {
debug_assert_eq!(out_interleaved.len(), left.len() * 2);
let out_pairs: &mut [[f32; 2]] = cast_slice_mut(out_interleaved);
i32_stereo_to_pairs_f32::<B>(left, right, out_pairs);
}
// interleaved [f32] -> i32 L/R
pub fn interleaved_f32_to_i32_stereo<B: BitDepth>(
input_interleaved: &[f32],
left: &mut [i32],
right: &mut [i32],
) {
debug_assert_eq!(input_interleaved.len(), left.len() * 2);
let input_pairs: &[[f32; 2]] = cast_slice(input_interleaved);
pairs_f32_to_i32_stereo::<B>(input_pairs, left, right);
}

13
pmoaudio/src/dsp/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
pub mod depth;
pub mod gain;
pub mod int_float;
pub mod resampling;
pub use depth::bitdepth_change_stereo;
pub use gain::apply_gain_stereo;
pub use int_float::{
i32_stereo_to_interleaved_f32, i32_stereo_to_pairs_f32, interleaved_f32_to_i32_stereo,
pairs_f32_to_i32_stereo,
};
pub use resampling::resampling;

View File

@@ -0,0 +1,74 @@
use soxr::format::Stereo;
use soxr::params::{QualityRecipe, QualitySpec, RuntimeSpec};
use soxr::Soxr;
use crate::dsp::int_float::{Bit16, Bit24, Bit32, Bit8};
use crate::dsp::{i32_stereo_to_pairs_f32, pairs_f32_to_i32_stereo};
use crate::AudioError;
pub struct Resampler {
source_hz: f64,
dest_hz: f64,
bit_depth: u32,
soxr: Soxr<Stereo<f32>>,
}
pub fn build_resampler(
source_hz: u32,
dest_hz: u32,
bit_depth: u32,
) -> Result<Resampler, AudioError> {
let qrecipe = match bit_depth {
8 => QualityRecipe::Medium,
16 => QualityRecipe::high(), // High plutôt que Bits16 pour 16-bit
24 => QualityRecipe::very_high(), // VeryHigh pour 24-bit
32 => QualityRecipe::very_high(), // VeryHigh pour 32-bit
_ => unreachable!(), // Déjà vérifié plus haut
};
let quality = QualitySpec::new(qrecipe); // Phase response linear, no steep filter
let rt = RuntimeSpec::default();
let soxr = Soxr::<Stereo<f32>>::new_with_params(source_hz as f64, dest_hz as f64, quality, rt)
.map_err(|e| AudioError::ProcessingError(e.to_string()))?;
Ok(Resampler {
source_hz: source_hz as f64,
dest_hz: dest_hz as f64,
bit_depth: bit_depth,
soxr: soxr,
})
}
pub fn resampling(left: &[i32], right: &[i32], resampler: &mut Resampler) -> (Vec<i32>, Vec<i32>) {
if left.len() != right.len() {
panic!("Left and right channels must have the same length");
}
let mut input = vec![[0.0f32; 2]; left.len()];
match resampler.bit_depth {
8 => i32_stereo_to_pairs_f32::<Bit8>(left, right, &mut input),
16 => i32_stereo_to_pairs_f32::<Bit16>(left, right, &mut input),
24 => i32_stereo_to_pairs_f32::<Bit24>(left, right, &mut input),
32 => i32_stereo_to_pairs_f32::<Bit32>(left, right, &mut input),
_ => panic!("Unsupported bit depth: {}", resampler.bit_depth),
}
let output_len =
((input.len() as f64) * resampler.dest_hz / resampler.source_hz).ceil() as usize;
let mut output = vec![[0.0f32; 2]; output_len];
resampler.soxr.process(&input, &mut output).unwrap();
let mut oleft = vec![0i32; output.len()];
let mut oright = vec![0i32; output.len()];
match resampler.bit_depth {
8 => pairs_f32_to_i32_stereo::<Bit8>(&output, &mut oleft, &mut oright),
16 => pairs_f32_to_i32_stereo::<Bit16>(&output, &mut oleft, &mut oright),
24 => pairs_f32_to_i32_stereo::<Bit24>(&output, &mut oleft, &mut oright),
32 => pairs_f32_to_i32_stereo::<Bit32>(&output, &mut oleft, &mut oright),
_ => unreachable!(), // Déjà vérifié plus haut
};
(oleft, oright)
}

View File

@@ -1,85 +1,95 @@
//! PMOAudio - Pipeline audio stéréo async optimisé #![cfg_attr(feature = "simd", feature(portable_simd))]
//! #![doc = r#"
//! Cette crate fournit un pipeline audio push-based async utilisant Tokio, PMOAudio - Pipeline audio stéréo async optimisé
//! optimisé pour minimiser les clonages de données via `Arc<Vec<f32>>`.
//! Cette crate fournit un pipeline audio push-based async utilisant Tokio,
//! # Architecture optimisé pour minimiser les clonages de données via `Arc<[[i32; 2]]>`.
//!
//! Le pipeline est composé de nodes asynchrones qui communiquent via des channels Tokio. # Architecture
//! Les données audio sont encapsulées dans des [`AudioChunk`] et partagées via `Arc` pour
//! éviter les copies inutiles. Le pipeline est composé de nodes asynchrones qui communiquent via des channels Tokio.
//! Les données audio sont encapsulées dans des [`AudioChunk`] et partagées via `Arc` pour
//! ## Pipeline type éviter les copies inutiles.
//!
//! ```text ## Pipeline type
//! SourceNode → DecoderNode → DSPNode → BufferNode → TimerNode → SinkNode(s)
//! ↓ ```text
//! Multiroom Sinks SourceNode → DecoderNode → DSPNode → BufferNode → TimerNode → SinkNode(s)
//! (avec offsets)
//! ``` Multiroom Sinks
//! (avec offsets)
//! # Exemples ```
//!
//! ## Pipeline simple # Exemples
//!
//! ```no_run ## Pipeline simple
//! use pmoaudio::{SinkNode, SourceNode, TimerNode};
//! ```no_run
//! #[tokio::main] use pmoaudio::{SinkNode, SourceNode, TimerNode};
//! async fn main() {
//! let (mut timer, timer_tx) = TimerNode::new(10); #[tokio::main]
//! let (sink, sink_tx) = SinkNode::new("Output".to_string(), 10); async fn main() {
//! let (mut timer, timer_tx) = TimerNode::new(10);
//! timer.add_subscriber(sink_tx); let (sink, sink_tx) = SinkNode::new("Output".to_string(), 10);
//!
//! tokio::spawn(async move { timer.run().await.unwrap() }); timer.add_subscriber(sink_tx);
//! let sink_handle = tokio::spawn(async move {
//! sink.run_with_stats().await.unwrap() tokio::spawn(async move { timer.run().await.unwrap() });
//! }); let sink_handle = tokio::spawn(async move {
//! sink.run_with_stats().await.unwrap()
//! tokio::spawn(async move { });
//! let mut source = SourceNode::new();
//! source.add_subscriber(timer_tx); tokio::spawn(async move {
//! source.generate_chunks(30, 4800, 48000, 440.0).await.unwrap(); let mut source = SourceNode::new();
//! }); source.add_subscriber(timer_tx);
//! source.generate_chunks(30, 4800, 48000, 440.0).await.unwrap();
//! sink_handle.await.unwrap(); });
//! }
//! ``` sink_handle.await.unwrap();
//! }
//! ## Configuration multiroom ```
//!
//! ```no_run ## Configuration multiroom
//! use pmoaudio::{BufferNode, SinkNode};
//! ```no_run
//! #[tokio::main] use pmoaudio::{BufferNode, SinkNode};
//! async fn main() {
//! let (buffer, buffer_tx) = BufferNode::new(50, 10); #[tokio::main]
//! async fn main() {
//! let (sink1, sink1_tx) = SinkNode::new("Room 1".to_string(), 10); let (buffer, buffer_tx) = BufferNode::new(50, 10);
//! let (sink2, sink2_tx) = SinkNode::new("Room 2".to_string(), 10);
//! let (sink1, sink1_tx) = SinkNode::new("Room 1".to_string(), 10);
//! // Room 1 sans délai, Room 2 avec 5 chunks de retard let (sink2, sink2_tx) = SinkNode::new("Room 2".to_string(), 10);
//! buffer.add_subscriber_with_offset(sink1_tx, 0).await;
//! buffer.add_subscriber_with_offset(sink2_tx, 5).await; // Room 1 sans délai, Room 2 avec 5 chunks de retard
//! buffer.add_subscriber_with_offset(sink1_tx, 0).await;
//! tokio::spawn(async move { buffer.run().await.unwrap() }); buffer.add_subscriber_with_offset(sink2_tx, 5).await;
//! // ... spawn sinks et source
//! } tokio::spawn(async move { buffer.run().await.unwrap() });
//! ``` // ... spawn sinks et source
//! }
//! # Optimisations ```
//!
//! - **Zero-copy** : Les [`AudioChunk`] sont partagés via `Arc`, seul le pointeur est cloné # Optimisations
//! - **Copy-on-Write** : Les nodes DSP clonent les données uniquement si modification nécessaire
//! - **Backpressure** : Channels bounded avec `try_send` pour éviter les blocages - **Zero-copy** : Les [`AudioChunk`] sont partagés via `Arc`, seul le pointeur est cloné
//! - **RwLock** : Pour partage concurrent du compteur [`TimerNode`] - **Copy-on-Write** : Les nodes DSP clonent les données uniquement si modification nécessaire
- **Backpressure** : Channels bounded avec `try_send` pour éviter les blocages
- **RwLock** : Pour partage concurrent du compteur [`TimerNode`]
"#]
#[cfg(feature = "simd")]
use std::simd::*;
mod audio_chunk; mod audio_chunk;
pub mod events; pub mod events;
mod nodes; mod nodes;
pub mod bit_depth;
pub mod dsp;
pub use audio_chunk::AudioChunk; pub use audio_chunk::AudioChunk;
pub use bit_depth::{Bit16, Bit24, Bit32, Bit8, BitDepth};
pub use events::{ pub use events::{
AudioDataEvent, EventPublisher, EventReceiver, NodeEvent, NodeListener, SourceNameUpdateEvent, AudioDataEvent, EventPublisher, EventReceiver, NodeEvent, NodeListener, SourceNameUpdateEvent,
VolumeChangeEvent, VolumeChangeEvent,
@@ -90,6 +100,8 @@ pub use nodes::{
decoder_node::DecoderNode, decoder_node::DecoderNode,
disk_sink::{AudioFileFormat, DiskSink, DiskSinkConfig, DiskSinkStats}, disk_sink::{AudioFileFormat, DiskSink, DiskSinkConfig, DiskSinkStats},
dsp_node::DspNode, dsp_node::DspNode,
file_source::FileSource,
flac_file_sink::{FlacFileSink, FlacFileSinkStats},
mpd_sink::{MpdAudioFormat, MpdConfig, MpdHandle, MpdSink, MpdStats}, mpd_sink::{MpdAudioFormat, MpdConfig, MpdHandle, MpdSink, MpdStats},
sink_node::{SinkNode, SinkStats}, sink_node::{SinkNode, SinkStats},
source_node::SourceNode, source_node::SourceNode,

View File

@@ -190,6 +190,7 @@ impl BufferNode {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::BitDepth;
#[tokio::test] #[tokio::test]
async fn test_buffer_node_basic() { async fn test_buffer_node_basic() {
@@ -205,14 +206,14 @@ mod tests {
// Envoyer des chunks // Envoyer des chunks
for i in 0..3 { for i in 0..3 {
let chunk = AudioChunk::new(i, vec![0.0; 100], vec![0.0; 100], 48000); let chunk = AudioChunk::new(i, vec![[0i32; 2]; 100], 48000, BitDepth::B24);
tx.send(Arc::new(chunk)).await.unwrap(); tx.send(chunk).await.unwrap();
} }
// Recevoir les chunks // Recevoir les chunks
for i in 0..3 { for i in 0..3 {
let chunk = out_rx.recv().await.unwrap(); let chunk = out_rx.recv().await.unwrap();
assert_eq!(chunk.order, i); assert_eq!(chunk.order(), i);
} }
} }
@@ -231,14 +232,14 @@ mod tests {
// Envoyer 5 chunks // Envoyer 5 chunks
for i in 0..5 { for i in 0..5 {
let chunk = AudioChunk::new(i, vec![0.0; 100], vec![0.0; 100], 48000); let chunk = AudioChunk::new(i, vec![[0i32; 2]; 100], 48000, BitDepth::B24);
tx.send(Arc::new(chunk)).await.unwrap(); tx.send(chunk).await.unwrap();
} }
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
// L'abonné devrait recevoir les chunks 0, 1, 2 (avec 2 chunks de retard) // L'abonné devrait recevoir les chunks 0, 1, 2 (avec 2 chunks de retard)
let chunk = out_rx.try_recv().unwrap(); let chunk = out_rx.try_recv().unwrap();
assert_eq!(chunk.order, 0); assert_eq!(chunk.order(), 0);
} }
} }

View File

@@ -194,10 +194,10 @@ impl ChromecastSink {
// Boucle principale // Boucle principale
while let Some(chunk) = self.rx.recv().await { while let Some(chunk) = self.rx.recv().await {
// Appliquer le gain si nécessaire // Appliquer le gain si nécessaire
let chunk_to_send = if (chunk.gain - 1.0).abs() > f32::EPSILON { let chunk_to_send = if chunk.gain_db().abs() > f64::EPSILON {
chunk.apply_gain() Arc::clone(&chunk).apply_gain()
} else { } else {
(*chunk).clone() Arc::clone(&chunk)
}; };
// Envoyer au Chromecast // Envoyer au Chromecast
@@ -238,7 +238,7 @@ impl ChromecastStats {
pub fn record_chunk(&mut self, chunk: &AudioChunk) { pub fn record_chunk(&mut self, chunk: &AudioChunk) {
self.chunks_sent += 1; self.chunks_sent += 1;
self.total_samples += chunk.len() as u64; self.total_samples += chunk.len() as u64;
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate as f64; self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
} }
pub fn finalize(&mut self) { pub fn finalize(&mut self) {
@@ -257,7 +257,10 @@ impl ChromecastStats {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::i32;
use super::*; use super::*;
use crate::BitDepth;
#[tokio::test] #[tokio::test]
async fn test_chromecast_sink_basic() { async fn test_chromecast_sink_basic() {
@@ -273,8 +276,9 @@ mod tests {
// Envoyer quelques chunks // Envoyer quelques chunks
for i in 0..5 { for i in 0..5 {
let chunk = AudioChunk::new(i, vec![0.5; 1000], vec![0.5; 1000], 48000); let stereo = vec![[i32::MAX / 2; 2]; 1000];
tx.send(Arc::new(chunk)).await.unwrap(); let chunk = AudioChunk::new(i, stereo, 48000, BitDepth::B24);
tx.send(chunk).await.unwrap();
} }
drop(tx); drop(tx);

View File

@@ -40,41 +40,45 @@ impl DecoderNode {
/// Mode mock décodage - simule un changement de sample rate /// Mode mock décodage - simule un changement de sample rate
pub async fn run_with_resampling(mut self, target_sample_rate: u32) -> Result<(), AudioError> { pub async fn run_with_resampling(mut self, target_sample_rate: u32) -> Result<(), AudioError> {
while let Some(chunk) = self.rx.recv().await { while let Some(chunk) = self.rx.recv().await {
if chunk.sample_rate == target_sample_rate { if chunk.sample_rate() == target_sample_rate {
// Pas besoin de resampling // Pas besoin de resampling
self.subscribers.push(chunk).await?; self.subscribers.push(chunk).await?;
} else { } else {
// Simuler un resampling (mock simple) // Simuler un resampling (mock simple)
let ratio = target_sample_rate as f64 / chunk.sample_rate as f64; let ratio = target_sample_rate as f64 / chunk.sample_rate() as f64;
let new_len = (chunk.len() as f64 * ratio) as usize; let new_len = (chunk.len() as f64 * ratio) as usize;
let (left_data, right_data) = chunk.clone_data(); let pairs = chunk.to_pairs_f32();
let mut new_left = Vec::with_capacity(new_len); let mut resampled = Vec::with_capacity(new_len);
let mut new_right = Vec::with_capacity(new_len);
// Resampling linéaire simple (mock) // Resampling linéaire simple (mock)
for i in 0..new_len { for i in 0..new_len {
let src_pos = i as f64 / ratio; let src_pos = i as f64 / ratio;
let src_idx = src_pos as usize; let src_idx = src_pos as usize;
if src_idx < left_data.len() - 1 { if src_idx + 1 < pairs.len() {
let frac = src_pos - src_idx as f64; let frac = src_pos - src_idx as f64;
let left_sample = left_data[src_idx] * (1.0 - frac as f32) let alpha = (1.0 - frac) as f32;
+ left_data[src_idx + 1] * frac as f32; let beta = frac as f32;
let right_sample = right_data[src_idx] * (1.0 - frac as f32) let left_sample = pairs[src_idx][0] * alpha + pairs[src_idx + 1][0] * beta;
+ right_data[src_idx + 1] * frac as f32; let right_sample = pairs[src_idx][1] * alpha + pairs[src_idx + 1][1] * beta;
new_left.push(left_sample); resampled.push([left_sample, right_sample]);
new_right.push(right_sample); } else if src_idx < pairs.len() {
} else if src_idx < left_data.len() { resampled.push(pairs[src_idx]);
new_left.push(left_data[src_idx]);
new_right.push(right_data[src_idx]);
} }
} }
let new_chunk = let mut new_chunk = AudioChunk::from_pairs_f32(
AudioChunk::new(chunk.order, new_left, new_right, target_sample_rate); chunk.order(),
self.subscribers.push(Arc::new(new_chunk)).await?; resampled,
target_sample_rate,
chunk.bit_depth(),
);
if chunk.gain_db().abs() > f64::EPSILON {
new_chunk = new_chunk.set_gain_db(chunk.gain_db());
}
self.subscribers.push(new_chunk).await?;
} }
} }
Ok(()) Ok(())
@@ -84,6 +88,7 @@ impl DecoderNode {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::BitDepth;
#[tokio::test] #[tokio::test]
async fn test_decoder_passthrough() { async fn test_decoder_passthrough() {
@@ -97,13 +102,18 @@ mod tests {
}); });
// Envoyer un chunk // Envoyer un chunk
let chunk = AudioChunk::new(0, vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0], 48000); let chunk = AudioChunk::from_channels_f32(
let chunk_arc = Arc::new(chunk); 0,
tx.send(chunk_arc.clone()).await.unwrap(); vec![1.0, 2.0, 3.0],
vec![4.0, 5.0, 6.0],
48000,
BitDepth::B24,
);
tx.send(chunk.clone()).await.unwrap();
// Recevoir le chunk // Recevoir le chunk
let received = out_rx.recv().await.unwrap(); let received = out_rx.recv().await.unwrap();
assert!(Arc::ptr_eq(&chunk_arc, &received)); assert!(Arc::ptr_eq(&chunk, &received));
} }
#[tokio::test] #[tokio::test]
@@ -118,12 +128,13 @@ mod tests {
}); });
// Envoyer un chunk à 48000 Hz // Envoyer un chunk à 48000 Hz
let chunk = AudioChunk::new(0, vec![1.0; 100], vec![1.0; 100], 48000); let chunk =
tx.send(Arc::new(chunk)).await.unwrap(); AudioChunk::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
tx.send(chunk).await.unwrap();
// Recevoir le chunk resampleé // Recevoir le chunk resampleé
let received = out_rx.recv().await.unwrap(); let received = out_rx.recv().await.unwrap();
assert_eq!(received.sample_rate, 96000); assert_eq!(received.sample_rate(), 96000);
// Le chunk devrait être environ 2x plus grand // Le chunk devrait être environ 2x plus grand
assert!(received.len() > 150 && received.len() < 250); assert!(received.len() > 150 && received.len() < 250);
} }
@@ -140,12 +151,12 @@ mod tests {
}); });
// Envoyer un chunk déjà au bon sample rate // Envoyer un chunk déjà au bon sample rate
let chunk = AudioChunk::new(0, vec![1.0; 100], vec![1.0; 100], 48000); let chunk =
let chunk_arc = Arc::new(chunk); AudioChunk::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
tx.send(chunk_arc.clone()).await.unwrap(); tx.send(chunk.clone()).await.unwrap();
// Le chunk devrait être passé sans modification // Le chunk devrait être passé sans modification
let received = out_rx.recv().await.unwrap(); let received = out_rx.recv().await.unwrap();
assert!(Arc::ptr_eq(&chunk_arc, &received)); assert!(Arc::ptr_eq(&chunk, &received));
} }
} }

View File

@@ -220,10 +220,10 @@ impl DiskSink {
} }
// Appliquer le gain avant l'écriture // Appliquer le gain avant l'écriture
let chunk_with_gain = if (chunk.gain - 1.0).abs() > f32::EPSILON { let chunk_with_gain = if chunk.gain_db().abs() > f64::EPSILON {
chunk.apply_gain() Arc::clone(&chunk).apply_gain()
} else { } else {
(*chunk).clone() Arc::clone(&chunk)
}; };
// Écrire le chunk // Écrire le chunk
@@ -308,26 +308,25 @@ impl AudioFileWriter {
async fn write_chunk(&mut self, chunk: &AudioChunk) -> Result<(), AudioError> { async fn write_chunk(&mut self, chunk: &AudioChunk) -> Result<(), AudioError> {
// Enregistrer le sample rate du premier chunk // Enregistrer le sample rate du premier chunk
if self.sample_rate.is_none() { if self.sample_rate.is_none() {
self.sample_rate = Some(chunk.sample_rate); let sr = chunk.sample_rate();
self.sample_rate = Some(sr);
// Pour WAV, écrire l'en-tête (simplifié) // Pour WAV, écrire l'en-tête (simplifié)
if matches!(self.format, AudioFileFormat::Wav) { if matches!(self.format, AudioFileFormat::Wav) {
self.write_wav_header(chunk.sample_rate).await?; self.write_wav_header(sr).await?;
} }
} }
// Entrelacer les canaux gauche et droit
let mut interleaved = Vec::with_capacity(chunk.len() * 2);
for i in 0..chunk.len() {
interleaved.push(chunk.left[i]);
interleaved.push(chunk.right[i]);
}
// Convertir en bytes (little-endian 16-bit PCM) // Convertir en bytes (little-endian 16-bit PCM)
let mut bytes = Vec::with_capacity(interleaved.len() * 2); let mut bytes = Vec::with_capacity(chunk.len() * 4);
for &sample in &interleaved { let max_val = chunk.bit_depth().max_value();
let sample_i16 = (sample.clamp(-1.0, 1.0) * 32767.0) as i16; for frame in chunk.frames() {
let left = (frame[0] as f32 / max_val).clamp(-1.0, 1.0);
let right = (frame[1] as f32 / max_val).clamp(-1.0, 1.0);
let sample_i16 = (left * 32767.0) as i16;
bytes.extend_from_slice(&sample_i16.to_le_bytes()); bytes.extend_from_slice(&sample_i16.to_le_bytes());
let sample_r16 = (right * 32767.0) as i16;
bytes.extend_from_slice(&sample_r16.to_le_bytes());
} }
self.file.write_all(&bytes).await.map_err(|e| { self.file.write_all(&bytes).await.map_err(|e| {
@@ -436,7 +435,7 @@ impl DiskSinkStats {
pub fn record_chunk(&mut self, chunk: &AudioChunk) { pub fn record_chunk(&mut self, chunk: &AudioChunk) {
self.chunks_written += 1; self.chunks_written += 1;
self.total_samples += chunk.len() as u64; self.total_samples += chunk.len() as u64;
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate as f64; self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
} }
pub fn finalize(&mut self) { pub fn finalize(&mut self) {
@@ -455,6 +454,7 @@ impl DiskSinkStats {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::BitDepth;
#[tokio::test] #[tokio::test]
async fn test_disk_sink_basic() { async fn test_disk_sink_basic() {
@@ -474,8 +474,14 @@ mod tests {
// Envoyer quelques chunks // Envoyer quelques chunks
for i in 0..5 { for i in 0..5 {
let chunk = AudioChunk::new(i, vec![0.5; 1000], vec![0.5; 1000], 48000); let chunk = AudioChunk::from_channels_f32(
tx.send(Arc::new(chunk)).await.unwrap(); i,
vec![0.5; 1000],
vec![0.5; 1000],
48000,
BitDepth::B24,
);
tx.send(chunk).await.unwrap();
} }
drop(tx); drop(tx);

View File

@@ -11,17 +11,17 @@ use tokio::sync::mpsc;
pub struct DspNode { pub struct DspNode {
rx: mpsc::Receiver<Arc<AudioChunk>>, rx: mpsc::Receiver<Arc<AudioChunk>>,
subscribers: MultiSubscriberNode, subscribers: MultiSubscriberNode,
gain: f32, gain_db: f32,
} }
impl DspNode { impl DspNode {
pub fn new(channel_size: usize, gain: f32) -> (Self, mpsc::Sender<Arc<AudioChunk>>) { pub fn new(channel_size: usize, gain_db: f32) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
let (tx, rx) = mpsc::channel(channel_size); let (tx, rx) = mpsc::channel(channel_size);
let node = Self { let node = Self {
rx, rx,
subscribers: MultiSubscriberNode::new(), subscribers: MultiSubscriberNode::new(),
gain, gain_db,
}; };
(node, tx) (node, tx)
@@ -34,33 +34,36 @@ impl DspNode {
/// Applique le gain aux chunks /// Applique le gain aux chunks
pub async fn run(mut self) -> Result<(), AudioError> { pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(chunk) = self.rx.recv().await { while let Some(chunk) = self.rx.recv().await {
if (self.gain - 1.0).abs() < f32::EPSILON { if self.gain_db.abs() < f32::EPSILON {
// Gain = 1.0, pas de transformation nécessaire // Gain = 0 dB, pas de transformation nécessaire
self.subscribers.push(chunk).await?; self.subscribers.push(chunk).await?;
} else { continue;
// Clone les données pour les modifier
let (mut left_data, mut right_data) = chunk.clone_data();
// Appliquer le gain
for sample in &mut left_data {
*sample *= self.gain;
}
for sample in &mut right_data {
*sample *= self.gain;
}
let new_chunk =
AudioChunk::new(chunk.order, left_data, right_data, chunk.sample_rate);
self.subscribers.push(Arc::new(new_chunk)).await?;
} }
let gain_linear = AudioChunk::gain_linear_from_db(self.gain_db as f64) as f32;
let mut pairs = chunk.to_pairs_f32();
for frame in &mut pairs {
frame[0] *= gain_linear;
frame[1] *= gain_linear;
}
let mut new_chunk = AudioChunk::from_pairs_f32(
chunk.order(),
pairs,
chunk.sample_rate(),
chunk.bit_depth(),
);
if chunk.gain_db().abs() > f64::EPSILON {
new_chunk = new_chunk.set_gain_db(chunk.gain_db());
}
self.subscribers.push(new_chunk).await?;
} }
Ok(()) Ok(())
} }
/// Met à jour le gain dynamiquement (nécessite un `Arc<RwLock<f32>>` dans une version réelle) /// Met à jour le gain dynamiquement (nécessite un `Arc<RwLock<f32>>` dans une version réelle)
pub fn set_gain(&mut self, gain: f32) { pub fn set_gain_db(&mut self, gain_db: f32) {
self.gain = gain; self.gain_db = gain_db;
} }
} }
@@ -102,24 +105,26 @@ impl LowPassDspNode {
#[allow(dead_code)] #[allow(dead_code)]
pub async fn run(mut self) -> Result<(), AudioError> { pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(chunk) = self.rx.recv().await { while let Some(chunk) = self.rx.recv().await {
let (left_data, right_data) = chunk.clone_data(); let pairs = chunk.to_pairs_f32();
let mut new_left = Vec::with_capacity(left_data.len()); let mut filtered = Vec::with_capacity(pairs.len());
let mut new_right = Vec::with_capacity(right_data.len());
// Appliquer le filtre for sample in pairs.iter() {
for &sample in &left_data { self.prev_left = self.prev_left + self.alpha * (sample[0] - self.prev_left);
self.prev_left = self.prev_left + self.alpha * (sample - self.prev_left); self.prev_right = self.prev_right + self.alpha * (sample[1] - self.prev_right);
new_left.push(self.prev_left); filtered.push([self.prev_left, self.prev_right]);
} }
for &sample in &right_data { let mut new_chunk = AudioChunk::from_pairs_f32(
self.prev_right = self.prev_right + self.alpha * (sample - self.prev_right); chunk.order(),
new_right.push(self.prev_right); filtered,
chunk.sample_rate(),
chunk.bit_depth(),
);
if chunk.gain_db().abs() > f64::EPSILON {
new_chunk = new_chunk.set_gain_db(chunk.gain_db());
} }
let new_chunk = AudioChunk::new(chunk.order, new_left, new_right, chunk.sample_rate); self.subscribers.push(new_chunk).await?;
self.subscribers.push(Arc::new(new_chunk)).await?;
} }
Ok(()) Ok(())
} }
@@ -128,10 +133,11 @@ impl LowPassDspNode {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::BitDepth;
#[tokio::test] #[tokio::test]
async fn test_dsp_node_unity_gain() { async fn test_dsp_node_unity_gain() {
let (mut node, tx) = DspNode::new(10, 1.0); let (mut node, tx) = DspNode::new(10, 0.0);
let (out_tx, mut out_rx) = mpsc::channel(10); let (out_tx, mut out_rx) = mpsc::channel(10);
node.add_subscriber(out_tx); node.add_subscriber(out_tx);
@@ -141,18 +147,24 @@ mod tests {
}); });
// Envoyer un chunk // Envoyer un chunk
let chunk = AudioChunk::new(0, vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0], 48000); let chunk = AudioChunk::from_channels_f32(
let chunk_arc = Arc::new(chunk); 0,
tx.send(chunk_arc.clone()).await.unwrap(); vec![0.25, 0.5, 0.75],
vec![0.1, 0.2, 0.3],
48000,
BitDepth::B24,
);
tx.send(chunk.clone()).await.unwrap();
// Avec gain = 1.0, le chunk ne devrait pas être cloné // Avec gain = 1.0, le chunk ne devrait pas être cloné
let received = out_rx.recv().await.unwrap(); let received = out_rx.recv().await.unwrap();
assert!(Arc::ptr_eq(&chunk_arc, &received)); assert!(Arc::ptr_eq(&chunk, &received));
} }
#[tokio::test] #[tokio::test]
async fn test_dsp_node_gain() { async fn test_dsp_node_gain() {
let (mut node, tx) = DspNode::new(10, 2.0); let gain_db = AudioChunk::gain_db_from_linear(2.0) as f32;
let (mut node, tx) = DspNode::new(10, gain_db);
let (out_tx, mut out_rx) = mpsc::channel(10); let (out_tx, mut out_rx) = mpsc::channel(10);
node.add_subscriber(out_tx); node.add_subscriber(out_tx);
@@ -162,17 +174,25 @@ mod tests {
}); });
// Envoyer un chunk // Envoyer un chunk
let chunk = AudioChunk::new(0, vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0], 48000); let chunk = AudioChunk::from_channels_f32(
tx.send(Arc::new(chunk)).await.unwrap(); 0,
vec![0.25, 0.5, 0.75],
vec![0.1, 0.2, 0.3],
48000,
BitDepth::B24,
);
tx.send(chunk).await.unwrap();
// Vérifier que le gain a été appliqué // Vérifier que le gain a été appliqué
let received = out_rx.recv().await.unwrap(); let received = out_rx.recv().await.unwrap();
assert_eq!(received.left[0], 2.0); let frames = received.to_pairs_f32();
assert_eq!(received.left[1], 4.0); const EPS: f32 = 1e-3;
assert_eq!(received.left[2], 6.0); assert!((frames[0][0] - 0.5).abs() < EPS);
assert_eq!(received.right[0], 8.0); assert!((frames[1][0] - 1.0).abs() < EPS);
assert_eq!(received.right[1], 10.0); assert!((frames[2][0] - 1.0).abs() < EPS); // Clamp at full scale
assert_eq!(received.right[2], 12.0); assert!((frames[0][1] - 0.2).abs() < EPS);
assert!((frames[1][1] - 0.4).abs() < EPS);
assert!((frames[2][1] - 0.6).abs() < EPS);
} }
#[tokio::test] #[tokio::test]
@@ -187,25 +207,26 @@ mod tests {
}); });
// Envoyer un chunk avec un signal carré // Envoyer un chunk avec un signal carré
let chunk = AudioChunk::new( let chunk = AudioChunk::from_channels_f32(
0, 0,
vec![1.0, 1.0, 1.0, -1.0, -1.0, -1.0], vec![1.0, 1.0, 1.0, -1.0, -1.0, -1.0],
vec![1.0, 1.0, 1.0, -1.0, -1.0, -1.0], vec![1.0, 1.0, 1.0, -1.0, -1.0, -1.0],
48000, 48000,
BitDepth::B24,
); );
tx.send(Arc::new(chunk)).await.unwrap(); tx.send(chunk).await.unwrap();
// Le filtre devrait lisser le signal // Le filtre devrait lisser le signal
let received = out_rx.recv().await.unwrap(); let received = out_rx.recv().await.unwrap();
let frames = received.to_pairs_f32();
// Vérifier que le signal est lissé (valeurs intermédiaires) assert!(frames[0][0].abs() < 1.0); // Premier échantillon lissé
assert!(received.left[0].abs() < 1.0); // Premier échantillon lissé assert!(frames[2][0].abs() < 1.0); // Signal ne devrait pas atteindre 1.0 immédiatement
assert!(received.left[2].abs() < 1.0); // Signal ne devrait pas atteindre 1.0 immédiatement
} }
#[tokio::test] #[tokio::test]
async fn test_dsp_node_multiple_subscribers() { async fn test_dsp_node_multiple_subscribers() {
let (mut node, tx) = DspNode::new(10, 0.5); let gain_db = AudioChunk::gain_db_from_linear(0.5) as f32;
let (mut node, tx) = DspNode::new(10, gain_db);
let (out_tx1, mut out_rx1) = mpsc::channel(10); let (out_tx1, mut out_rx1) = mpsc::channel(10);
let (out_tx2, mut out_rx2) = mpsc::channel(10); let (out_tx2, mut out_rx2) = mpsc::channel(10);
@@ -216,15 +237,18 @@ mod tests {
node.run().await.unwrap(); node.run().await.unwrap();
}); });
let chunk = AudioChunk::new(0, vec![2.0, 4.0], vec![2.0, 4.0], 48000); let chunk =
tx.send(Arc::new(chunk)).await.unwrap(); AudioChunk::from_channels_f32(0, vec![0.8, 0.4], vec![0.8, 0.4], 48000, BitDepth::B24);
tx.send(chunk).await.unwrap();
// Les deux abonnés devraient recevoir le même Arc // Les deux abonnés devraient recevoir le même Arc
let received1 = out_rx1.recv().await.unwrap(); let received1 = out_rx1.recv().await.unwrap();
let received2 = out_rx2.recv().await.unwrap(); let received2 = out_rx2.recv().await.unwrap();
assert!(Arc::ptr_eq(&received1, &received2)); assert!(Arc::ptr_eq(&received1, &received2));
assert_eq!(received1.left[0], 1.0); // 2.0 * 0.5 let frames = received1.to_pairs_f32();
assert_eq!(received1.left[1], 2.0); // 4.0 * 0.5 const EPS: f32 = 1e-3;
assert!((frames[0][0] - 0.4).abs() < EPS); // 0.8 * 0.5
assert!((frames[1][0] - 0.2).abs() < EPS); // 0.4 * 0.5
} }
} }

View File

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

View File

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

View File

@@ -12,6 +12,8 @@ pub mod chromecast_sink;
pub mod decoder_node; pub mod decoder_node;
pub mod disk_sink; pub mod disk_sink;
pub mod dsp_node; pub mod dsp_node;
pub mod file_source;
pub mod flac_file_sink;
pub mod mpd_sink; pub mod mpd_sink;
pub mod sink_node; pub mod sink_node;
pub mod source_node; pub mod source_node;

View File

@@ -243,10 +243,10 @@ impl MpdSink {
// Boucle principale // Boucle principale
while let Some(chunk) = self.rx.recv().await { while let Some(chunk) = self.rx.recv().await {
// Appliquer le gain si nécessaire // Appliquer le gain si nécessaire
let chunk_to_send = if (chunk.gain - 1.0).abs() > f32::EPSILON { let chunk_to_send = if chunk.gain_db().abs() > f64::EPSILON {
chunk.apply_gain() Arc::clone(&chunk).apply_gain()
} else { } else {
(*chunk).clone() Arc::clone(&chunk)
}; };
// Envoyer au serveur MPD // Envoyer au serveur MPD
@@ -329,7 +329,7 @@ impl MpdStats {
pub fn record_chunk(&mut self, chunk: &AudioChunk) { pub fn record_chunk(&mut self, chunk: &AudioChunk) {
self.chunks_sent += 1; self.chunks_sent += 1;
self.total_samples += chunk.len() as u64; self.total_samples += chunk.len() as u64;
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate as f64; self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
} }
pub fn finalize(&mut self) { pub fn finalize(&mut self) {
@@ -349,6 +349,7 @@ impl MpdStats {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::BitDepth;
#[tokio::test] #[tokio::test]
async fn test_mpd_sink_basic() { async fn test_mpd_sink_basic() {
@@ -364,8 +365,14 @@ mod tests {
// Envoyer quelques chunks // Envoyer quelques chunks
for i in 0..5 { for i in 0..5 {
let chunk = AudioChunk::new(i, vec![0.5; 1000], vec![0.5; 1000], 48000); let chunk = AudioChunk::from_channels_f32(
tx.send(Arc::new(chunk)).await.unwrap(); i,
vec![0.5; 1000],
vec![0.5; 1000],
48000,
BitDepth::B24,
);
tx.send(chunk).await.unwrap();
} }
drop(tx); drop(tx);

View File

@@ -33,9 +33,9 @@ impl SinkNode {
println!( println!(
"[{}] Received chunk #{} - {} samples @ {} Hz", "[{}] Received chunk #{} - {} samples @ {} Hz",
self.name, self.name,
chunk.order, chunk.order(),
chunk.len(), chunk.len(),
chunk.sample_rate chunk.sample_rate()
); );
} }
Ok(()) Ok(())
@@ -95,34 +95,41 @@ impl SinkStats {
pub fn process_chunk(&mut self, chunk: &AudioChunk) { pub fn process_chunk(&mut self, chunk: &AudioChunk) {
self.chunks_received += 1; self.chunks_received += 1;
self.total_samples += chunk.len() as u64; let len = chunk.len() as u64;
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate as f64; self.total_samples += len;
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
// Calculer les peaks let inv_max = 1.0f32 / chunk.bit_depth().max_value();
for &sample in chunk.left.iter() { let mut peak_left = self.peak_left;
if sample.abs() > self.peak_left { let mut peak_right = self.peak_right;
self.peak_left = sample.abs(); let mut sum_squares_left = 0.0f64;
let mut sum_squares_right = 0.0f64;
for frame in chunk.frames() {
let left = frame[0] as f32 * inv_max;
let right = frame[1] as f32 * inv_max;
let left_abs = left.abs();
let right_abs = right.abs();
if left_abs > peak_left {
peak_left = left_abs;
} }
if right_abs > peak_right {
peak_right = right_abs;
}
let l64 = left as f64;
let r64 = right as f64;
sum_squares_left += l64 * l64;
sum_squares_right += r64 * r64;
} }
for &sample in chunk.right.iter() { self.peak_left = peak_left;
if sample.abs() > self.peak_right { self.peak_right = peak_right;
self.peak_right = sample.abs();
}
}
// Calculer RMS (moyenne des carrés) let prev_samples = self.total_samples - len;
let sum_squares_left: f64 = chunk.left.iter().map(|&x| (x * x) as f64).sum(); self.rms_left = ((self.rms_left.powi(2) * prev_samples as f64 + sum_squares_left)
let sum_squares_right: f64 = chunk.right.iter().map(|&x| (x * x) as f64).sum();
self.rms_left = ((self.rms_left.powi(2)
* (self.total_samples - chunk.len() as u64) as f64
+ sum_squares_left)
/ self.total_samples as f64) / self.total_samples as f64)
.sqrt(); .sqrt();
self.rms_right = ((self.rms_right.powi(2) self.rms_right = ((self.rms_right.powi(2) * prev_samples as f64 + sum_squares_right)
* (self.total_samples - chunk.len() as u64) as f64
+ sum_squares_right)
/ self.total_samples as f64) / self.total_samples as f64)
.sqrt(); .sqrt();
} }
@@ -141,6 +148,9 @@ impl SinkStats {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::BitDepth;
const BD: BitDepth = BitDepth::B24;
#[tokio::test] #[tokio::test]
async fn test_sink_node_silent() { async fn test_sink_node_silent() {
@@ -150,8 +160,8 @@ mod tests {
// Envoyer quelques chunks // Envoyer quelques chunks
for i in 0..3 { for i in 0..3 {
let chunk = AudioChunk::new(i, vec![0.0; 100], vec![0.0; 100], 48000); let chunk = AudioChunk::from_channels_f32(i, vec![0.0; 100], vec![0.0; 100], 48000, BD);
tx.send(Arc::new(chunk)).await.unwrap(); tx.send(chunk).await.unwrap();
} }
drop(tx); drop(tx);
@@ -166,8 +176,9 @@ mod tests {
// Envoyer des chunks avec signal connu // Envoyer des chunks avec signal connu
for i in 0..3 { for i in 0..3 {
let chunk = AudioChunk::new(i, vec![1.0; 1000], vec![0.5; 1000], 48000); let chunk =
tx.send(Arc::new(chunk)).await.unwrap(); AudioChunk::from_channels_f32(i, vec![1.0; 1000], vec![0.5; 1000], 48000, BD);
tx.send(chunk).await.unwrap();
} }
drop(tx); drop(tx);
@@ -175,8 +186,8 @@ mod tests {
assert_eq!(stats.chunks_received, 3); assert_eq!(stats.chunks_received, 3);
assert_eq!(stats.total_samples, 3000); assert_eq!(stats.total_samples, 3000);
assert_eq!(stats.peak_left, 1.0); assert!((stats.peak_left - 1.0).abs() < 1e-6);
assert_eq!(stats.peak_right, 0.5); assert!((stats.peak_right - 0.5).abs() < 1e-6);
assert!((stats.rms_left - 1.0).abs() < 0.001); assert!((stats.rms_left - 1.0).abs() < 0.001);
assert!((stats.rms_right - 0.5).abs() < 0.001); assert!((stats.rms_right - 0.5).abs() < 0.001);
} }
@@ -189,8 +200,8 @@ mod tests {
// Envoyer des chunks // Envoyer des chunks
for i in 0..5 { for i in 0..5 {
let chunk = AudioChunk::new(i, vec![0.0; 100], vec![0.0; 100], 48000); let chunk = AudioChunk::from_channels_f32(i, vec![0.0; 100], vec![0.0; 100], 48000, BD);
tx.send(Arc::new(chunk)).await.unwrap(); tx.send(chunk).await.unwrap();
} }
drop(tx); drop(tx);

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
nodes::{AudioError, MultiSubscriberNode}, nodes::{AudioError, MultiSubscriberNode},
AudioChunk, AudioChunk, BitDepth,
}; };
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -12,6 +12,8 @@ pub struct SourceNode {
subscribers: MultiSubscriberNode, subscribers: MultiSubscriberNode,
} }
const DEFAULT_BIT_DEPTH: BitDepth = BitDepth::B24;
impl SourceNode { impl SourceNode {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -29,7 +31,7 @@ impl SourceNode {
size: usize, size: usize,
sample_rate: u32, sample_rate: u32,
frequency: f32, frequency: f32,
) -> AudioChunk { ) -> Arc<AudioChunk> {
let mut left = Vec::with_capacity(size); let mut left = Vec::with_capacity(size);
let mut right = Vec::with_capacity(size); let mut right = Vec::with_capacity(size);
@@ -40,7 +42,7 @@ impl SourceNode {
right.push(sample * 0.8); // Légèrement différent pour la stéréo right.push(sample * 0.8); // Légèrement différent pour la stéréo
} }
AudioChunk::new(order, left, right, sample_rate) AudioChunk::from_channels_f32(order, left, right, sample_rate, DEFAULT_BIT_DEPTH)
} }
/// Génère et envoie des chunks de test /// Génère et envoie des chunks de test
@@ -53,7 +55,7 @@ impl SourceNode {
) -> Result<(), AudioError> { ) -> Result<(), AudioError> {
for i in 0..count { for i in 0..count {
let chunk = Self::generate_test_chunk(i, chunk_size, sample_rate, frequency); let chunk = Self::generate_test_chunk(i, chunk_size, sample_rate, frequency);
self.subscribers.push(Arc::new(chunk)).await?; self.subscribers.push(chunk).await?;
} }
Ok(()) Ok(())
} }
@@ -66,9 +68,9 @@ impl SourceNode {
sample_rate: u32, sample_rate: u32,
) -> Result<(), AudioError> { ) -> Result<(), AudioError> {
for i in 0..count { for i in 0..count {
let chunk = let stereo = vec![[0i32; 2]; chunk_size];
AudioChunk::new(i, vec![0.0; chunk_size], vec![0.0; chunk_size], sample_rate); let chunk = AudioChunk::new(i, stereo, sample_rate, DEFAULT_BIT_DEPTH);
self.subscribers.push(Arc::new(chunk)).await?; self.subscribers.push(chunk).await?;
} }
Ok(()) Ok(())
} }
@@ -89,7 +91,7 @@ impl SourceNode {
while start.elapsed() < duration { while start.elapsed() < duration {
let chunk = Self::generate_test_chunk(order, chunk_size, sample_rate, frequency); let chunk = Self::generate_test_chunk(order, chunk_size, sample_rate, frequency);
self.subscribers.push(Arc::new(chunk)).await?; self.subscribers.push(chunk).await?;
order += 1; order += 1;
@@ -124,9 +126,9 @@ mod tests {
// Vérifier la réception // Vérifier la réception
for i in 0..3 { for i in 0..3 {
let chunk = rx.recv().await.unwrap(); let chunk = rx.recv().await.unwrap();
assert_eq!(chunk.order, i); assert_eq!(chunk.order(), i);
assert_eq!(chunk.len(), 100); assert_eq!(chunk.len(), 100);
assert_eq!(chunk.sample_rate, 48000); assert_eq!(chunk.sample_rate(), 48000);
} }
} }
@@ -136,7 +138,8 @@ mod tests {
// Vérifier qu'on a bien une sinusoïde // Vérifier qu'on a bien une sinusoïde
// À 440 Hz avec 48000 samples/s, on devrait avoir 440 cycles // À 440 Hz avec 48000 samples/s, on devrait avoir 440 cycles
let left = &*chunk.left; let pairs = chunk.to_pairs_f32();
let left: Vec<f32> = pairs.iter().map(|frame| frame[0]).collect();
// Trouver les passages par zéro // Trouver les passages par zéro
let mut zero_crossings = 0; let mut zero_crossings = 0;
@@ -161,8 +164,8 @@ mod tests {
for _ in 0..2 { for _ in 0..2 {
let chunk = rx.recv().await.unwrap(); let chunk = rx.recv().await.unwrap();
assert!(chunk.left.iter().all(|&x| x == 0.0)); assert!(chunk.frames().iter().all(|frame| frame[0] == 0));
assert!(chunk.right.iter().all(|&x| x == 0.0)); assert!(chunk.frames().iter().all(|frame| frame[1] == 0));
} }
} }
} }

View File

@@ -93,8 +93,8 @@ impl TimerNode {
// Mettre à jour le sample rate si nécessaire // Mettre à jour le sample rate si nécessaire
{ {
let mut sr = self.current_sample_rate.write().await; let mut sr = self.current_sample_rate.write().await;
if *sr != chunk.sample_rate { if *sr != chunk.sample_rate() {
*sr = chunk.sample_rate; *sr = chunk.sample_rate();
} }
} }
@@ -116,8 +116,8 @@ impl TimerNode {
while let Some(chunk) = self.rx.recv().await { while let Some(chunk) = self.rx.recv().await {
{ {
let mut sr = self.current_sample_rate.write().await; let mut sr = self.current_sample_rate.write().await;
if *sr != chunk.sample_rate { if *sr != chunk.sample_rate() {
*sr = chunk.sample_rate; *sr = chunk.sample_rate();
} }
} }
@@ -190,6 +190,7 @@ impl TimerHandle {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::BitDepth;
#[tokio::test] #[tokio::test]
async fn test_timer_node_position_calculation() { async fn test_timer_node_position_calculation() {
@@ -207,8 +208,9 @@ mod tests {
// Envoyer 3 chunks de 1000 samples à 48000 Hz // Envoyer 3 chunks de 1000 samples à 48000 Hz
for i in 0..3 { for i in 0..3 {
let chunk = AudioChunk::new(i, vec![0.0; 1000], vec![0.0; 1000], 48000); let stereo = vec![[0i32; 2]; 1000];
tx.send(Arc::new(chunk)).await.unwrap(); let chunk = AudioChunk::new(i, stereo, 48000, BitDepth::B24);
tx.send(chunk).await.unwrap();
} }
// Attendre que les chunks soient traités // Attendre que les chunks soient traités
@@ -237,16 +239,21 @@ mod tests {
}); });
// Envoyer un chunk // Envoyer un chunk
let chunk = AudioChunk::new(42, vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0], 48000); let chunk = AudioChunk::from_channels_i32(
let chunk_arc = Arc::new(chunk); 42,
tx.send(chunk_arc.clone()).await.unwrap(); vec![100, 200, 300],
vec![400, 500, 600],
48000,
BitDepth::B24,
);
tx.send(chunk.clone()).await.unwrap();
// Recevoir le chunk // Recevoir le chunk
let received = out_rx.recv().await.unwrap(); let received = out_rx.recv().await.unwrap();
// Vérifier que c'est le même Arc (pas de clone des données) // Vérifier que c'est le même Arc (pas de clone des données)
assert!(Arc::ptr_eq(&chunk_arc, &received)); assert!(Arc::ptr_eq(&chunk, &received));
assert_eq!(received.order, 42); assert_eq!(received.order(), 42);
} }
#[tokio::test] #[tokio::test]
@@ -263,8 +270,8 @@ mod tests {
}); });
// Chunk à 48000 Hz // Chunk à 48000 Hz
let chunk1 = AudioChunk::new(0, vec![0.0; 48000], vec![0.0; 48000], 48000); let chunk1 = AudioChunk::new(0, vec![[0i32; 2]; 48000], 48000, BitDepth::B24);
tx.send(Arc::new(chunk1)).await.unwrap(); tx.send(chunk1).await.unwrap();
out_rx.recv().await.unwrap(); out_rx.recv().await.unwrap();
// Après 48000 samples à 48000 Hz = 1 seconde // Après 48000 samples à 48000 Hz = 1 seconde
@@ -272,8 +279,8 @@ mod tests {
assert!((pos1 - 1.0).abs() < 0.0001); assert!((pos1 - 1.0).abs() < 0.0001);
// Chunk à 96000 Hz // Chunk à 96000 Hz
let chunk2 = AudioChunk::new(1, vec![0.0; 96000], vec![0.0; 96000], 96000); let chunk2 = AudioChunk::new(1, vec![[0i32; 2]; 96000], 96000, BitDepth::B24);
tx.send(Arc::new(chunk2)).await.unwrap(); tx.send(chunk2).await.unwrap();
out_rx.recv().await.unwrap(); out_rx.recv().await.unwrap();
// Position calculée avec le nouveau sample rate // Position calculée avec le nouveau sample rate

View File

@@ -126,13 +126,14 @@ impl VolumeNode {
match chunk_opt { match chunk_opt {
Some(chunk) => { Some(chunk) => {
let local_volume = *self.volume.read().await; let local_volume = *self.volume.read().await;
let total_volume = local_volume * master_volume; let total_volume = (local_volume * master_volume).max(0.0);
// Créer un nouveau chunk avec le gain modifié // Créer un nouveau chunk avec le gain modifié (conversion vers dB)
let modified_chunk = chunk.with_modified_gain(total_volume); let modified_chunk =
chunk.with_modified_gain_linear(total_volume as f64);
// Envoyer aux subscribers // Envoyer aux subscribers
self.subscribers.push(Arc::new(modified_chunk)).await?; self.subscribers.push(modified_chunk).await?;
} }
None => { None => {
// Channel fermé, terminer // Channel fermé, terminer
@@ -255,6 +256,7 @@ impl HardwareVolumeNode {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::BitDepth;
#[tokio::test] #[tokio::test]
async fn test_volume_node_basic() { async fn test_volume_node_basic() {
@@ -266,12 +268,13 @@ mod tests {
let handle = tokio::spawn(async move { node.run().await }); let handle = tokio::spawn(async move { node.run().await });
// Envoyer un chunk avec gain 1.0 // Envoyer un chunk avec gain 1.0
let chunk = AudioChunk::with_gain(0, vec![1.0; 100], vec![1.0; 100], 48000, 1.0); let chunk =
tx.send(Arc::new(chunk)).await.unwrap(); AudioChunk::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
tx.send(chunk).await.unwrap();
// Recevoir le chunk modifié // Recevoir le chunk modifié
let modified = out_rx.recv().await.unwrap(); let modified = out_rx.recv().await.unwrap();
assert!((modified.gain - 0.5).abs() < f32::EPSILON); assert!((modified.gain_linear() - 0.5).abs() < 1e-6);
drop(tx); drop(tx);
handle.await.unwrap().unwrap(); handle.await.unwrap().unwrap();
@@ -332,8 +335,9 @@ mod tests {
tokio::spawn(async move { slave.run().await }); tokio::spawn(async move { slave.run().await });
// Envoyer un chunk au slave // Envoyer un chunk au slave
let chunk = AudioChunk::with_gain(0, vec![1.0; 100], vec![1.0; 100], 48000, 1.0); let chunk =
slave_tx.send(Arc::new(chunk)).await.unwrap(); AudioChunk::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
slave_tx.send(chunk).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
@@ -343,14 +347,15 @@ mod tests {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
// Envoyer un autre chunk // Envoyer un autre chunk
let chunk2 = AudioChunk::with_gain(1, vec![1.0; 100], vec![1.0; 100], 48000, 1.0); let chunk2 =
slave_tx.send(Arc::new(chunk2)).await.unwrap(); AudioChunk::from_channels_f32(1, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
slave_tx.send(chunk2).await.unwrap();
// Le deuxième chunk devrait avoir un gain de 0.8 * 0.5 = 0.4 // Le deuxième chunk devrait avoir un gain de 0.8 * 0.5 = 0.4 (≈ -7.96 dB)
let _first = out_rx.recv().await.unwrap(); // gain = 0.8 let _first = out_rx.recv().await.unwrap(); // gain 0.8
let second = out_rx.recv().await.unwrap(); // gain = 0.4 let second = out_rx.recv().await.unwrap(); // gain 0.4
assert!((second.gain - 0.4).abs() < 0.01); assert!((second.gain_linear() - 0.4).abs() < 0.01);
drop(master_tx); drop(master_tx);
drop(slave_tx); drop(slave_tx);

View File

@@ -1,13 +1,14 @@
//! Tests d'intégration pour le pipeline audio complet //! Tests d'intégration pour le pipeline audio complet
use pmoaudio::{BufferNode, DecoderNode, DspNode, SinkNode, SourceNode, TimerNode}; use pmoaudio::{AudioChunk, BufferNode, DecoderNode, DspNode, SinkNode, SourceNode, TimerNode};
#[tokio::test] #[tokio::test]
async fn test_complete_pipeline() { async fn test_complete_pipeline() {
// Créer un pipeline complet : Source → Decoder → DSP → Buffer → Timer → Sink // Créer un pipeline complet : Source → Decoder → DSP → Buffer → Timer → Sink
let (mut decoder, decoder_tx) = DecoderNode::new(10); let (mut decoder, decoder_tx) = DecoderNode::new(10);
let (mut dsp, dsp_tx) = DspNode::new(10, 0.5); // Gain de 0.5 let gain_db = AudioChunk::gain_db_from_linear(0.5) as f32;
let (mut dsp, dsp_tx) = DspNode::new(10, gain_db); // Gain de 0.5
let (mut buffer, buffer_tx) = BufferNode::new(50, 10); let (mut buffer, buffer_tx) = BufferNode::new(50, 10);
let (mut timer, timer_tx) = TimerNode::new(10); let (mut timer, timer_tx) = TimerNode::new(10);
let (sink, sink_tx) = SinkNode::new("Integration Test".to_string(), 10); let (sink, sink_tx) = SinkNode::new("Integration Test".to_string(), 10);

123
tools/test_cpu Executable file
View File

@@ -0,0 +1,123 @@
#!/bin/bash
echo "=== Identification CPU ==="
echo ""
# Architecture
ARCH=$(uname -m)
echo "Architecture: $ARCH"
# Modèle CPU
if [ -f /proc/cpuinfo ]; then
MODEL=$(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | xargs)
if [ -z "$MODEL" ]; then
# Sur ARM, pas de "model name"
MODEL=$(grep "Hardware" /proc/cpuinfo | head -1 | cut -d: -f2 | xargs)
fi
echo "Modèle: $MODEL"
fi
# Nombre de cœurs
CORES=$(nproc)
echo "Cœurs: $CORES"
echo ""
echo "=== Capacités SIMD ==="
if [[ "$ARCH" == "x86_64" || "$ARCH" == "i686" ]]; then
# x86/x86_64
echo "Type: x86/x86_64"
if grep -q "avx512" /proc/cpuinfo; then
echo "✅ AVX-512 disponible"
fi
if grep -q "avx2" /proc/cpuinfo; then
echo "✅ AVX2 disponible (recommandé pour SIMD)"
fi
if grep -q "avx" /proc/cpuinfo; then
echo "✅ AVX disponible"
fi
if grep -q "sse4_2" /proc/cpuinfo; then
echo "✅ SSE4.2 disponible"
fi
if grep -q "fma" /proc/cpuinfo; then
echo "✅ FMA (Fused Multiply-Add) disponible"
fi
elif [[ "$ARCH" == "aarch64" ]]; then
# ARM 64-bit
echo "Type: ARM 64-bit"
FEATURES=$(grep "Features" /proc/cpuinfo | head -1)
if echo "$FEATURES" | grep -q "asimd"; then
echo "✅ NEON/Advanced SIMD disponible (recommandé)"
fi
if echo "$FEATURES" | grep -q "fp"; then
echo "✅ FPU matériel disponible"
fi
if echo "$FEATURES" | grep -q "sve"; then
echo "✅ SVE (Scalable Vector Extension) disponible"
fi
elif [[ "$ARCH" == "armv7l" || "$ARCH" == "armv6l" ]]; then
# ARM 32-bit
echo "Type: ARM 32-bit"
FEATURES=$(grep "Features" /proc/cpuinfo | head -1)
if echo "$FEATURES" | grep -q "neon"; then
echo "✅ NEON disponible (SIMD)"
fi
if echo "$FEATURES" | grep -q "vfpv4"; then
echo "✅ VFPv4 (FPU simple précision) disponible"
elif echo "$FEATURES" | grep -q "vfp"; then
echo "✅ VFP (FPU basique) disponible"
else
echo "❌ Pas de FPU matériel (Cortex-M0/M3?)"
fi
fi
echo ""
echo "=== Recommandation pour audio DSP ==="
if [[ "$ARCH" == "x86_64" ]]; then
if grep -q "avx2" /proc/cpuinfo; then
echo "🚀 Float f64 + SIMD (std::simd avec AVX2)"
echo " Performance optimale garantie"
else
echo "✅ Float f64 + SIMD (std::simd avec SSE)"
echo " Bonne performance"
fi
elif [[ "$ARCH" == "aarch64" ]]; then
if grep -q "asimd" /proc/cpuinfo; then
echo "🚀 Float f64 + SIMD (std::simd avec NEON)"
echo " Performance optimale sur ARM64"
else
echo "⚠️ ARM64 sans NEON (rare)"
fi
elif [[ "$ARCH" == "armv7l" ]]; then
if grep -q "neon" /proc/cpuinfo; then
echo "✅ Float f32 + NEON SIMD"
echo " Bonne performance sur ARM32"
elif grep -q "vfp" /proc/cpuinfo; then
echo "⚖️ Float f32 scalaire (avec FPU)"
echo " Performance correcte, équivalent à integer"
else
echo "💡 Integer fixed-point Q23"
echo " Pas de FPU, integer sera plus rapide"
fi
elif [[ "$ARCH" == "armv6l" ]]; then
echo "💡 Integer fixed-point Q23"
echo " CPU ancien, pas de FPU performant"
fi