Passons en stéreo
reprise du module DSP pmoaudio
This commit is contained in:
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -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
33
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
22
Makefile
22
Makefile
@@ -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)"
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 d’ordre 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 d’adapter 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(),
|
|
||||||
sample_rate: self.sample_rate,
|
|
||||||
gain: self.gain * new_gain, // Multiplication des gains
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Modifie le gain via un facteur linéaire multiplié au gain courant.
|
||||||
|
pub fn with_modified_gain_linear(&self, gain_linear: f64) -> Arc<Self> {
|
||||||
|
self.with_modified_gain_db(linear_to_db(gain_linear))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bit_depth(&self) -> BitDepth {
|
||||||
|
self.bit_depth
|
||||||
|
}
|
||||||
|
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
162
pmoaudio/src/bit_depth.rs
Normal 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 d’ordre 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
111
pmoaudio/src/dsp/depth.rs
Normal 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
93
pmoaudio/src/dsp/gain.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
188
pmoaudio/src/dsp/int_float.rs
Normal file
188
pmoaudio/src/dsp/int_float.rs
Normal 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 l’overflow 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
13
pmoaudio/src/dsp/mod.rs
Normal 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;
|
||||||
74
pmoaudio/src/dsp/resampling.rs
Normal file
74
pmoaudio/src/dsp/resampling.rs
Normal 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)
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 =
|
let gain_linear = AudioChunk::gain_linear_from_db(self.gain_db as f64) as f32;
|
||||||
AudioChunk::new(chunk.order, left_data, right_data, chunk.sample_rate);
|
let mut pairs = chunk.to_pairs_f32();
|
||||||
|
for frame in &mut pairs {
|
||||||
self.subscribers.push(Arc::new(new_chunk)).await?;
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
244
pmoaudio/src/nodes/file_source.rs
Normal file
244
pmoaudio/src/nodes/file_source.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
300
pmoaudio/src/nodes/flac_file_sink.rs
Normal file
300
pmoaudio/src/nodes/flac_file_sink.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
123
tools/test_cpu
Executable 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
|
||||||
Reference in New Issue
Block a user