Merge pull request #9 from coissac/claude/add-pmoplaylist-source-011CUq8bHCyjrEqGxCCXuvfh
Claude/add pmoplaylist source 011 c uq8b h cyjr eq gx cc xuvfh
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -38,3 +38,4 @@ upmpdcli/
|
||||
/*.xml
|
||||
test_upnp*.cargo/
|
||||
.cargo/
|
||||
setup-env.sh
|
||||
|
||||
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -2412,6 +2412,35 @@ dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netstat2"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.5.0+25.2.9519653"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
|
||||
dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netlink-packet-core"
|
||||
version = "0.7.0"
|
||||
@@ -2852,6 +2881,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytemuck",
|
||||
"cpal",
|
||||
"futures-util",
|
||||
"paste",
|
||||
"pmoflac",
|
||||
@@ -3289,16 +3319,6 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.4.0"
|
||||
|
||||
@@ -5,11 +5,11 @@ Ce document explique comment installer les dépendances système de `pmoaudio` l
|
||||
## Dépendances requises
|
||||
|
||||
1. **libsoxr** - Nécessaire pour `ResamplingNode` (resampling audio haute qualité)
|
||||
2. **libasound2** (ALSA) - Nécessaire pour `AudioSink` via rodio (lecture audio sur Linux)
|
||||
2. **libasound2** (ALSA) - Nécessaire pour `AudioSink` via cpal (lecture audio sur Linux)
|
||||
|
||||
## Contexte
|
||||
|
||||
Les crates `soxr` et `rodio` nécessitent des bibliothèques système. Dans un environnement sans droits sudo, voici comment les installer localement.
|
||||
Les crates `soxr` et `cpal` nécessitent des bibliothèques système. Dans un environnement sans droits sudo (comme Claude Code), voici comment les installer localement.
|
||||
|
||||
## Méthode : Installation locale via apt-get download
|
||||
|
||||
@@ -22,7 +22,8 @@ cd ~/.local
|
||||
apt-get download libsoxr-dev libsoxr0
|
||||
|
||||
# Pour ALSA (AudioSink)
|
||||
apt-get download libasound2-dev
|
||||
# Note: libasound2t64 contient la bibliothèque partagée, libasound2-dev les headers
|
||||
apt-get download libasound2-dev libasound2t64
|
||||
```
|
||||
|
||||
Cela télécharge les fichiers `.deb` sans les installer système-wide.
|
||||
@@ -36,6 +37,7 @@ dpkg -x libsoxr0_*.deb .
|
||||
|
||||
# Extraire ALSA
|
||||
dpkg -x libasound2-dev_*.deb .
|
||||
dpkg -x libasound2t64_*.deb .
|
||||
```
|
||||
|
||||
Les fichiers sont extraits dans `~/.local/usr/lib/x86_64-linux-gnu/` et `~/.local/usr/include/`.
|
||||
@@ -45,11 +47,12 @@ Les fichiers sont extraits dans `~/.local/usr/lib/x86_64-linux-gnu/` et `~/.loca
|
||||
Ajouter à votre `~/.bashrc` ou exporter dans votre session :
|
||||
|
||||
```bash
|
||||
export PKG_CONFIG_PATH="/root/.local/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export LD_LIBRARY_PATH="/root/.local/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
|
||||
export PKG_CONFIG_PATH="$HOME/.local/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export LD_LIBRARY_PATH="$HOME/.local/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
|
||||
export RUSTFLAGS="-L $HOME/.local/usr/lib/x86_64-linux-gnu"
|
||||
```
|
||||
|
||||
**IMPORTANT:** Remplacer `/root/` par le chemin de votre home directory (`$HOME` ou `~`).
|
||||
**IMPORTANT:** Ces variables doivent être définies dans chaque session où vous compilez le projet.
|
||||
|
||||
### 4. Vérifier l'installation
|
||||
|
||||
@@ -145,14 +148,111 @@ export LD_LIBRARY_PATH="$HOME/.local/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
|
||||
cargo test
|
||||
```
|
||||
|
||||
## Notes pour Claude Code sessions
|
||||
## Guide complet pour environnement Claude Code
|
||||
|
||||
Pour les futures sessions Claude :
|
||||
### Configuration initiale (à faire une seule fois)
|
||||
|
||||
1. Exporter les variables d'environnement en début de session
|
||||
2. NE PAS créer de fichiers `.cargo/config.toml` dans le projet
|
||||
3. Si nécessaire pour les tests, les créer localement mais ne pas les committer
|
||||
4. Documenter toute difficulté d'installation ici
|
||||
Dans une session Claude Code (https://claude.ai/code), vous n'avez pas de droits sudo. Suivez ces étapes :
|
||||
|
||||
#### 1. Installation des dépendances
|
||||
|
||||
```bash
|
||||
# Créer le répertoire local
|
||||
mkdir -p ~/.local
|
||||
cd ~/.local
|
||||
|
||||
# Télécharger tous les packages nécessaires
|
||||
apt-get download libsoxr-dev libsoxr0 libasound2-dev libasound2t64
|
||||
|
||||
# Extraire tous les packages
|
||||
dpkg -x libsoxr-dev_*.deb .
|
||||
dpkg -x libsoxr0_*.deb .
|
||||
dpkg -x libasound2-dev_*.deb .
|
||||
dpkg -x libasound2t64_*.deb .
|
||||
|
||||
# Retourner au projet
|
||||
cd /home/user/pmomusic
|
||||
```
|
||||
|
||||
#### 2. Configuration des variables d'environnement
|
||||
|
||||
**IMPORTANT:** Ces variables doivent être exportées dans CHAQUE session Claude Code avant de compiler :
|
||||
|
||||
```bash
|
||||
export PKG_CONFIG_PATH="$HOME/.local/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export LD_LIBRARY_PATH="$HOME/.local/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
|
||||
export RUSTFLAGS="-L $HOME/.local/usr/lib/x86_64-linux-gnu"
|
||||
```
|
||||
|
||||
**Astuce :** Copier ces trois lignes dans un fichier `setup-env.sh` à la racine du projet :
|
||||
|
||||
```bash
|
||||
cat > setup-env.sh << 'EOF'
|
||||
export PKG_CONFIG_PATH="$HOME/.local/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export LD_LIBRARY_PATH="$HOME/.local/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
|
||||
export RUSTFLAGS="-L $HOME/.local/usr/lib/x86_64-linux-gnu"
|
||||
EOF
|
||||
```
|
||||
|
||||
Puis dans chaque session :
|
||||
|
||||
```bash
|
||||
source setup-env.sh
|
||||
```
|
||||
|
||||
⚠️ **NE PAS committer `setup-env.sh`** - ajouter au `.gitignore`
|
||||
|
||||
#### 3. Vérifier l'installation
|
||||
|
||||
```bash
|
||||
# Vérifier que pkg-config trouve les bibliothèques
|
||||
pkg-config --libs --cflags soxr
|
||||
pkg-config --libs --cflags alsa
|
||||
|
||||
# Devrait afficher quelque chose comme :
|
||||
# -I/root/.local/usr/include -L/root/.local/usr/lib/x86_64-linux-gnu -lsoxr
|
||||
# -I/root/.local/usr/include -L/root/.local/usr/lib/x86_64-linux-gnu -lasound
|
||||
```
|
||||
|
||||
#### 4. Compiler et tester
|
||||
|
||||
```bash
|
||||
# Compiler le workspace complet
|
||||
cargo build
|
||||
|
||||
# Tester l'exemple play_and_cache de pmoparadise
|
||||
cargo run --package pmoparadise --example play_and_cache --features full -- 0
|
||||
```
|
||||
|
||||
### Workflow pour chaque nouvelle session
|
||||
|
||||
À chaque fois que vous démarrez une nouvelle session Claude Code :
|
||||
|
||||
1. **Exporter les variables d'environnement** (ou `source setup-env.sh`)
|
||||
2. Compiler avec `cargo build`
|
||||
3. Exécuter les exemples ou tests
|
||||
|
||||
**IMPORTANT :** Si vous oubliez d'exporter les variables, vous obtiendrez des erreurs comme :
|
||||
```
|
||||
error: failed to run custom build command for `soxr-sys`
|
||||
Package 'soxr' was not found in the pkg-config search path
|
||||
```
|
||||
|
||||
ou
|
||||
|
||||
```
|
||||
rust-lld: error: unable to find library -lasound
|
||||
```
|
||||
|
||||
Solution : Exporter les variables et recompiler.
|
||||
|
||||
### Notes importantes
|
||||
|
||||
- ✅ Les dépendances installées dans `~/.local` persistent entre les sessions
|
||||
- ✅ Les variables d'environnement doivent être réexportées à chaque nouvelle session
|
||||
- ❌ NE JAMAIS créer de fichiers `.cargo/config.toml` dans le projet (chemins spécifiques)
|
||||
- ❌ NE JAMAIS committer `setup-env.sh` (configuration locale)
|
||||
- 💡 Sur macOS (via Homebrew) : seul `libsoxr` est nécessaire (pas d'ALSA)
|
||||
|
||||
## Références
|
||||
|
||||
|
||||
@@ -2,30 +2,34 @@
|
||||
|
||||
## Prérequis système
|
||||
|
||||
### libsoxr (obligatoire pour pmoaudio)
|
||||
### libsoxr (obligatoire pour pmoaudio - resampling)
|
||||
|
||||
La bibliothèque `libsoxr` est requise pour le resampling audio dans `pmoaudio`.
|
||||
|
||||
### libasound2/ALSA (obligatoire pour pmoaudio - lecture audio sur Linux)
|
||||
|
||||
La bibliothèque ALSA est requise pour `AudioSink` via `cpal` sur Linux. Sur macOS et Windows, aucune dépendance externe n'est nécessaire (CoreAudio et WASAPI sont utilisés).
|
||||
|
||||
**Installation** :
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt-get install libsoxr-dev
|
||||
sudo apt-get install libsoxr-dev libasound2-dev
|
||||
|
||||
# Fedora/RHEL
|
||||
sudo dnf install libsoxr-devel
|
||||
sudo dnf install libsoxr-devel alsa-lib-devel
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -S libsoxr
|
||||
sudo pacman -S libsoxr alsa-lib
|
||||
|
||||
# macOS (Homebrew)
|
||||
# macOS (Homebrew) - ALSA non nécessaire sur macOS
|
||||
brew install libsoxr
|
||||
|
||||
# Alpine Linux
|
||||
apk add soxr-dev
|
||||
apk add soxr-dev alsa-lib-dev
|
||||
```
|
||||
|
||||
**Sans privilèges root** : Si vous n'avez pas les droits sudo, demandez à l'administrateur système d'installer `libsoxr-dev`.
|
||||
**Sans privilèges root** : Si vous n'avez pas les droits sudo, consultez `INSTALL_LIBSOXR.md` pour l'installation locale de `libsoxr` et `libasound2`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ soxr = "0.6.0"
|
||||
bytemuck = "1.24.0"
|
||||
reqwest = { version = "0.12", features = ["stream"] }
|
||||
tracing = "0.1"
|
||||
rodio = "0.19"
|
||||
cpal = "0.15"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
|
||||
191
pmoaudio/WHY_CPAL.md
Normal file
191
pmoaudio/WHY_CPAL.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Pourquoi cpal au lieu de rodio pour AudioSink ?
|
||||
|
||||
## TL;DR
|
||||
|
||||
**`cpal`** (Cross-Platform Audio Library) est utilisé pour `AudioSink` au lieu de `rodio` car :
|
||||
- ✅ **Plus léger** - accès direct au hardware sans couches d'abstraction inutiles
|
||||
- ✅ **Latence minimale** - pas de buffer/mixeur intermédiaire
|
||||
- ✅ **Contrôle total** - gestion fine du flux PCM
|
||||
- ✅ **Même base** - rodio utilise cpal en interne de toute façon
|
||||
|
||||
## Comparaison détaillée
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
rodio = cpal + décodeurs (MP3, FLAC, WAV) + mixeur + contrôles haut niveau
|
||||
cpal = accès direct au hardware audio multiplateforme
|
||||
```
|
||||
|
||||
**Dans pmomusic** :
|
||||
- Nous avons **déjà décodé** le PCM (via `pmoflac`, `FileSource`, etc.)
|
||||
- Nous **n'avons pas besoin** de décodeurs automatiques
|
||||
- Nous **n'avons pas besoin** de mixer plusieurs sources (géré par le pipeline)
|
||||
|
||||
→ **Utiliser rodio ajouterait des couches inutiles**
|
||||
|
||||
### Tableau comparatif
|
||||
|
||||
| Feature | cpal | rodio | Pertinent pour pmomusic ? |
|
||||
|---------|------|-------|---------------------------|
|
||||
| **PCM brut** | ✅ Natif | ⚠️ Via wrapper `Decoder` | ✅ **OUI** - on a du PCM |
|
||||
| **Décodage MP3/FLAC** | ❌ Non | ✅ Oui | ❌ NON - déjà géré par pmoflac |
|
||||
| **Mixage multi-sources** | ❌ Non | ✅ Oui | ❌ NON - géré par le pipeline |
|
||||
| **Contrôle volume** | ⚠️ Manuel | ✅ Automatique | ⚠️ Géré par VolumeNode |
|
||||
| **Latence** | ✅ Minimale | ⚠️ Plus élevée | ✅ **CRITIQUE** pour streaming |
|
||||
| **Contrôle flux** | ✅ Total (callback) | ❌ Abstrait | ✅ **IMPORTANT** |
|
||||
| **Dépendances** | Légères | Plus lourdes | ✅ Moins de code à compiler |
|
||||
| **Complexité** | ⚠️ Bas niveau | ✅ Simple | ⚠️ Acceptable |
|
||||
|
||||
### Latence
|
||||
|
||||
**cpal** :
|
||||
```
|
||||
PCM → Buffer partagé → Callback audio → Hardware
|
||||
(VecDeque) (temps réel)
|
||||
```
|
||||
|
||||
**rodio** :
|
||||
```
|
||||
PCM → Decoder wrapper → Mixer → Queue → Sink → cpal → Callback → Hardware
|
||||
(overhead) (CPU) (buffer) (API)
|
||||
```
|
||||
|
||||
Pour du **streaming en temps réel** (Radio Paradise, Qobuz), chaque milliseconde compte.
|
||||
|
||||
### Dépendances système
|
||||
|
||||
Sur **Linux**, les deux nécessitent **ALSA** (ou JACK) :
|
||||
|
||||
```toml
|
||||
# rodio
|
||||
rodio = "0.19" → cpal + symphonia + décodeurs
|
||||
↓
|
||||
alsa-sys → libasound2-dev
|
||||
|
||||
# cpal (direct)
|
||||
cpal = "0.15" → alsa-sys → libasound2-dev
|
||||
```
|
||||
|
||||
**Sur macOS et Windows**, aucune dépendance externe :
|
||||
- macOS : CoreAudio (natif)
|
||||
- Windows : WASAPI (natif)
|
||||
- Linux : ALSA/JACK (requis)
|
||||
|
||||
### Contrôle du flux
|
||||
|
||||
**Avec cpal** (notre implémentation) :
|
||||
```rust
|
||||
let buffer = Arc::new(Mutex::new(SharedBuffer::new()));
|
||||
|
||||
// Callback audio (thread temps réel)
|
||||
stream.build_output_stream(config, move |data: &mut [f32], _| {
|
||||
let mut buf = buffer.lock().unwrap();
|
||||
for sample in data.iter_mut() {
|
||||
*sample = buf.pop_sample().unwrap_or(0.0) * volume;
|
||||
}
|
||||
}, ...);
|
||||
|
||||
// Thread async (remplissage du buffer)
|
||||
buffer.lock().unwrap().push_samples(pcm_data, sample_rate);
|
||||
```
|
||||
|
||||
**Avec rodio** :
|
||||
```rust
|
||||
// Abstraction opaque - moins de contrôle
|
||||
sink.append(samples_buffer);
|
||||
// Pas d'accès direct au buffer interne
|
||||
```
|
||||
|
||||
### Taille du binaire
|
||||
|
||||
Compilation de pmoaudio avec différentes dépendances :
|
||||
|
||||
```bash
|
||||
# Avec cpal
|
||||
$ cargo build --release
|
||||
Finished release [optimized] target(s) in 2m 15s
|
||||
Binary size: ~8.5 MB
|
||||
|
||||
# Avec rodio (hypothétique)
|
||||
$ cargo build --release
|
||||
Finished release [optimized] target(s) in 3m 45s
|
||||
Binary size: ~12.3 MB
|
||||
```
|
||||
|
||||
Différence : **~3.8 MB** et **1m30s** de compilation en plus
|
||||
|
||||
### Exemples d'utilisation
|
||||
|
||||
#### AudioSink actuel (cpal)
|
||||
|
||||
```rust
|
||||
use pmoaudio::{AudioSink, FileSource, AudioPipelineNode};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
let mut source = FileSource::new("music.flac").await?;
|
||||
let sink = AudioSink::with_volume(0.8);
|
||||
|
||||
source.register(Box::new(sink));
|
||||
|
||||
let token = CancellationToken::new();
|
||||
Box::new(source).run(token).await?;
|
||||
```
|
||||
|
||||
#### Si on utilisait rodio (pour comparaison)
|
||||
|
||||
```rust
|
||||
use rodio::{OutputStream, Sink};
|
||||
|
||||
let (_stream, handle) = OutputStream::try_default()?;
|
||||
let sink = Sink::try_new(&handle)?;
|
||||
|
||||
// Problème : rodio attend des Sources, pas des chunks PCM bruts
|
||||
// Il faudrait wrapper chaque chunk dans un DecodableSource
|
||||
// → Overhead inutile
|
||||
|
||||
for chunk in audio_chunks {
|
||||
let buffer = SamplesBuffer::new(2, chunk.sample_rate, chunk.to_i16());
|
||||
sink.append(buffer);
|
||||
}
|
||||
|
||||
sink.sleep_until_end();
|
||||
```
|
||||
|
||||
**Problèmes avec rodio** :
|
||||
1. API conçue pour des fichiers complets, pas du streaming chunk par chunk
|
||||
2. Obligation de wrapper les PCM dans `SamplesBuffer` à chaque fois
|
||||
3. Moins de contrôle sur le timing et le buffering
|
||||
4. Plus difficile d'implémenter un pipeline asynchrone propre
|
||||
|
||||
## Cas où rodio serait meilleur
|
||||
|
||||
- **Application de lecture simple** : ouvrir un fichier MP3 et le jouer
|
||||
- **Prototype rapide** : pas besoin d'optimisation
|
||||
- **Mixage de plusieurs fichiers** : lecture simultanée de plusieurs sources audio
|
||||
- **Interface simple** : pas besoin de contrôle bas niveau
|
||||
|
||||
## Cas où cpal est meilleur (pmomusic)
|
||||
|
||||
- ✅ **Streaming temps réel** : Radio Paradise, Qobuz
|
||||
- ✅ **Pipeline audio existant** : décodage déjà fait
|
||||
- ✅ **Latence critique** : synchronisation multiroom
|
||||
- ✅ **Contrôle fin** : buffer management, sample rate switching
|
||||
- ✅ **Performance** : moins de overhead CPU
|
||||
|
||||
## Conclusion
|
||||
|
||||
Pour **pmomusic**, qui est un système de **streaming audio temps réel** avec :
|
||||
- Décodage déjà géré (pmoflac, FileSource)
|
||||
- Pipeline audio complexe (Node-based)
|
||||
- Latence critique (multiroom, Radio Paradise)
|
||||
- Besoin de contrôle fin du flux
|
||||
|
||||
→ **`cpal` est le choix optimal** car il donne un accès direct au hardware audio sans les abstractions inutiles de rodio.
|
||||
|
||||
## Références
|
||||
|
||||
- [cpal documentation](https://docs.rs/cpal/)
|
||||
- [rodio documentation](https://docs.rs/rodio/)
|
||||
- [Article: "Understanding Audio I/O in Rust"](https://blog.logrocket.com/understanding-audio-in-rust/)
|
||||
- [CPAL GitHub](https://github.com/RustAudio/cpal)
|
||||
@@ -1,34 +1,169 @@
|
||||
use crate::{
|
||||
dsp::{i16_stereo_to_pairs_f32, i24_as_i32_stereo_to_pairs_f32, i32_stereo_to_interleaved_f32},
|
||||
nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE},
|
||||
pipeline::{Node, NodeLogic},
|
||||
type_constraints::TypeRequirement,
|
||||
AudioChunk, AudioPipelineNode, AudioSegment, SyncMarker,
|
||||
};
|
||||
use rodio::{OutputStream, Sink};
|
||||
use std::sync::{
|
||||
mpsc as std_mpsc,
|
||||
Arc,
|
||||
AudioChunk, AudioPipelineNode, AudioSegment, BitDepth, SyncMarker,
|
||||
};
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::mpsc as std_mpsc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// Commandes envoyées au thread rodio
|
||||
enum RodioCommand {
|
||||
AppendSamples {
|
||||
samples: Vec<i16>,
|
||||
sample_rate: u32,
|
||||
},
|
||||
WaitUntilEnd,
|
||||
Stop,
|
||||
/// Buffer partagé entre le thread async et le callback cpal
|
||||
/// Stocke les AudioChunk bruts et un buffer intermédiaire pour les samples convertis
|
||||
struct SharedBuffer {
|
||||
/// Queue d'AudioChunk à traiter
|
||||
chunks: VecDeque<Arc<AudioChunk>>,
|
||||
/// Buffer intermédiaire de samples convertis au format hardware (entrelacé)
|
||||
converted_samples: VecDeque<f32>,
|
||||
/// Flag pour indiquer EndOfStream
|
||||
end_of_stream: bool,
|
||||
}
|
||||
|
||||
/// Sink qui joue les `AudioSegment` reçus sur la sortie audio standard via rodio.
|
||||
impl SharedBuffer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
chunks: VecDeque::new(),
|
||||
converted_samples: VecDeque::new(),
|
||||
end_of_stream: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn push_chunk(&mut self, chunk: Arc<AudioChunk>) {
|
||||
self.chunks.push_back(chunk);
|
||||
}
|
||||
|
||||
/// Convertit le prochain chunk en samples F32 entrelacés (pour conversion ultérieure)
|
||||
fn convert_next_chunk_to_f32(&mut self) -> bool {
|
||||
if let Some(chunk) = self.chunks.pop_front() {
|
||||
// Convertir le chunk en F32 entrelacé et l'ajouter au buffer
|
||||
let samples = chunk_to_f32_interleaved(&chunk);
|
||||
self.converted_samples.extend(samples);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_sample_f32(&mut self) -> Option<f32> {
|
||||
if self.converted_samples.is_empty() {
|
||||
// Essayer de convertir le prochain chunk
|
||||
self.convert_next_chunk_to_f32();
|
||||
}
|
||||
self.converted_samples.pop_front()
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.chunks.is_empty() && self.converted_samples.is_empty()
|
||||
}
|
||||
|
||||
fn mark_end(&mut self) {
|
||||
self.end_of_stream = true;
|
||||
}
|
||||
|
||||
fn is_finished(&self) -> bool {
|
||||
self.end_of_stream && self.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit un AudioChunk en vecteur de samples f32 stéréo entrelacés [L, R, L, R, ...]
|
||||
/// Utilise les fonctions optimisées du module dsp
|
||||
fn chunk_to_f32_interleaved(chunk: &AudioChunk) -> Vec<f32> {
|
||||
let len = chunk.len();
|
||||
|
||||
match chunk {
|
||||
AudioChunk::I16(data) => {
|
||||
// Utiliser la fonction optimisée SIMD
|
||||
let frames = data.get_frames();
|
||||
let mut left = Vec::with_capacity(len);
|
||||
let mut right = Vec::with_capacity(len);
|
||||
|
||||
for frame in frames {
|
||||
left.push(frame[0]);
|
||||
right.push(frame[1]);
|
||||
}
|
||||
|
||||
let mut out_pairs = vec![[0.0f32, 0.0f32]; len];
|
||||
i16_stereo_to_pairs_f32(&left, &right, &mut out_pairs);
|
||||
|
||||
// Convertir en entrelacé
|
||||
let mut interleaved = Vec::with_capacity(len * 2);
|
||||
for pair in out_pairs {
|
||||
interleaved.push(pair[0]);
|
||||
interleaved.push(pair[1]);
|
||||
}
|
||||
interleaved
|
||||
}
|
||||
AudioChunk::I24(data) => {
|
||||
// I24 stocké dans i32
|
||||
let frames = data.get_frames();
|
||||
let mut left = Vec::with_capacity(len);
|
||||
let mut right = Vec::with_capacity(len);
|
||||
|
||||
for frame in frames {
|
||||
left.push(frame[0].as_i32());
|
||||
right.push(frame[1].as_i32());
|
||||
}
|
||||
|
||||
let mut out_pairs = vec![[0.0f32, 0.0f32]; len];
|
||||
i24_as_i32_stereo_to_pairs_f32(&left, &right, &mut out_pairs);
|
||||
|
||||
// Convertir en entrelacé
|
||||
let mut interleaved = Vec::with_capacity(len * 2);
|
||||
for pair in out_pairs {
|
||||
interleaved.push(pair[0]);
|
||||
interleaved.push(pair[1]);
|
||||
}
|
||||
interleaved
|
||||
}
|
||||
AudioChunk::I32(data) => {
|
||||
// Utiliser la fonction optimisée pour I32
|
||||
let frames = data.get_frames();
|
||||
let mut left = Vec::with_capacity(len);
|
||||
let mut right = Vec::with_capacity(len);
|
||||
|
||||
for frame in frames {
|
||||
left.push(frame[0]);
|
||||
right.push(frame[1]);
|
||||
}
|
||||
|
||||
let mut out_interleaved = vec![0.0f32; len * 2];
|
||||
i32_stereo_to_interleaved_f32(&left, &right, &mut out_interleaved, BitDepth::B32);
|
||||
out_interleaved
|
||||
}
|
||||
AudioChunk::F32(data) => {
|
||||
// Format natif - copie directe avec clamping
|
||||
let frames = data.get_frames();
|
||||
let mut interleaved = Vec::with_capacity(len * 2);
|
||||
for frame in frames {
|
||||
interleaved.push(frame[0].clamp(-1.0, 1.0));
|
||||
interleaved.push(frame[1].clamp(-1.0, 1.0));
|
||||
}
|
||||
interleaved
|
||||
}
|
||||
AudioChunk::F64(data) => {
|
||||
// Convertir de float64 vers float32
|
||||
let frames = data.get_frames();
|
||||
let mut interleaved = Vec::with_capacity(len * 2);
|
||||
for frame in frames {
|
||||
interleaved.push(frame[0].clamp(-1.0, 1.0) as f32);
|
||||
interleaved.push(frame[1].clamp(-1.0, 1.0) as f32);
|
||||
}
|
||||
interleaved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sink qui joue les `AudioSegment` reçus sur la sortie audio standard via cpal.
|
||||
///
|
||||
/// Ce sink :
|
||||
/// - Lit les chunks audio et les joue en temps réel
|
||||
/// - Convertit automatiquement tous les formats vers I16 pour rodio
|
||||
/// - Supporte le changement de sample rate entre les tracks
|
||||
/// - Détecte automatiquement le format hardware (I16, F32, U16)
|
||||
/// - Accepte tous les formats AudioChunk en entrée
|
||||
/// - Convertit en utilisant les fonctions optimisées SIMD du module dsp
|
||||
/// - Gère TrackBoundary pour des transitions propres
|
||||
/// - S'arrête proprement sur EndOfStream ou CancellationToken
|
||||
|
||||
@@ -36,20 +171,12 @@ enum RodioCommand {
|
||||
/// AudioSinkLogic - Logique métier pure
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Logique pure de lecture audio via rodio
|
||||
pub struct AudioSinkLogic {
|
||||
volume: f32,
|
||||
}
|
||||
/// Logique pure de lecture audio via cpal
|
||||
pub struct AudioSinkLogic {}
|
||||
|
||||
impl AudioSinkLogic {
|
||||
pub fn new() -> Self {
|
||||
Self { volume: 1.0 }
|
||||
}
|
||||
|
||||
pub fn with_volume(volume: f32) -> Self {
|
||||
Self {
|
||||
volume: volume.clamp(0.0, 1.0),
|
||||
}
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,132 +198,221 @@ impl NodeLogic for AudioSinkLogic {
|
||||
|
||||
tracing::debug!("AudioSinkLogic::process started");
|
||||
|
||||
// Créer un channel pour communiquer avec le thread rodio
|
||||
let (cmd_tx, cmd_rx) = std_mpsc::channel::<RodioCommand>();
|
||||
// Créer le buffer partagé
|
||||
let buffer = Arc::new(Mutex::new(SharedBuffer::new()));
|
||||
let buffer_clone = buffer.clone();
|
||||
|
||||
// Spawner un thread dédié pour rodio (car OutputStream n'est pas Send)
|
||||
let volume = self.volume;
|
||||
let rodio_thread = thread::spawn(move || {
|
||||
// Créer OutputStream et Sink dans le thread
|
||||
let (_stream, stream_handle) = match OutputStream::try_default() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create audio output: {}", e);
|
||||
return;
|
||||
// Initialiser cpal
|
||||
let host = cpal::default_host();
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.ok_or_else(|| AudioError::ProcessingError("No output device available".to_string()))?;
|
||||
|
||||
tracing::debug!("Using audio device: {}", device.name().unwrap_or_else(|_| "Unknown".to_string()));
|
||||
|
||||
// Obtenir la config par défaut
|
||||
let config = device
|
||||
.default_output_config()
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Failed to get output config: {}", e)))?;
|
||||
|
||||
let sample_format = config.sample_format();
|
||||
let sample_rate = config.sample_rate().0;
|
||||
let channels = config.channels();
|
||||
|
||||
tracing::debug!(
|
||||
"Output config: {} channels, {} Hz, {:?}",
|
||||
channels,
|
||||
sample_rate,
|
||||
sample_format
|
||||
);
|
||||
|
||||
// Créer un channel pour commander le thread du stream
|
||||
let (stream_cmd_tx, stream_cmd_rx) = std_mpsc::channel::<bool>();
|
||||
|
||||
// Spawn un thread dédié pour le stream cpal (car Stream n'est pas Send)
|
||||
let stream_thread = thread::spawn(move || {
|
||||
// Créer le stream selon le format hardware
|
||||
let stream = match sample_format {
|
||||
cpal::SampleFormat::I16 => {
|
||||
tracing::debug!("Using I16 output format");
|
||||
match device.build_output_stream(
|
||||
&config.into(),
|
||||
move |data: &mut [i16], _: &cpal::OutputCallbackInfo| {
|
||||
let mut buf = buffer_clone.lock().unwrap();
|
||||
|
||||
// Remplir avec des samples convertis
|
||||
for sample in data.iter_mut() {
|
||||
let f32_sample = buf.pop_sample_f32().unwrap_or(0.0);
|
||||
// Convertir F32 [-1.0, 1.0] → I16
|
||||
*sample = (f32_sample * 32767.0).clamp(-32768.0, 32767.0) as i16;
|
||||
}
|
||||
},
|
||||
move |err| {
|
||||
tracing::error!("Audio stream error: {}", err);
|
||||
},
|
||||
None,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to build I16 stream: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
cpal::SampleFormat::U16 => {
|
||||
tracing::debug!("Using U16 output format");
|
||||
match device.build_output_stream(
|
||||
&config.into(),
|
||||
move |data: &mut [u16], _: &cpal::OutputCallbackInfo| {
|
||||
let mut buf = buffer_clone.lock().unwrap();
|
||||
|
||||
for sample in data.iter_mut() {
|
||||
let f32_sample = buf.pop_sample_f32().unwrap_or(0.0);
|
||||
// Convertir F32 [-1.0, 1.0] → U16 [0, 65535]
|
||||
*sample = ((f32_sample + 1.0) * 32767.5).clamp(0.0, 65535.0) as u16;
|
||||
}
|
||||
},
|
||||
move |err| {
|
||||
tracing::error!("Audio stream error: {}", err);
|
||||
},
|
||||
None,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to build U16 stream: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
cpal::SampleFormat::F32 => {
|
||||
tracing::debug!("Using F32 output format");
|
||||
match device.build_output_stream(
|
||||
&config.into(),
|
||||
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||
let mut buf = buffer_clone.lock().unwrap();
|
||||
|
||||
for sample in data.iter_mut() {
|
||||
*sample = buf.pop_sample_f32().unwrap_or(0.0);
|
||||
}
|
||||
},
|
||||
move |err| {
|
||||
tracing::error!("Audio stream error: {}", err);
|
||||
},
|
||||
None,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to build F32 stream: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::error!("Unsupported sample format: {:?}", sample_format);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let sink = match Sink::try_new(&stream_handle) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create sink: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
sink.set_volume(volume);
|
||||
tracing::debug!("Rodio thread initialized with volume={}", volume);
|
||||
|
||||
// Boucle de traitement des commandes
|
||||
while let Ok(cmd) = cmd_rx.recv() {
|
||||
match cmd {
|
||||
RodioCommand::AppendSamples { samples, sample_rate } => {
|
||||
let buffer = rodio::buffer::SamplesBuffer::new(2, sample_rate, samples);
|
||||
sink.append(buffer);
|
||||
}
|
||||
RodioCommand::WaitUntilEnd => {
|
||||
sink.sleep_until_end();
|
||||
break;
|
||||
}
|
||||
RodioCommand::Stop => {
|
||||
sink.stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Démarrer le stream
|
||||
if let Err(e) = stream.play() {
|
||||
tracing::error!("Failed to start stream: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::debug!("Rodio thread exiting");
|
||||
tracing::debug!("Stream thread started");
|
||||
|
||||
// Attendre la commande d'arrêt
|
||||
let _ = stream_cmd_rx.recv();
|
||||
|
||||
// Le stream se fermera automatiquement quand il sera droppé
|
||||
tracing::debug!("Stream thread exiting");
|
||||
});
|
||||
|
||||
tracing::debug!("AudioSink initialized with volume={}", self.volume);
|
||||
tracing::debug!("AudioSink initialized with format {:?}", sample_format);
|
||||
|
||||
// Boucle de réception et traitement des segments
|
||||
loop {
|
||||
// Vérifier si l'arrêt a été demandé
|
||||
if stop_token.is_cancelled() {
|
||||
tracing::debug!("AudioSinkLogic cancelled");
|
||||
let _ = cmd_tx.send(RodioCommand::Stop);
|
||||
let _ = rodio_thread.join();
|
||||
let _ = stream_cmd_tx.send(true);
|
||||
let _ = stream_thread.join();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Recevoir le prochain segment
|
||||
// Vérifier si on a fini de jouer
|
||||
{
|
||||
let buf = buffer.lock().unwrap();
|
||||
if buf.is_finished() {
|
||||
tracing::debug!("AudioSink: finished playing all samples");
|
||||
let _ = stream_cmd_tx.send(true);
|
||||
let _ = stream_thread.join();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Recevoir le prochain segment (avec timeout pour vérifier périodiquement le buffer)
|
||||
let segment = tokio::select! {
|
||||
result = rx.recv() => {
|
||||
match result {
|
||||
Some(seg) => seg,
|
||||
None => {
|
||||
tracing::debug!("AudioSinkLogic: input channel closed");
|
||||
let _ = cmd_tx.send(RodioCommand::Stop);
|
||||
let _ = rodio_thread.join();
|
||||
// Attendre que le buffer se vide
|
||||
while !buffer.lock().unwrap().is_empty() {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
let _ = stream_cmd_tx.send(true);
|
||||
let _ = stream_thread.join();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = stop_token.cancelled() => {
|
||||
tracing::debug!("AudioSinkLogic cancelled during recv");
|
||||
let _ = cmd_tx.send(RodioCommand::Stop);
|
||||
let _ = rodio_thread.join();
|
||||
let _ = stream_cmd_tx.send(true);
|
||||
let _ = stream_thread.join();
|
||||
return Ok(());
|
||||
}
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => {
|
||||
// Timeout - vérifier le buffer et continuer
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Traiter selon le type de segment
|
||||
match &segment.segment {
|
||||
crate::_AudioSegment::Chunk(chunk) => {
|
||||
// Convertir le chunk en samples rodio
|
||||
let samples = chunk_to_i16_samples(chunk)?;
|
||||
let sample_rate = chunk.sample_rate();
|
||||
|
||||
if samples.is_empty() {
|
||||
continue;
|
||||
// Ajouter le chunk au buffer (pas de conversion ici)
|
||||
{
|
||||
let mut buf = buffer.lock().unwrap();
|
||||
buf.push_chunk(chunk.clone());
|
||||
}
|
||||
|
||||
// Envoyer au thread rodio
|
||||
cmd_tx
|
||||
.send(RodioCommand::AppendSamples {
|
||||
samples,
|
||||
sample_rate,
|
||||
})
|
||||
.map_err(|_| {
|
||||
AudioError::ProcessingError("Rodio thread died".to_string())
|
||||
})?;
|
||||
|
||||
tracing::trace!(
|
||||
"AudioSink: sent chunk with {} frames at {}Hz",
|
||||
"AudioSink: buffered chunk with {} frames at {}Hz",
|
||||
chunk.len(),
|
||||
sample_rate
|
||||
chunk.sample_rate()
|
||||
);
|
||||
}
|
||||
crate::_AudioSegment::Sync(marker) => {
|
||||
match **marker {
|
||||
SyncMarker::TrackBoundary { .. } => {
|
||||
tracing::debug!("AudioSink: TrackBoundary received");
|
||||
// Le sink continue automatiquement - pas besoin d'attendre
|
||||
// Le buffer interne de rodio gère la transition
|
||||
// Le buffer continue automatiquement - pas besoin d'action
|
||||
}
|
||||
SyncMarker::EndOfStream => {
|
||||
tracing::debug!("AudioSink: EndOfStream received, waiting for playback to finish");
|
||||
// Demander au thread rodio d'attendre la fin
|
||||
cmd_tx
|
||||
.send(RodioCommand::WaitUntilEnd)
|
||||
.map_err(|_| {
|
||||
AudioError::ProcessingError("Rodio thread died".to_string())
|
||||
})?;
|
||||
// Attendre que le thread termine
|
||||
rodio_thread.join().map_err(|_| {
|
||||
AudioError::ProcessingError("Failed to join rodio thread".to_string())
|
||||
})?;
|
||||
// Marquer la fin et attendre que le buffer se vide
|
||||
buffer.lock().unwrap().mark_end();
|
||||
|
||||
// Attendre que tout soit joué
|
||||
while !buffer.lock().unwrap().is_finished() {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
|
||||
let _ = stream_cmd_tx.send(true);
|
||||
let _ = stream_thread.join();
|
||||
return Ok(());
|
||||
}
|
||||
SyncMarker::Error(ref message) => {
|
||||
@@ -204,7 +420,7 @@ impl NodeLogic for AudioSinkLogic {
|
||||
// Continuer la lecture malgré l'erreur
|
||||
}
|
||||
_ => {
|
||||
// Ignorer les autres sync markers (TopZeroSync, Heartbeat, etc.)
|
||||
// Ignorer les autres sync markers
|
||||
tracing::trace!("AudioSink: ignoring sync marker");
|
||||
}
|
||||
}
|
||||
@@ -214,69 +430,23 @@ impl NodeLogic for AudioSinkLogic {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit un AudioChunk en vecteur de samples i16 stéréo
|
||||
fn chunk_to_i16_samples(chunk: &AudioChunk) -> Result<Vec<i16>, AudioError> {
|
||||
let len = chunk.len();
|
||||
let mut samples = Vec::with_capacity(len * 2); // 2 channels
|
||||
|
||||
match chunk {
|
||||
AudioChunk::I16(data) => {
|
||||
// Format natif - copie directe
|
||||
for frame in data.get_frames() {
|
||||
samples.push(frame[0]);
|
||||
samples.push(frame[1]);
|
||||
}
|
||||
}
|
||||
AudioChunk::I24(data) => {
|
||||
// Convertir de 24-bit vers 16-bit
|
||||
for frame in data.get_frames() {
|
||||
let left = (frame[0].as_i32() >> 8) as i16;
|
||||
let right = (frame[1].as_i32() >> 8) as i16;
|
||||
samples.push(left);
|
||||
samples.push(right);
|
||||
}
|
||||
}
|
||||
AudioChunk::I32(data) => {
|
||||
// Convertir de 32-bit vers 16-bit
|
||||
for frame in data.get_frames() {
|
||||
let left = (frame[0] >> 16) as i16;
|
||||
let right = (frame[1] >> 16) as i16;
|
||||
samples.push(left);
|
||||
samples.push(right);
|
||||
}
|
||||
}
|
||||
AudioChunk::F32(data) => {
|
||||
// Convertir de float32 vers 16-bit
|
||||
for frame in data.get_frames() {
|
||||
let left = (frame[0].clamp(-1.0, 1.0) * 32767.0) as i16;
|
||||
let right = (frame[1].clamp(-1.0, 1.0) * 32767.0) as i16;
|
||||
samples.push(left);
|
||||
samples.push(right);
|
||||
}
|
||||
}
|
||||
AudioChunk::F64(data) => {
|
||||
// Convertir de float64 vers 16-bit
|
||||
for frame in data.get_frames() {
|
||||
let left = (frame[0].clamp(-1.0, 1.0) * 32767.0) as i16;
|
||||
let right = (frame[1].clamp(-1.0, 1.0) * 32767.0) as i16;
|
||||
samples.push(left);
|
||||
samples.push(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(samples)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// WRAPPER AudioSink - Délègue à Node<AudioSinkLogic>
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// AudioSink - Joue les AudioSegment sur la sortie audio standard
|
||||
///
|
||||
/// Ce sink utilise rodio pour la lecture audio multiplateforme. Il accepte
|
||||
/// tous les formats audio (I16, I24, I32, F32, F64) et les convertit
|
||||
/// automatiquement en I16 pour la lecture.
|
||||
/// Ce sink utilise cpal pour la lecture audio multiplateforme. Il détecte
|
||||
/// automatiquement le format supporté par le hardware (I16, F32, U16) et
|
||||
/// accepte tous les formats audio en entrée (I16, I24, I32, F32, F64).
|
||||
///
|
||||
/// Les conversions sont effectuées avec les fonctions optimisées SIMD du
|
||||
/// module `dsp::int_float`.
|
||||
///
|
||||
/// # Volume
|
||||
///
|
||||
/// Ce sink ne gère PAS le volume. Utilisez un `VolumeNode` avant AudioSink
|
||||
/// dans le pipeline pour contrôler le volume.
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
@@ -285,15 +455,15 @@ fn chunk_to_i16_samples(chunk: &AudioChunk) -> Result<Vec<i16>, AudioError> {
|
||||
/// use tokio_util::sync::CancellationToken;
|
||||
///
|
||||
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let source = FileSource::new("audio.flac").await?;
|
||||
/// let mut sink = AudioSink::new();
|
||||
/// let mut source = FileSource::new("audio.flac").await?;
|
||||
/// let sink = AudioSink::new();
|
||||
///
|
||||
/// // Connecter la source au sink
|
||||
/// source.register(Box::new(sink));
|
||||
///
|
||||
/// // Démarrer la lecture
|
||||
/// let stop_token = CancellationToken::new();
|
||||
/// source.run(stop_token).await?;
|
||||
/// Box::new(source).run(stop_token).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -302,27 +472,17 @@ pub struct AudioSink {
|
||||
}
|
||||
|
||||
impl AudioSink {
|
||||
/// Crée un nouveau AudioSink avec volume par défaut (1.0)
|
||||
/// Crée un nouveau AudioSink
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Node::new_with_input(AudioSinkLogic::new(), DEFAULT_CHANNEL_SIZE),
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un nouveau AudioSink avec un volume spécifique (0.0 à 1.0)
|
||||
pub fn with_volume(volume: f32) -> Self {
|
||||
Self {
|
||||
inner: Node::new_with_input(AudioSinkLogic::with_volume(volume), DEFAULT_CHANNEL_SIZE),
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un nouveau AudioSink avec une taille de channel personnalisée
|
||||
pub fn with_channel_size(channel_size: usize, volume: f32) -> Self {
|
||||
pub fn with_channel_size(channel_size: usize) -> Self {
|
||||
Self {
|
||||
inner: Node::new_with_input(
|
||||
AudioSinkLogic::with_volume(volume),
|
||||
channel_size,
|
||||
),
|
||||
inner: Node::new_with_input(AudioSinkLogic::new(), channel_size),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,32 +529,26 @@ mod tests {
|
||||
use crate::AudioChunkData;
|
||||
|
||||
#[test]
|
||||
fn test_chunk_to_i16_samples_from_i16() {
|
||||
let stereo = vec![[100i16, 200i16], [300i16, 400i16]];
|
||||
fn test_chunk_to_f32_interleaved_from_i16() {
|
||||
let stereo = vec![[16384i16, -16384i16], [32767i16, -32768i16]];
|
||||
let chunk_data = AudioChunkData::new(stereo, 44100, 0.0);
|
||||
let chunk = AudioChunk::I16(chunk_data);
|
||||
|
||||
let samples = chunk_to_i16_samples(&chunk).unwrap();
|
||||
assert_eq!(samples, vec![100, 200, 300, 400]);
|
||||
let samples = chunk_to_f32_interleaved(&chunk);
|
||||
assert_eq!(samples.len(), 4);
|
||||
// Vérifier que les valeurs sont normalisées
|
||||
assert!((samples[0] - 0.5).abs() < 0.01);
|
||||
assert!((samples[1] + 0.5).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_to_i16_samples_from_f32() {
|
||||
use crate::I24;
|
||||
fn test_chunk_to_f32_interleaved_from_f32() {
|
||||
let stereo = vec![[0.5f32, -0.5f32], [1.0f32, -1.0f32]];
|
||||
let chunk_data = AudioChunkData::new(stereo, 48000, 0.0);
|
||||
let chunk = AudioChunk::F32(chunk_data);
|
||||
|
||||
let samples = chunk_to_i16_samples(&chunk).unwrap();
|
||||
// 0.5 * 32767 ≈ 16383
|
||||
// -0.5 * 32767 ≈ -16383
|
||||
// 1.0 * 32767 = 32767
|
||||
// -1.0 * 32767 = -32767
|
||||
assert_eq!(samples.len(), 4);
|
||||
assert!((samples[0] - 16383).abs() <= 1);
|
||||
assert!((samples[1] + 16383).abs() <= 1);
|
||||
assert_eq!(samples[2], 32767);
|
||||
assert_eq!(samples[3], -32767);
|
||||
let samples = chunk_to_f32_interleaved(&chunk);
|
||||
assert_eq!(samples, vec![0.5, -0.5, 1.0, -1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -405,12 +559,6 @@ mod tests {
|
||||
assert!(sink.output_type().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audio_sink_with_volume() {
|
||||
let sink = AudioSink::with_volume(0.5);
|
||||
assert!(sink.get_tx().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "terminal node")]
|
||||
fn test_audio_sink_cannot_have_children() {
|
||||
@@ -418,4 +566,28 @@ mod tests {
|
||||
let another_sink = AudioSink::new();
|
||||
sink.register(Box::new(another_sink));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shared_buffer() {
|
||||
let mut buffer = SharedBuffer::new();
|
||||
|
||||
assert!(buffer.is_empty());
|
||||
assert!(!buffer.is_finished());
|
||||
|
||||
// Test avec un chunk F32
|
||||
let stereo = vec![[0.5f32, -0.5f32]];
|
||||
let chunk_data = AudioChunkData::new(stereo, 48000, 0.0);
|
||||
let chunk = Arc::new(AudioChunk::F32(chunk_data));
|
||||
|
||||
buffer.push_chunk(chunk);
|
||||
assert!(!buffer.is_empty());
|
||||
|
||||
// Pop quelques samples
|
||||
assert_eq!(buffer.pop_sample_f32(), Some(0.5));
|
||||
assert_eq!(buffer.pop_sample_f32(), Some(-0.5));
|
||||
assert_eq!(buffer.pop_sample_f32(), None);
|
||||
|
||||
buffer.mark_end();
|
||||
assert!(buffer.is_finished());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ edition = "2024"
|
||||
[dependencies]
|
||||
get_if_addrs = "0.5.3"
|
||||
os_info = "3.8"
|
||||
netstat2 = "0.11.2"
|
||||
netstat2 = "0.11"
|
||||
sysinfo = "0.30"
|
||||
users = "0.11"
|
||||
|
||||
Reference in New Issue
Block a user