Retour sur pmoaudio

This commit is contained in:
2025-11-01 21:10:57 +01:00
parent 3402b2b434
commit cd385162ff
26 changed files with 4481 additions and 1048 deletions

3
Cargo.lock generated
View File

@@ -2630,6 +2630,7 @@ name = "pmoaudiocache"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"axum 0.8.6",
"bytes",
"chrono",
@@ -2640,11 +2641,13 @@ dependencies = [
"pmoconfig",
"pmodidl",
"pmoflac",
"pmometadata",
"pmoserver",
"quick-xml 0.37.5",
"rusqlite",
"serde",
"serde_json",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",

View File

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

View File

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

View File

@@ -9,13 +9,17 @@ simd = []
[dependencies]
tokio = { version = "1.42", features = ["full"] }
tokio-util = { version = "0.7", features = ["io"] }
async-trait = "0.1"
futures-util = "0.3"
pmoflac = { path = "../pmoflac" }
pmometadata = { path = "../pmometadata" }
paste = "1"
soxr = "0.6.0"
bytemuck = "1.24.0"
reqwest = { version = "0.12", features = ["stream"] }
[dev-dependencies]
tokio-test = "0.4"
tempfile = "3"
wiremock = "0.6"

View File

@@ -30,12 +30,20 @@ fn example_create_chunks() {
// Chunk I32 stéréo
let stereo_i32 = vec![[1000i32, 2000i32], [3000i32, 4000i32]];
let chunk_i32 = AudioChunkData::new(stereo_i32, 48000, 0.0);
println!("Chunk I32: {} frames @ {}Hz", chunk_i32.len(), chunk_i32.sample_rate());
println!(
"Chunk I32: {} frames @ {}Hz",
chunk_i32.len(),
chunk_i32.sample_rate()
);
// Chunk F32 stéréo (normalisé [-1.0, 1.0])
let stereo_f32 = vec![[0.5f32, -0.5f32], [0.8f32, -0.8f32]];
let chunk_f32 = AudioChunkData::new(stereo_f32, 48000, 0.0);
println!("Chunk F32: {} frames @ {}Hz", chunk_f32.len(), chunk_f32.sample_rate());
println!(
"Chunk F32: {} frames @ {}Hz",
chunk_f32.len(),
chunk_f32.sample_rate()
);
// Chunk depuis canaux séparés
let left = vec![100i32, 200i32, 300i32];
@@ -77,7 +85,10 @@ fn example_conversions() {
// Utilisation des traits From/Into
let chunk_i16 = AudioChunkData::new(vec![[1000i16, 2000i16]], 48000, 0.0);
let chunk_i32_from_i16: std::sync::Arc<AudioChunkData<i32>> = (&*chunk_i16).into();
println!("\nConversion I16 → I32 via Into: {} frames", chunk_i32_from_i16.len());
println!(
"\nConversion I16 → I32 via Into: {} frames",
chunk_i32_from_i16.len()
);
println!();
}
@@ -163,30 +174,33 @@ fn example_gain_manipulation() {
println!(">>> Manipulation du gain\n");
// Créer un segment
let segment = AudioSegment::new_chunk(
0,
0.0,
vec![[1000i32, 2000i32]],
48000,
BitDepth::B32,
);
let segment = AudioSegment::new_chunk(0, 0.0, vec![[1000i32, 2000i32]], 48000, BitDepth::B32);
println!("Gain initial: {} dB", segment.gain_db().unwrap());
// Définir un gain absolu
let segment_6db = segment.with_gain_db(6.0).unwrap();
println!("Après with_gain_db(6.0): {} dB", segment_6db.gain_db().unwrap());
println!(
"Après with_gain_db(6.0): {} dB",
segment_6db.gain_db().unwrap()
);
// Ajuster le gain (relatif)
let segment_9db = segment_6db.adjust_gain_db(3.0).unwrap();
println!("Après adjust_gain_db(+3.0): {} dB", segment_9db.gain_db().unwrap());
println!(
"Après adjust_gain_db(+3.0): {} dB",
segment_9db.gain_db().unwrap()
);
// Les segments originaux ne sont pas modifiés (immutabilité)
println!("Gain du segment original: {} dB", segment.gain_db().unwrap());
println!(
"Gain du segment original: {} dB",
segment.gain_db().unwrap()
);
// Conversion gain linéaire ↔ dB
let linear_gain = db_to_linear(6.0);
let gain_db = linear_to_db(linear_gain);
let linear_gain = gain_linear_from_db(6.0);
let gain_db = gain_db_from_linear(linear_gain);
println!("\n6 dB = {:.4}x (linéaire)", linear_gain);
println!("{:.4}x = {:.2} dB", linear_gain, gain_db);

View File

@@ -0,0 +1,75 @@
//! Test d'intégration pour FileSource et FlacFileSink
//!
//! Ce programme teste la chaîne complète :
//! 1. Lecture d'un fichier audio avec FileSource
//! 2. Écriture vers FLAC avec FlacFileSink
//!
//! Usage:
//! cargo run --example file_nodes_test -- <input_file> <output_file>
use pmoaudio::{FileSource, FlacFileSink};
use std::env;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Récupérer les arguments
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
eprintln!("Usage: {} <input_file> <output_file>", args[0]);
eprintln!("Example: {} input.flac output.flac", args[0]);
std::process::exit(1);
}
let input_path = &args[1];
let output_path = &args[2];
println!("Input: {}", input_path);
println!("Output: {}", output_path);
println!();
// Créer le pipeline: FileSource → FlacFileSink
let mut source = FileSource::new(input_path); // Calcul automatique de la taille des chunks (~50ms)
let (sink, tx) = FlacFileSink::new(output_path); // Utilise le buffer par défaut (16 segments)
source.add_subscriber(tx);
// Lancer le sink dans une tâche séparée
let sink_handle = tokio::spawn(async move {
println!("FlacFileSink started");
let result = sink.run().await;
println!("FlacFileSink finished");
result
});
// Lancer le source
println!("FileSource started");
let source_result = source.run().await;
println!("FileSource finished");
// Vérifier les résultats
match source_result {
Ok(()) => println!("✓ FileSource completed successfully"),
Err(e) => {
eprintln!("✗ FileSource error: {}", e);
return Err(e.into());
}
}
let stats = sink_handle.await??;
println!("✓ FlacFileSink completed successfully");
println!();
println!("Statistics:");
println!(" Tracks written: {}", stats.tracks.len());
for (i, track) in stats.tracks.iter().enumerate() {
println!(" Track {}:", i);
println!(" Output file: {:?}", track.path);
println!(" Chunks received: {}", track.chunks_received);
println!(" Total samples: {}", track.total_samples);
println!(
" Duration: {:.2} seconds",
track.total_duration_sec
);
}
Ok(())
}

View File

@@ -1,7 +1,7 @@
//! AudioChunk : Représentation générique de données audio stéréo
//!
//! Cette nouvelle architecture supporte différents types de samples :
//! - Entiers : i8, i16, I24 (24-bit), i32
//! - Entiers : i16, I24 (24-bit), i32
//! - Flottants : f32, f64
//!
//! L'utilisation de génériques permet de factoriser le code tout en gardant
@@ -18,7 +18,7 @@ use crate::{dsp, BitDepth, Sample, I24};
/// Représente un chunk audio stéréo typé avec partage zero-copy via Arc
///
/// Cette structure générique encapsule des données audio de n'importe quel type
/// de sample (i8, i16, I24, i32, f32, f64). Les données sont partagées via `Arc`
/// de sample (i16, I24, i32, f32, f64). Les données sont partagées via `Arc`
/// pour permettre un partage efficace entre plusieurs consumers sans copier.
///
/// # Optimisation zero-copy
@@ -118,7 +118,7 @@ impl<T: Sample> AudioChunkData<T> {
/// Gain sous forme linéaire
#[inline]
pub fn gain_linear(&self) -> f64 {
db_to_linear(self.gain_db)
gain_linear_from_db(self.gain_db)
}
/// Retourne une vue immuable sur les frames `[L, R]`
@@ -146,7 +146,7 @@ impl<T: Sample> AudioChunkData<T> {
/// Définit le gain à l'aide d'un facteur linéaire (>0)
pub fn set_gain_linear(&self, gain_linear: f64) -> Arc<Self> {
self.set_gain_db(linear_to_db(gain_linear))
self.set_gain_db(gain_db_from_linear(gain_linear))
}
/// Modifie le gain de ce chunk (ajoute un delta en dB)
@@ -156,11 +156,11 @@ impl<T: Sample> AudioChunkData<T> {
/// Modifie le gain via un facteur linéaire multiplié au gain courant
pub fn with_modified_gain_linear(&self, gain_linear: f64) -> Arc<Self> {
self.with_modified_gain_db(linear_to_db(gain_linear))
self.with_modified_gain_db(gain_db_from_linear(gain_linear))
}
}
// Méthodes spécifiques pour les types entiers (i8, i16, I24, i32)
// Méthodes spécifiques pour les types entiers (i16, I24, i32)
impl AudioChunkData<i32> {
/// Applique le gain et retourne un nouveau chunk avec les données modifiées
///
@@ -172,14 +172,18 @@ impl AudioChunkData<i32> {
}
let mut stereo = self.clone_frames();
dsp::apply_gain_stereo(&mut stereo, self.gain_db);
dsp::apply_gain_stereo_i32(&mut stereo, self.gain_db);
AudioChunkData::new(stereo, self.sample_rate, 0.0)
}
/// Construit un chunk depuis deux vecteurs `i32` séparés (L/R)
pub fn from_channels(left: Vec<i32>, right: Vec<i32>, sample_rate: u32) -> Arc<Self> {
assert_eq!(left.len(), right.len(), "channels must have identical length");
assert_eq!(
left.len(),
right.len(),
"channels must have identical length"
);
let stereo = left
.into_iter()
.zip(right.into_iter())
@@ -213,7 +217,7 @@ impl AudioChunkData<f32> {
return self; // Pas de gain à appliquer
}
let gain_linear = db_to_linear(self.gain_db) as f32;
let gain_linear = gain_linear_from_db(self.gain_db) as f32;
let mut stereo = self.clone_frames();
for frame in &mut stereo {
frame[0] *= gain_linear;
@@ -225,7 +229,11 @@ impl AudioChunkData<f32> {
/// Construit un chunk depuis deux vecteurs `f32` séparés (L/R)
pub fn from_channels(left: Vec<f32>, right: Vec<f32>, sample_rate: u32) -> Arc<Self> {
assert_eq!(left.len(), right.len(), "channels must have identical length");
assert_eq!(
left.len(),
right.len(),
"channels must have identical length"
);
let stereo = left
.into_iter()
.zip(right.into_iter())
@@ -243,7 +251,7 @@ impl AudioChunkData<f64> {
return self; // Pas de gain à appliquer
}
let gain_linear = db_to_linear(self.gain_db);
let gain_linear = gain_linear_from_db(self.gain_db);
let mut stereo = self.clone_frames();
for frame in &mut stereo {
frame[0] *= gain_linear;
@@ -255,7 +263,11 @@ impl AudioChunkData<f64> {
/// Construit un chunk depuis deux vecteurs `f64` séparés (L/R)
pub fn from_channels(left: Vec<f64>, right: Vec<f64>, sample_rate: u32) -> Arc<Self> {
assert_eq!(left.len(), right.len(), "channels must have identical length");
assert_eq!(
left.len(),
right.len(),
"channels must have identical length"
);
let stereo = left
.into_iter()
.zip(right.into_iter())
@@ -276,7 +288,6 @@ impl AudioChunkData<f64> {
///
/// # Variantes
///
/// - `I8` : Échantillons 8-bit signés
/// - `I16` : Échantillons 16-bit signés
/// - `I24` : Échantillons 24-bit signés (stockés sur i32)
/// - `I32` : Échantillons 32-bit signés
@@ -298,7 +309,6 @@ impl AudioChunkData<f64> {
/// ```
#[derive(Debug, Clone)]
pub enum AudioChunk {
I8(Arc<AudioChunkData<i8>>),
I16(Arc<AudioChunkData<i16>>),
I24(Arc<AudioChunkData<I24>>),
I32(Arc<AudioChunkData<i32>>),
@@ -310,7 +320,6 @@ impl AudioChunk {
/// Retourne le nombre de frames du chunk
pub fn len(&self) -> usize {
match self {
AudioChunk::I8(d) => d.len(),
AudioChunk::I16(d) => d.len(),
AudioChunk::I24(d) => d.len(),
AudioChunk::I32(d) => d.len(),
@@ -327,7 +336,6 @@ impl AudioChunk {
/// Taux d'échantillonnage (Hz)
pub fn sample_rate(&self) -> u32 {
match self {
AudioChunk::I8(d) => d.sample_rate(),
AudioChunk::I16(d) => d.sample_rate(),
AudioChunk::I24(d) => d.sample_rate(),
AudioChunk::I32(d) => d.sample_rate(),
@@ -339,7 +347,6 @@ impl AudioChunk {
/// Gain courant en décibels
pub fn gain_db(&self) -> f64 {
match self {
AudioChunk::I8(d) => d.gain_db(),
AudioChunk::I16(d) => d.gain_db(),
AudioChunk::I24(d) => d.gain_db(),
AudioChunk::I32(d) => d.gain_db(),
@@ -350,13 +357,12 @@ impl AudioChunk {
/// Gain sous forme linéaire
pub fn gain_linear(&self) -> f64 {
db_to_linear(self.gain_db())
gain_linear_from_db(self.gain_db())
}
/// Définit le gain en dB
pub fn set_gain_db(&self, gain_db: f64) -> Self {
match self {
AudioChunk::I8(d) => AudioChunk::I8(d.set_gain_db(gain_db)),
AudioChunk::I16(d) => AudioChunk::I16(d.set_gain_db(gain_db)),
AudioChunk::I24(d) => AudioChunk::I24(d.set_gain_db(gain_db)),
AudioChunk::I32(d) => AudioChunk::I32(d.set_gain_db(gain_db)),
@@ -367,7 +373,7 @@ impl AudioChunk {
/// Définit le gain via un facteur linéaire
pub fn set_gain_linear(&self, gain_linear: f64) -> Self {
self.set_gain_db(linear_to_db(gain_linear))
self.set_gain_db(gain_db_from_linear(gain_linear))
}
/// Modifie le gain (ajoute un delta en dB)
@@ -380,31 +386,20 @@ impl AudioChunk {
/// Le gain du chunk résultant est remis à 0.0 dB.
pub fn apply_gain(self) -> Self {
match self {
AudioChunk::I8(d) => {
// Pour i8, on convert en i32, applique gain, puis reconvertit
// TODO: optimiser avec une version directe
let gain_db = d.gain_db();
if gain_db.abs() < f64::EPSILON {
return AudioChunk::I8(d);
}
let gain_linear = db_to_linear(gain_db) as f32;
let mut stereo = d.clone_frames();
for frame in &mut stereo {
frame[0] = (frame[0] as f32 * gain_linear).round().clamp(-128.0, 127.0) as i8;
frame[1] = (frame[1] as f32 * gain_linear).round().clamp(-128.0, 127.0) as i8;
}
AudioChunk::I8(AudioChunkData::new(stereo, d.sample_rate(), 0.0))
}
AudioChunk::I16(d) => {
let gain_db = d.gain_db();
if gain_db.abs() < f64::EPSILON {
return AudioChunk::I16(d);
}
let gain_linear = db_to_linear(gain_db) as f32;
let gain_linear = gain_linear_from_db(gain_db) as f32;
let mut stereo = d.clone_frames();
for frame in &mut stereo {
frame[0] = (frame[0] as f32 * gain_linear).round().clamp(-32768.0, 32767.0) as i16;
frame[1] = (frame[1] as f32 * gain_linear).round().clamp(-32768.0, 32767.0) as i16;
frame[0] = (frame[0] as f32 * gain_linear)
.round()
.clamp(-32768.0, 32767.0) as i16;
frame[1] = (frame[1] as f32 * gain_linear)
.round()
.clamp(-32768.0, 32767.0) as i16;
}
AudioChunk::I16(AudioChunkData::new(stereo, d.sample_rate(), 0.0))
}
@@ -413,11 +408,15 @@ impl AudioChunk {
if gain_db.abs() < f64::EPSILON {
return AudioChunk::I24(d);
}
let gain_linear = db_to_linear(gain_db) as f32;
let gain_linear = gain_linear_from_db(gain_db) as f32;
let mut stereo = d.clone_frames();
for frame in &mut stereo {
let l = (frame[0].as_i32() as f32 * gain_linear).round().clamp(-8_388_608.0, 8_388_607.0) as i32;
let r = (frame[1].as_i32() as f32 * gain_linear).round().clamp(-8_388_608.0, 8_388_607.0) as i32;
let l = (frame[0].as_i32() as f32 * gain_linear)
.round()
.clamp(-8_388_608.0, 8_388_607.0) as i32;
let r = (frame[1].as_i32() as f32 * gain_linear)
.round()
.clamp(-8_388_608.0, 8_388_607.0) as i32;
frame[0] = I24::new_clamped(l);
frame[1] = I24::new_clamped(r);
}
@@ -432,7 +431,6 @@ impl AudioChunk {
/// Retourne le nom du type de sample
pub fn type_name(&self) -> &'static str {
match self {
AudioChunk::I8(_) => "i8",
AudioChunk::I16(_) => "i16",
AudioChunk::I24(_) => "I24",
AudioChunk::I32(_) => "i32",
@@ -440,6 +438,415 @@ impl AudioChunk {
AudioChunk::F64(_) => "f64",
}
}
/// Tente de convertir vers AudioIntegerChunk (retourne None si float)
pub fn try_as_integer(&self) -> Option<AudioIntegerChunk> {
match self {
AudioChunk::I16(d) => Some(AudioIntegerChunk::I16(d.clone())),
AudioChunk::I24(d) => Some(AudioIntegerChunk::I24(d.clone())),
AudioChunk::I32(d) => Some(AudioIntegerChunk::I32(d.clone())),
AudioChunk::F32(_) | AudioChunk::F64(_) => None,
}
}
/// Tente de convertir vers AudioFloatChunk (retourne None si integer)
pub fn try_as_float(&self) -> Option<AudioFloatChunk> {
match self {
AudioChunk::F32(d) => Some(AudioFloatChunk::F32(d.clone())),
AudioChunk::F64(d) => Some(AudioFloatChunk::F64(d.clone())),
AudioChunk::I16(_) | AudioChunk::I24(_) | AudioChunk::I32(_) => None,
}
}
/// Vérifie si le chunk est de type entier
pub fn is_integer(&self) -> bool {
matches!(
self,
AudioChunk::I16(_) | AudioChunk::I24(_) | AudioChunk::I32(_)
)
}
/// Vérifie si le chunk est de type flottant
pub fn is_float(&self) -> bool {
matches!(self, AudioChunk::F32(_) | AudioChunk::F64(_))
}
}
#[derive(Debug, Clone)]
pub enum AudioIntegerChunk {
I16(Arc<AudioChunkData<i16>>),
I24(Arc<AudioChunkData<I24>>),
I32(Arc<AudioChunkData<i32>>),
}
impl AudioIntegerChunk {
/// Retourne le nombre de frames du chunk
pub fn len(&self) -> usize {
match self {
AudioIntegerChunk::I16(d) => d.len(),
AudioIntegerChunk::I24(d) => d.len(),
AudioIntegerChunk::I32(d) => d.len(),
}
}
/// Vérifie si le chunk est vide
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Taux d'échantillonnage (Hz)
pub fn sample_rate(&self) -> u32 {
match self {
AudioIntegerChunk::I16(d) => d.sample_rate(),
AudioIntegerChunk::I24(d) => d.sample_rate(),
AudioIntegerChunk::I32(d) => d.sample_rate(),
}
}
/// Gain courant en décibels
pub fn gain_db(&self) -> f64 {
match self {
AudioIntegerChunk::I16(d) => d.gain_db(),
AudioIntegerChunk::I24(d) => d.gain_db(),
AudioIntegerChunk::I32(d) => d.gain_db(),
}
}
/// Gain sous forme linéaire
pub fn gain_linear(&self) -> f64 {
gain_linear_from_db(self.gain_db())
}
/// Définit le gain en dB
pub fn set_gain_db(&self, gain_db: f64) -> Self {
match self {
AudioIntegerChunk::I16(d) => AudioIntegerChunk::I16(d.set_gain_db(gain_db)),
AudioIntegerChunk::I24(d) => AudioIntegerChunk::I24(d.set_gain_db(gain_db)),
AudioIntegerChunk::I32(d) => AudioIntegerChunk::I32(d.set_gain_db(gain_db)),
}
}
/// Définit le gain via un facteur linéaire
pub fn set_gain_linear(&self, gain_linear: f64) -> Self {
self.set_gain_db(gain_db_from_linear(gain_linear))
}
/// Modifie le gain (ajoute un delta en dB)
pub fn with_modified_gain_db(&self, delta_gain_db: f64) -> Self {
self.set_gain_db(self.gain_db() + delta_gain_db)
}
/// Applique le gain et retourne un nouveau chunk avec les données modifiées
///
/// Le gain du chunk résultant est remis à 0.0 dB.
pub fn apply_gain(self) -> Self {
match self {
AudioIntegerChunk::I16(d) => {
let gain_db = d.gain_db();
if gain_db.abs() < f64::EPSILON {
return AudioIntegerChunk::I16(d);
}
let gain_linear = gain_linear_from_db(gain_db) as f32;
let mut stereo = d.clone_frames();
for frame in &mut stereo {
frame[0] = (frame[0] as f32 * gain_linear)
.round()
.clamp(-32768.0, 32767.0) as i16;
frame[1] = (frame[1] as f32 * gain_linear)
.round()
.clamp(-32768.0, 32767.0) as i16;
}
AudioIntegerChunk::I16(AudioChunkData::new(stereo, d.sample_rate(), 0.0))
}
AudioIntegerChunk::I24(d) => {
let gain_db = d.gain_db();
if gain_db.abs() < f64::EPSILON {
return AudioIntegerChunk::I24(d);
}
let gain_linear = gain_linear_from_db(gain_db) as f32;
let mut stereo = d.clone_frames();
for frame in &mut stereo {
let l = (frame[0].as_i32() as f32 * gain_linear)
.round()
.clamp(-8_388_608.0, 8_388_607.0) as i32;
let r = (frame[1].as_i32() as f32 * gain_linear)
.round()
.clamp(-8_388_608.0, 8_388_607.0) as i32;
frame[0] = I24::new_clamped(l);
frame[1] = I24::new_clamped(r);
}
AudioIntegerChunk::I24(AudioChunkData::new(stereo, d.sample_rate(), 0.0))
}
AudioIntegerChunk::I32(d) => AudioIntegerChunk::I32(d.apply_gain()),
}
}
/// Retourne le nom du type de sample
pub fn type_name(&self) -> &'static str {
match self {
AudioIntegerChunk::I16(_) => "i16",
AudioIntegerChunk::I24(_) => "I24",
AudioIntegerChunk::I32(_) => "i32",
}
}
/// Vérifie si le chunk est de type I16
pub fn is_i16(&self) -> bool {
matches!(self, AudioIntegerChunk::I16(_))
}
/// Vérifie si le chunk est de type I24
pub fn is_i24(&self) -> bool {
matches!(self, AudioIntegerChunk::I24(_))
}
/// Vérifie si le chunk est de type I32
pub fn is_i32(&self) -> bool {
matches!(self, AudioIntegerChunk::I32(_))
}
/// Retourne la profondeur de bit du chunk
pub fn bit_depth(&self) -> u8 {
match self {
AudioIntegerChunk::I16(_) => 16,
AudioIntegerChunk::I24(_) => 24,
AudioIntegerChunk::I32(_) => 32,
}
}
/// Convertit vers AudioChunk
pub fn as_audio_chunk(&self) -> AudioChunk {
match self {
AudioIntegerChunk::I16(d) => AudioChunk::I16(d.clone()),
AudioIntegerChunk::I24(d) => AudioChunk::I24(d.clone()),
AudioIntegerChunk::I32(d) => AudioChunk::I32(d.clone()),
}
}
/// Convertit vers I16 (avec conversion si nécessaire)
pub fn to_i16(&self) -> AudioIntegerChunk {
match self {
AudioIntegerChunk::I16(_) => self.clone(),
AudioIntegerChunk::I24(d) => {
// I24 -> I32 -> I16
let i32_chunk = crate::conversions::convert_i24_to_i32(d);
let converted = crate::conversions::convert_i32_to_i16(&i32_chunk);
AudioIntegerChunk::I16(converted)
}
AudioIntegerChunk::I32(d) => {
let converted = crate::conversions::convert_i32_to_i16(d);
AudioIntegerChunk::I16(converted)
}
}
}
/// Convertit vers I24 (avec conversion si nécessaire)
pub fn to_i24(&self) -> AudioIntegerChunk {
match self {
AudioIntegerChunk::I16(d) => {
// I16 -> I32 -> I24
let i32_chunk = crate::conversions::convert_i16_to_i32(d);
let converted = crate::conversions::convert_i32_to_i24(&i32_chunk);
AudioIntegerChunk::I24(converted)
}
AudioIntegerChunk::I24(_) => self.clone(),
AudioIntegerChunk::I32(d) => {
let converted = crate::conversions::convert_i32_to_i24(d);
AudioIntegerChunk::I24(converted)
}
}
}
/// Convertit vers I32 (avec conversion si nécessaire)
pub fn to_i32(&self) -> AudioIntegerChunk {
match self {
AudioIntegerChunk::I16(d) => {
let converted = crate::conversions::convert_i16_to_i32(d);
AudioIntegerChunk::I32(converted)
}
AudioIntegerChunk::I24(d) => {
let converted = crate::conversions::convert_i24_to_i32(d);
AudioIntegerChunk::I32(converted)
}
AudioIntegerChunk::I32(_) => self.clone(),
}
}
/// Retourne un itérateur sur les frames
pub fn frames(&self) -> Box<dyn Iterator<Item = [i32; 2]> + '_> {
match self {
AudioIntegerChunk::I16(d) => {
Box::new(d.frames().iter().map(|f| [f[0] as i32, f[1] as i32]))
}
AudioIntegerChunk::I24(d) => {
Box::new(d.frames().iter().map(|f| [f[0].as_i32(), f[1].as_i32()]))
}
AudioIntegerChunk::I32(d) => Box::new(d.frames().iter().map(|f| [f[0], f[1]])),
}
}
}
impl From<AudioChunk> for AudioIntegerChunk {
/// Convertit depuis AudioChunk (panic si le chunk est float)
fn from(chunk: AudioChunk) -> Self {
match chunk {
AudioChunk::I16(d) => AudioIntegerChunk::I16(d),
AudioChunk::I24(d) => AudioIntegerChunk::I24(d),
AudioChunk::I32(d) => AudioIntegerChunk::I32(d),
AudioChunk::F32(_) | AudioChunk::F64(_) => {
panic!("Cannot convert float AudioChunk to AudioIntegerChunk")
}
}
}
}
#[derive(Debug, Clone)]
pub enum AudioFloatChunk {
F32(Arc<AudioChunkData<f32>>),
F64(Arc<AudioChunkData<f64>>),
}
impl AudioFloatChunk {
/// Retourne le nombre de frames du chunk
pub fn len(&self) -> usize {
match self {
AudioFloatChunk::F32(d) => d.len(),
AudioFloatChunk::F64(d) => d.len(),
}
}
/// Vérifie si le chunk est vide
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Taux d'échantillonnage (Hz)
pub fn sample_rate(&self) -> u32 {
match self {
AudioFloatChunk::F32(d) => d.sample_rate(),
AudioFloatChunk::F64(d) => d.sample_rate(),
}
}
/// Gain courant en décibels
pub fn gain_db(&self) -> f64 {
match self {
AudioFloatChunk::F32(d) => d.gain_db(),
AudioFloatChunk::F64(d) => d.gain_db(),
}
}
/// Gain sous forme linéaire
pub fn gain_linear(&self) -> f64 {
gain_linear_from_db(self.gain_db())
}
/// Définit le gain en dB
pub fn set_gain_db(&self, gain_db: f64) -> Self {
match self {
AudioFloatChunk::F32(d) => AudioFloatChunk::F32(d.set_gain_db(gain_db)),
AudioFloatChunk::F64(d) => AudioFloatChunk::F64(d.set_gain_db(gain_db)),
}
}
/// Définit le gain via un facteur linéaire
pub fn set_gain_linear(&self, gain_linear: f64) -> Self {
self.set_gain_db(gain_db_from_linear(gain_linear))
}
/// Modifie le gain (ajoute un delta en dB)
pub fn with_modified_gain_db(&self, delta_gain_db: f64) -> Self {
self.set_gain_db(self.gain_db() + delta_gain_db)
}
/// Applique le gain et retourne un nouveau chunk avec les données modifiées
///
/// Le gain du chunk résultant est remis à 0.0 dB.
pub fn apply_gain(self) -> Self {
match self {
AudioFloatChunk::F32(d) => AudioFloatChunk::F32(d.apply_gain()),
AudioFloatChunk::F64(d) => AudioFloatChunk::F64(d.apply_gain()),
}
}
/// Retourne le nom du type de sample
pub fn type_name(&self) -> &'static str {
match self {
AudioFloatChunk::F32(_) => "f32",
AudioFloatChunk::F64(_) => "f64",
}
}
/// Vérifie si le chunk est de type F32
pub fn is_f32(&self) -> bool {
matches!(self, AudioFloatChunk::F32(_))
}
/// Vérifie si le chunk est de type F64
pub fn is_f64(&self) -> bool {
matches!(self, AudioFloatChunk::F64(_))
}
/// Retourne la profondeur de bit du chunk (32 ou 64)
pub fn bit_depth(&self) -> u8 {
match self {
AudioFloatChunk::F32(_) => 32,
AudioFloatChunk::F64(_) => 64,
}
}
/// Convertit vers AudioChunk
pub fn as_audio_chunk(&self) -> AudioChunk {
match self {
AudioFloatChunk::F32(d) => AudioChunk::F32(d.clone()),
AudioFloatChunk::F64(d) => AudioChunk::F64(d.clone()),
}
}
/// Convertit vers F32 (avec conversion si nécessaire)
pub fn to_f32(&self) -> AudioFloatChunk {
match self {
AudioFloatChunk::F32(_) => self.clone(),
AudioFloatChunk::F64(d) => {
let converted = crate::conversions::convert_f64_to_f32(d);
AudioFloatChunk::F32(converted)
}
}
}
/// Convertit vers F64 (avec conversion si nécessaire)
pub fn to_f64(&self) -> AudioFloatChunk {
match self {
AudioFloatChunk::F32(d) => {
let converted = crate::conversions::convert_f32_to_f64(d);
AudioFloatChunk::F64(converted)
}
AudioFloatChunk::F64(_) => self.clone(),
}
}
/// Retourne un itérateur sur les frames
pub fn frames(&self) -> Box<dyn Iterator<Item = [f64; 2]> + '_> {
match self {
AudioFloatChunk::F32(d) => {
Box::new(d.frames().iter().map(|f| [f[0] as f64, f[1] as f64]))
}
AudioFloatChunk::F64(d) => Box::new(d.frames().iter().map(|f| [f[0], f[1]])),
}
}
}
impl From<AudioChunk> for AudioFloatChunk {
/// Convertit depuis AudioChunk (panic si le chunk est entier)
fn from(chunk: AudioChunk) -> Self {
match chunk {
AudioChunk::F32(d) => AudioFloatChunk::F32(d),
AudioChunk::F64(d) => AudioFloatChunk::F64(d),
AudioChunk::I16(_) | AudioChunk::I24(_) | AudioChunk::I32(_) => {
panic!("Cannot convert integer AudioChunk to AudioFloatChunk")
}
}
}
}
// ============================================================================
@@ -450,7 +857,7 @@ const MIN_GAIN_DB: f64 = -120.0;
/// Convertit un gain linéaire (>0) en décibels
#[inline]
pub fn linear_to_db(gain_linear: f64) -> f64 {
pub fn gain_db_from_linear(gain_linear: f64) -> f64 {
if gain_linear <= 0.0 {
MIN_GAIN_DB
} else {
@@ -460,18 +867,8 @@ pub fn linear_to_db(gain_linear: f64) -> f64 {
/// Convertit un gain en décibels vers un gain linéaire
#[inline]
pub fn db_to_linear(gain_db: f64) -> f64 {
10f64.powf(gain_db / 20.0)
}
/// Convertit un gain linéaire en décibels (méthode publique pour compatibilité)
pub fn gain_db_from_linear(gain_linear: f64) -> f64 {
linear_to_db(gain_linear)
}
/// Convertit un gain en décibels vers un gain linéaire (méthode publique pour compatibilité)
pub fn gain_linear_from_db(gain_db: f64) -> f64 {
db_to_linear(gain_db)
10f64.powf(gain_db / 20.0)
}
// ============================================================================
@@ -515,10 +912,10 @@ mod tests {
#[test]
fn test_gain_conversion() {
let linear = 2.0;
let db = linear_to_db(linear);
let db = gain_db_from_linear(linear);
assert!((db - 6.0206).abs() < 0.01); // 2x ≈ +6dB
let back = db_to_linear(db);
let back = gain_linear_from_db(db);
assert!((back - linear).abs() < 0.001);
}
}

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use pmometadata::TrackMetadata;
use crate::{AudioChunk, AudioChunkData, BitDepth, SyncMarker, linear_to_db};
use crate::{gain_db_from_linear, AudioChunk, AudioChunkData, BitDepth, SyncMarker};
pub enum _AudioSegment {
Chunk(Arc<AudioChunk>),
@@ -60,7 +60,7 @@ impl AudioSegment {
_bit_depth: BitDepth, // Conservé pour compatibilité API
gain_linear: f64,
) -> Arc<Self> {
let chunk_data = AudioChunkData::new(stereo, sample_rate, linear_to_db(gain_linear));
let chunk_data = AudioChunkData::new(stereo, sample_rate, gain_db_from_linear(gain_linear));
let chunk = AudioChunk::I32(chunk_data);
Arc::new(Self {
order,
@@ -98,7 +98,11 @@ impl AudioSegment {
sample_rate: u32,
bit_depth: BitDepth,
) -> Arc<Self> {
assert_eq!(left.len(), right.len(), "channels must have identical length");
assert_eq!(
left.len(),
right.len(),
"channels must have identical length"
);
// Convertir f32 → i32 selon le bit_depth
let max_value = bit_depth.max_value();
@@ -152,9 +156,10 @@ impl AudioSegment {
}
pub fn new_track_boundary(
order: u64,
timestamp_sec: f64,
metadata: Arc<dyn TrackMetadata>) -> Arc<Self> {
order: u64,
timestamp_sec: f64,
metadata: Arc<dyn TrackMetadata>,
) -> Arc<Self> {
let marker = Arc::new(SyncMarker::TrackBoundary {
metadata: Arc::clone(&metadata),
});
@@ -166,13 +171,13 @@ impl AudioSegment {
}
pub fn new_stream_metadata(
order: u64,
order: u64,
timestamp_sec: f64,
key: String,
value: String
) -> Arc<Self> {
value: String,
) -> Arc<Self> {
let marker = Arc::new(SyncMarker::StreamMetadata { key, value });
Arc::new(Self {
order,
timestamp_sec,
@@ -183,51 +188,41 @@ impl AudioSegment {
pub fn new_top_zero_sync() -> Arc<Self> {
let marker = Arc::new(SyncMarker::TopZeroSync);
Arc::new(Self{
Arc::new(Self {
order: 0,
timestamp_sec: 0.0,
segment: _AudioSegment::Sync(marker)
segment: _AudioSegment::Sync(marker),
})
}
pub fn new_hearbeat(
order: u64,
timestamp_sec: f64,
) -> Arc<Self> {
pub fn new_hearbeat(order: u64, timestamp_sec: f64) -> Arc<Self> {
let marker = Arc::new(SyncMarker::Heartbeat);
Arc::new(Self{
order: order,
timestamp_sec: timestamp_sec,
segment: _AudioSegment::Sync(marker)
})
}
pub fn new_end_of_stream(
order: u64,
timestamp_sec: f64,
) -> Arc<Self> {
let marker = Arc::new(SyncMarker::EndOfStream);
Arc::new(Self{
Arc::new(Self {
order: order,
timestamp_sec: timestamp_sec,
segment: _AudioSegment::Sync(marker),
})
}
pub fn new_error(
order: u64,
timestamp_sec: f64,
error: String,
) -> Arc<Self> {
pub fn new_end_of_stream(order: u64, timestamp_sec: f64) -> Arc<Self> {
let marker = Arc::new(SyncMarker::EndOfStream);
Arc::new(Self {
order: order,
timestamp_sec: timestamp_sec,
segment: _AudioSegment::Sync(marker),
})
}
pub fn new_error(order: u64, timestamp_sec: f64, error: String) -> Arc<Self> {
let marker = Arc::new(SyncMarker::Error(error));
Arc::new(Self{
order: order,
timestamp_sec: timestamp_sec,
segment: _AudioSegment::Sync(marker),
})
Arc::new(Self {
order: order,
timestamp_sec: timestamp_sec,
segment: _AudioSegment::Sync(marker),
})
}
pub fn is_audio_chunk(&self) -> bool {
@@ -460,13 +455,7 @@ mod tests {
#[test]
fn test_audio_segment_gain_manipulation() {
let segment = AudioSegment::new_chunk(
0,
0.0,
vec![[100i32, 200i32]],
44100,
BitDepth::B32,
);
let segment = AudioSegment::new_chunk(0, 0.0, vec![[100i32, 200i32]], 44100, BitDepth::B32);
// Test with_gain_db
let segment_6db = segment.with_gain_db(6.0).unwrap();
@@ -486,13 +475,8 @@ mod tests {
#[test]
fn test_audio_segment_conversions() {
let segment = AudioSegment::new_chunk(
0,
0.0,
vec![[1000000i32, 2000000i32]],
44100,
BitDepth::B32,
);
let segment =
AudioSegment::new_chunk(0, 0.0, vec![[1000000i32, 2000000i32]], 44100, BitDepth::B32);
// Test to_f32_chunk
let f32_chunk = segment.to_f32_chunk();

View File

@@ -14,22 +14,6 @@ use crate::{dsp, AudioChunk, AudioChunkData, BitDepth, I24};
// Ces fonctions utilisent la fonction DSP optimisée SIMD `bitdepth_change_stereo`
// pour les conversions i32 ↔ i32 avec différents bit depths.
/// Convertit i32 vers i8 (downsampling via bit depth change)
pub fn convert_i32_to_i8(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<i8>> {
let mut stereo = chunk.clone_frames();
// Utiliser la fonction DSP optimisée pour passer de B32 → B8
dsp::bitdepth_change_stereo(&mut stereo, BitDepth::B32, BitDepth::B8);
// Convertir i32 → i8 (les valeurs sont maintenant dans la plage i8)
let stereo_i8: Vec<[i8; 2]> = stereo
.into_iter()
.map(|[l, r]| [l as i8, r as i8])
.collect();
AudioChunkData::new(stereo_i8, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i32 vers i16 (downsampling via bit depth change)
pub fn convert_i32_to_i16(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<i16>> {
let mut stereo = chunk.clone_frames();
@@ -62,21 +46,6 @@ pub fn convert_i32_to_i24(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<I24
AudioChunkData::new(stereo_i24, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i8 vers i32 (upsampling via bit depth change)
pub fn convert_i8_to_i32(chunk: &AudioChunkData<i8>) -> Arc<AudioChunkData<i32>> {
// Convertir i8 → i32 d'abord
let mut stereo: Vec<[i32; 2]> = chunk
.frames()
.iter()
.map(|[l, r]| [*l as i32, *r as i32])
.collect();
// Utiliser la fonction DSP optimisée pour passer de B8 → B32
dsp::bitdepth_change_stereo(&mut stereo, BitDepth::B8, BitDepth::B32);
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i16 vers i32 (upsampling via bit depth change)
pub fn convert_i16_to_i32(chunk: &AudioChunkData<i16>) -> Arc<AudioChunkData<i32>> {
// Convertir i16 → i32 d'abord
@@ -142,21 +111,24 @@ pub fn convert_i32_to_f64(chunk: &AudioChunkData<i32>) -> Arc<AudioChunkData<f64
convert_f32_to_f64(&f32_chunk)
}
/// Convertit I24 vers f32
/// Convertit I24 vers f32 via les fonctions DSP optimisées SIMD
pub fn convert_i24_to_f32(chunk: &AudioChunkData<I24>) -> Arc<AudioChunkData<f32>> {
let frames = chunk.frames();
let max_value = 8_388_608.0f32; // 2^23
let len = frames.len();
let stereo: Vec<[f32; 2]> = frames
.iter()
.map(|[l, r]| {
let lf = l.as_i32() as f32 / max_value;
let rf = r.as_i32() as f32 / max_value;
[lf, rf]
})
.collect();
// Séparer les canaux I24 en i32
let mut left = Vec::with_capacity(len);
let mut right = Vec::with_capacity(len);
for [l, r] in frames {
left.push(l.as_i32());
right.push(r.as_i32());
}
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
// Utiliser la fonction SIMD optimisée du module DSP pour I24
let mut out_pairs = vec![[0.0f32; 2]; len];
dsp::i24_as_i32_stereo_to_pairs_f32(&left, &right, &mut out_pairs);
AudioChunkData::new(out_pairs, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit I24 vers f64
@@ -176,21 +148,24 @@ pub fn convert_i24_to_f64(chunk: &AudioChunkData<I24>) -> Arc<AudioChunkData<f64
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i16 vers f32
/// Convertit i16 vers f32 via les fonctions DSP optimisées SIMD
pub fn convert_i16_to_f32(chunk: &AudioChunkData<i16>) -> Arc<AudioChunkData<f32>> {
let frames = chunk.frames();
let max_value = 32_768.0f32; // 2^15
let len = frames.len();
let stereo: Vec<[f32; 2]> = frames
.iter()
.map(|[l, r]| {
let lf = *l as f32 / max_value;
let rf = *r as f32 / max_value;
[lf, rf]
})
.collect();
// Séparer les canaux
let mut left = Vec::with_capacity(len);
let mut right = Vec::with_capacity(len);
for [l, r] in frames {
left.push(*l);
right.push(*r);
}
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
// Utiliser la fonction SIMD optimisée du module DSP
let mut out_pairs = vec![[0.0f32; 2]; len];
dsp::i16_stereo_to_pairs_f32(&left, &right, &mut out_pairs);
AudioChunkData::new(out_pairs, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i16 vers f64
@@ -210,40 +185,6 @@ pub fn convert_i16_to_f64(chunk: &AudioChunkData<i16>) -> Arc<AudioChunkData<f64
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i8 vers f32
pub fn convert_i8_to_f32(chunk: &AudioChunkData<i8>) -> Arc<AudioChunkData<f32>> {
let frames = chunk.frames();
let max_value = 128.0f32; // 2^7
let stereo: Vec<[f32; 2]> = frames
.iter()
.map(|[l, r]| {
let lf = *l as f32 / max_value;
let rf = *r as f32 / max_value;
[lf, rf]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit i8 vers f64
pub fn convert_i8_to_f64(chunk: &AudioChunkData<i8>) -> Arc<AudioChunkData<f64>> {
let frames = chunk.frames();
let max_value = 128.0f64; // 2^7
let stereo: Vec<[f64; 2]> = frames
.iter()
.map(|[l, r]| {
let lf = *l as f64 / max_value;
let rf = *r as f64 / max_value;
[lf, rf]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
// ============================================================================
// Conversions float → int (quantization)
// ============================================================================
@@ -279,19 +220,21 @@ pub fn convert_f64_to_i32(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i32
convert_f32_to_i32(&f32_chunk)
}
/// Convertit f32 vers I24
/// Convertit f32 vers I24 via les fonctions DSP optimisées SIMD
pub fn convert_f32_to_i24(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<I24>> {
let frames = chunk.frames();
let max_value = 8_388_607.0f32; // 2^23 - 1
let min_value = -8_388_608.0f32; // -2^23
let len = frames.len();
let stereo: Vec<[I24; 2]> = frames
.iter()
.map(|[l, r]| {
let l_scaled = (l * max_value).clamp(min_value, max_value).round() as i32;
let r_scaled = (r * max_value).clamp(min_value, max_value).round() as i32;
[I24::new_clamped(l_scaled), I24::new_clamped(r_scaled)]
})
// Utiliser la fonction SIMD optimisée du module DSP
let mut left = vec![0i32; len];
let mut right = vec![0i32; len];
dsp::pairs_f32_to_i24_as_i32_stereo(frames, &mut left, &mut right);
// Recombiner en frames I24
let stereo: Vec<[I24; 2]> = left
.into_iter()
.zip(right.into_iter())
.map(|(l, r)| [I24::new_clamped(l), I24::new_clamped(r)])
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
@@ -315,19 +258,21 @@ pub fn convert_f64_to_i24(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<I24
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit f32 vers i16
/// Convertit f32 vers i16 via les fonctions DSP optimisées SIMD
pub fn convert_f32_to_i16(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<i16>> {
let frames = chunk.frames();
let max_value = 32_767.0f32; // 2^15 - 1
let min_value = -32_768.0f32; // -2^15
let len = frames.len();
let stereo: Vec<[i16; 2]> = frames
.iter()
.map(|[l, r]| {
let l16 = (l * max_value).clamp(min_value, max_value).round() as i16;
let r16 = (r * max_value).clamp(min_value, max_value).round() as i16;
[l16, r16]
})
// Utiliser la fonction SIMD optimisée du module DSP
let mut left = vec![0i16; len];
let mut right = vec![0i16; len];
dsp::pairs_f32_to_i16_stereo(frames, &mut left, &mut right);
// Recombiner en frames
let stereo: Vec<[i16; 2]> = left
.into_iter()
.zip(right.into_iter())
.map(|(l, r)| [l, r])
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
@@ -351,42 +296,6 @@ pub fn convert_f64_to_i16(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i16
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit f32 vers i8
pub fn convert_f32_to_i8(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<i8>> {
let frames = chunk.frames();
let max_value = 127.0f32; // 2^7 - 1
let min_value = -128.0f32; // -2^7
let stereo: Vec<[i8; 2]> = frames
.iter()
.map(|[l, r]| {
let l8 = (l * max_value).clamp(min_value, max_value).round() as i8;
let r8 = (r * max_value).clamp(min_value, max_value).round() as i8;
[l8, r8]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
/// Convertit f64 vers i8
pub fn convert_f64_to_i8(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i8>> {
let frames = chunk.frames();
let max_value = 127.0f64; // 2^7 - 1
let min_value = -128.0f64; // -2^7
let stereo: Vec<[i8; 2]> = frames
.iter()
.map(|[l, r]| {
let l8 = (l * max_value).clamp(min_value, max_value).round() as i8;
let r8 = (r * max_value).clamp(min_value, max_value).round() as i8;
[l8, r8]
})
.collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
// ============================================================================
// Conversions F32 ↔ F64
// ============================================================================
@@ -394,10 +303,7 @@ pub fn convert_f64_to_i8(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<i8>>
/// Convertit f32 vers f64 (upcast simple)
pub fn convert_f32_to_f64(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<f64>> {
let frames = chunk.frames();
let stereo: Vec<[f64; 2]> = frames
.iter()
.map(|[l, r]| [*l as f64, *r as f64])
.collect();
let stereo: Vec<[f64; 2]> = frames.iter().map(|[l, r]| [*l as f64, *r as f64]).collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
@@ -405,10 +311,7 @@ pub fn convert_f32_to_f64(chunk: &AudioChunkData<f32>) -> Arc<AudioChunkData<f64
/// Convertit f64 vers f32 (downcast simple)
pub fn convert_f64_to_f32(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<f32>> {
let frames = chunk.frames();
let stereo: Vec<[f32; 2]> = frames
.iter()
.map(|[l, r]| [*l as f32, *r as f32])
.collect();
let stereo: Vec<[f32; 2]> = frames.iter().map(|[l, r]| [*l as f32, *r as f32]).collect();
AudioChunkData::new(stereo, chunk.sample_rate(), chunk.gain_db())
}
@@ -420,10 +323,9 @@ pub fn convert_f64_to_f32(chunk: &AudioChunkData<f64>) -> Arc<AudioChunkData<f32
impl AudioChunk {
/// Convertit ce chunk vers f32
///
/// Chaque type utilise sa plage native (I8=±2^7, I16=±2^15, I24=±2^23, I32=±2^31)
/// Chaque type utilise sa plage native (I16=±2^15, I24=±2^23, I32=±2^31)
pub fn to_f32(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => AudioChunk::F32(convert_i8_to_f32(d)),
AudioChunk::I16(d) => AudioChunk::F32(convert_i16_to_f32(d)),
AudioChunk::I24(d) => AudioChunk::F32(convert_i24_to_f32(d)),
AudioChunk::I32(d) => AudioChunk::F32(convert_i32_to_f32(d)),
@@ -434,10 +336,9 @@ impl AudioChunk {
/// Convertit ce chunk vers f64
///
/// Chaque type utilise sa plage native (I8=±2^7, I16=±2^15, I24=±2^23, I32=±2^31)
/// Chaque type utilise sa plage native (I16=±2^15, I24=±2^23, I32=±2^31)
pub fn to_f64(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => AudioChunk::F64(convert_i8_to_f64(d)),
AudioChunk::I16(d) => AudioChunk::F64(convert_i16_to_f64(d)),
AudioChunk::I24(d) => AudioChunk::F64(convert_i24_to_f64(d)),
AudioChunk::I32(d) => AudioChunk::F64(convert_i32_to_f64(d)),
@@ -451,7 +352,6 @@ impl AudioChunk {
/// I32 = 32 bits complets (±2^31)
pub fn to_i32(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => AudioChunk::I32(convert_i8_to_i32(d)),
AudioChunk::I16(d) => AudioChunk::I32(convert_i16_to_i32(d)),
AudioChunk::I24(d) => AudioChunk::I32(convert_i24_to_i32(d)),
AudioChunk::I32(d) => AudioChunk::I32(d.clone()),
@@ -463,11 +363,6 @@ impl AudioChunk {
/// Convertit ce chunk vers I24
pub fn to_i24(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => {
// I8 → I32 → I24
let i32_chunk = convert_i8_to_i32(d);
AudioChunk::I24(convert_i32_to_i24(&i32_chunk))
}
AudioChunk::I16(d) => {
// I16 → I32 → I24
let i32_chunk = convert_i16_to_i32(d);
@@ -483,11 +378,6 @@ impl AudioChunk {
/// Convertit ce chunk vers i16
pub fn to_i16(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => {
// I8 → I32 → I16
let i32_chunk = convert_i8_to_i32(d);
AudioChunk::I16(convert_i32_to_i16(&i32_chunk))
}
AudioChunk::I16(d) => AudioChunk::I16(d.clone()),
AudioChunk::I24(d) => {
// I24 → I32 → I16
@@ -499,26 +389,6 @@ impl AudioChunk {
AudioChunk::F64(d) => AudioChunk::I16(convert_f64_to_i16(d)),
}
}
/// Convertit ce chunk vers i8
pub fn to_i8(&self) -> AudioChunk {
match self {
AudioChunk::I8(d) => AudioChunk::I8(d.clone()),
AudioChunk::I16(d) => {
// I16 → I32 → I8
let i32_chunk = convert_i16_to_i32(d);
AudioChunk::I8(convert_i32_to_i8(&i32_chunk))
}
AudioChunk::I24(d) => {
// I24 → I32 → I8
let i32_chunk = convert_i24_to_i32(d);
AudioChunk::I8(convert_i32_to_i8(&i32_chunk))
}
AudioChunk::I32(d) => AudioChunk::I8(convert_i32_to_i8(d)),
AudioChunk::F32(d) => AudioChunk::I8(convert_f32_to_i8(d)),
AudioChunk::F64(d) => AudioChunk::I8(convert_f64_to_i8(d)),
}
}
}
// ============================================================================
@@ -527,12 +397,6 @@ impl AudioChunk {
// ---------- From<Arc<AudioChunkData<T>>> pour AudioChunk ----------
impl From<Arc<AudioChunkData<i8>>> for AudioChunk {
fn from(data: Arc<AudioChunkData<i8>>) -> Self {
AudioChunk::I8(data)
}
}
impl From<Arc<AudioChunkData<i16>>> for AudioChunk {
fn from(data: Arc<AudioChunkData<i16>>) -> Self {
AudioChunk::I16(data)
@@ -565,25 +429,6 @@ impl From<Arc<AudioChunkData<f64>>> for AudioChunk {
// ---------- From entre AudioChunkData types (sans BitDepth requis) ----------
// I8 conversions
impl From<&AudioChunkData<i8>> for Arc<AudioChunkData<i32>> {
fn from(chunk: &AudioChunkData<i8>) -> Self {
convert_i8_to_i32(chunk)
}
}
impl From<&AudioChunkData<i8>> for Arc<AudioChunkData<f32>> {
fn from(chunk: &AudioChunkData<i8>) -> Self {
convert_i8_to_f32(chunk)
}
}
impl From<&AudioChunkData<i8>> for Arc<AudioChunkData<f64>> {
fn from(chunk: &AudioChunkData<i8>) -> Self {
convert_i8_to_f64(chunk)
}
}
// I16 conversions
impl From<&AudioChunkData<i16>> for Arc<AudioChunkData<i32>> {
fn from(chunk: &AudioChunkData<i16>) -> Self {
@@ -623,11 +468,6 @@ impl From<&AudioChunkData<I24>> for Arc<AudioChunkData<f64>> {
}
// I32 conversions vers types int (downsampling)
impl From<&AudioChunkData<i32>> for Arc<AudioChunkData<i8>> {
fn from(chunk: &AudioChunkData<i32>) -> Self {
convert_i32_to_i8(chunk)
}
}
impl From<&AudioChunkData<i32>> for Arc<AudioChunkData<i16>> {
fn from(chunk: &AudioChunkData<i32>) -> Self {
@@ -661,12 +501,6 @@ impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<f64>> {
}
}
impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<i8>> {
fn from(chunk: &AudioChunkData<f32>) -> Self {
convert_f32_to_i8(chunk)
}
}
impl From<&AudioChunkData<f32>> for Arc<AudioChunkData<i16>> {
fn from(chunk: &AudioChunkData<f32>) -> Self {
convert_f32_to_i16(chunk)
@@ -692,12 +526,6 @@ impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<f32>> {
}
}
impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<i8>> {
fn from(chunk: &AudioChunkData<f64>) -> Self {
convert_f64_to_i8(chunk)
}
}
impl From<&AudioChunkData<f64>> for Arc<AudioChunkData<i16>> {
fn from(chunk: &AudioChunkData<f64>) -> Self {
convert_f64_to_i16(chunk)
@@ -785,10 +613,7 @@ mod tests {
#[test]
fn test_i24_conversions() {
let stereo = vec![
[I24::new(1_000_000).unwrap(), I24::new(-500_000).unwrap()];
10
];
let stereo = vec![[I24::new(1_000_000).unwrap(), I24::new(-500_000).unwrap()]; 10];
let chunk_i24 = AudioChunkData::new(stereo.clone(), 48_000, 0.0);
// I24 → F32 → I24
@@ -855,10 +680,7 @@ mod tests {
#[test]
fn test_from_trait_i24() {
// Test conversions I24 via From
let stereo_i24 = vec![
[I24::new(1_000_000).unwrap(), I24::new(-500_000).unwrap()];
50
];
let stereo_i24 = vec![[I24::new(1_000_000).unwrap(), I24::new(-500_000).unwrap()]; 50];
let chunk_i24 = AudioChunkData::new(stereo_i24, 48_000, 0.0);
// I24 → I32 via From
@@ -892,10 +714,7 @@ mod tests {
#[test]
fn test_from_trait_roundtrip() {
// Test round-trip I24 → F32 → I24 via From
let original = vec![
[I24::new(1_000_000).unwrap(), I24::new(-500_000).unwrap()];
10
];
let original = vec![[I24::new(1_000_000).unwrap(), I24::new(-500_000).unwrap()]; 10];
let chunk_i24 = AudioChunkData::new(original.clone(), 48_000, 0.0);
// I24 → F32 via From

View File

@@ -0,0 +1,94 @@
/// Applique un gain (en dB) sur des échantillons stéréo interleavés `[L,R]`
/// codés sur 16 bits signés.
pub fn apply_gain_stereo_i16(samples: &mut [[i16; 2]], gain_db: f64) {
let gain = 10f64.powf(gain_db / 20.0);
let g_q15 = (gain * (1u32 << 15) as f64).round() as i16;
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
unsafe {
apply_gain_stereo_i16_neon(samples, g_q15);
return;
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
unsafe {
apply_gain_stereo_i16_avx2(samples, g_q15);
return;
}
// Fallback scalaire
#[cfg(not(any(
all(target_arch = "aarch64", target_feature = "neon"),
all(target_arch = "x86_64", target_feature = "avx2")
)))]
{
apply_gain_stereo_i16_scalar(samples, g_q15);
}
}
#[cfg(not(any(
all(target_arch = "aarch64", target_feature = "neon"),
all(target_arch = "x86_64", target_feature = "avx2")
)))]
#[inline(always)]
fn apply_gain_stereo_i16_scalar(samples: &mut [[i16; 2]], g_q15: i16) {
for frame in samples.iter_mut() {
// L
let prod_l = (frame[0] as i32 * g_q15 as i32 + (1 << 14)) >> 15;
frame[0] = prod_l.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
// R
let prod_r = (frame[1] as i32 * g_q15 as i32 + (1 << 14)) >> 15;
frame[1] = prod_r.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
}
}
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
#[inline(always)]
unsafe fn apply_gain_stereo_i16_neon(samples: &mut [[i16; 2]], g_q15: i16) {
use core::arch::aarch64::*;
let gvec = vdupq_n_s16(g_q15);
let mut i = 0;
let n = samples.len() * 2;
let ptr = samples.as_mut_ptr() as *mut i16;
while i + 8 <= n {
let v = vld1q_s16(ptr.add(i));
let res = vqdmulhq_s16(v, gvec); // Q15 multiply high
vst1q_s16(ptr.add(i), res);
i += 8;
}
// reste scalaire
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
apply_gain_i16_scalar(slice, g_q15);
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
#[inline(always)]
unsafe fn apply_gain_stereo_i16_avx2(samples: &mut [[i16; 2]], g_q15: i16) {
use core::arch::x86_64::*;
let g = _mm256_set1_epi16(g_q15 as i16);
let mut i = 0;
let n = samples.len() * 2;
let ptr = samples.as_mut_ptr() as *mut i16;
while i + 16 <= n {
let x = _mm256_loadu_si256(ptr.add(i) as *const __m256i);
let hi = _mm256_mulhi_epi16(x, g);
_mm256_storeu_si256(ptr.add(i) as *mut __m256i, hi);
i += 16;
}
// reste scalaire
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
apply_gain_i16_scalar(slice, g_q15);
}
/// version mono utilisée pour le reste scalaire
#[inline(always)]
fn apply_gain_i16_scalar(samples: &mut [i16], g_q15: i16) {
for s in samples.iter_mut() {
let prod = (*s as i32 * g_q15 as i32 + (1 << 14)) >> 15;
*s = prod.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
}
}

View File

@@ -0,0 +1,99 @@
use crate::I24;
/// Applique un gain (en dB) sur des échantillons stéréo interleavés `[L,R]`
/// codés sur 24 bits signés (`I24`).
pub fn apply_gain_stereo_i24(samples: &mut [[I24; 2]], gain_db: f64) {
let gain = 10f64.powf(gain_db / 20.0);
// Q23 scaling
let g_q23 = (gain * (1u64 << 23) as f64).round() as i32;
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
unsafe {
apply_gain_stereo_i24_neon(samples, g_q23);
return;
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
unsafe {
apply_gain_stereo_i24_avx2(samples, g_q23);
return;
}
// Fallback scalaire
#[cfg(not(any(
all(target_arch = "aarch64", target_feature = "neon"),
all(target_arch = "x86_64", target_feature = "avx2")
)))]
{
apply_gain_stereo_i24_scalar(samples, g_q23);
}
}
#[cfg(not(any(
all(target_arch = "aarch64", target_feature = "neon"),
all(target_arch = "x86_64", target_feature = "avx2")
)))]
#[inline(always)]
fn apply_gain_stereo_i24_scalar(samples: &mut [[I24; 2]], g_q23: i32) {
for frame in samples.iter_mut() {
// L
let prod_l = (frame[0].as_i32() as i64 * g_q23 as i64 + (1 << 22)) >> 23;
let clamped_l = prod_l.clamp(I24::MIN_VALUE as i64, I24::MAX_VALUE as i64) as i32;
frame[0] = I24::new_clamped(clamped_l);
// R
let prod_r = (frame[1].as_i32() as i64 * g_q23 as i64 + (1 << 22)) >> 23;
let clamped_r = prod_r.clamp(I24::MIN_VALUE as i64, I24::MAX_VALUE as i64) as i32;
frame[1] = I24::new_clamped(clamped_r);
}
}
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
#[inline(always)]
unsafe fn apply_gain_stereo_i24_neon(samples: &mut [[I24; 2]], g_q23: i32) {
use core::arch::aarch64::*;
let gvec = vdupq_n_s32(g_q23);
let mut i = 0;
let n = samples.len() * 2;
let ptr = samples.as_mut_ptr() as *mut i32;
while i + 4 <= n {
let v = vld1q_s32(ptr.add(i));
let res = vqdmulhq_s32(v, gvec); // Q23 multiply high
vst1q_s32(ptr.add(i), res);
i += 4;
}
// reste scalaire
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
apply_gain_i24_scalar(slice, g_q23);
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
#[inline(always)]
unsafe fn apply_gain_stereo_i24_avx2(samples: &mut [[I24; 2]], g_q23: i32) {
use core::arch::x86_64::*;
let g = _mm256_set1_epi32(g_q23);
let mut i = 0;
let n = samples.len() * 2;
let ptr = samples.as_mut_ptr() as *mut i32;
while i + 8 <= n {
let x = _mm256_loadu_si256(ptr.add(i) as *const __m256i);
let hi = _mm256_mulhi_epi32(x, g);
_mm256_storeu_si256(ptr.add(i) as *mut __m256i, hi);
i += 8;
}
// reste scalaire
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
apply_gain_i24_scalar(slice, g_q23);
}
/// Version mono utilisée pour le reste scalaire.
#[inline(always)]
fn apply_gain_i24_scalar(samples: &mut [i32], g_q23: i32) {
for s in samples.iter_mut() {
let prod = (*s as i64 * g_q23 as i64 + (1 << 22)) >> 23;
*s = prod.clamp(I24::MIN_VALUE as i64, I24::MAX_VALUE as i64) as i32;
}
}

View File

@@ -1,16 +1,16 @@
/// Applique un gain (en dB) sur des échantillons stéréo interleavés `[L,R]`.
pub fn apply_gain_stereo(samples: &mut [[i32; 2]], gain_db: f64) {
pub fn apply_gain_stereo_i32(samples: &mut [[i32; 2]], gain_db: f64) {
let gain = 10f64.powf(gain_db / 20.0);
let g_q31 = (gain * (1u64 << 31) as f64).round() as i32;
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
unsafe {
apply_gain_stereo_neon(samples, g_q31);
apply_gain_stereo_i32_neon(samples, g_q31);
return;
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
unsafe {
apply_gain_stereo_avx2(samples, g_q31);
apply_gain_stereo_i32_avx2(samples, g_q31);
return;
}
@@ -20,7 +20,7 @@ pub fn apply_gain_stereo(samples: &mut [[i32; 2]], gain_db: f64) {
all(target_arch = "x86_64", target_feature = "avx2")
)))]
{
apply_gain_stereo_scalar(samples, g_q31);
apply_gain_stereo_i32_scalar(samples, g_q31);
}
}
@@ -29,7 +29,7 @@ pub fn apply_gain_stereo(samples: &mut [[i32; 2]], gain_db: f64) {
all(target_arch = "x86_64", target_feature = "avx2")
)))]
#[inline(always)]
fn apply_gain_stereo_scalar(samples: &mut [[i32; 2]], g_q31: i32) {
fn apply_gain_stereo_i32_scalar(samples: &mut [[i32; 2]], g_q31: i32) {
for frame in samples.iter_mut() {
// L
let prod_l = (frame[0] as i64 * g_q31 as i64 + (1 << 30)) >> 31;
@@ -43,7 +43,7 @@ fn apply_gain_stereo_scalar(samples: &mut [[i32; 2]], g_q31: i32) {
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
#[inline(always)]
unsafe fn apply_gain_stereo_neon(samples: &mut [[i32; 2]], g_q31: i32) {
unsafe fn apply_gain_stereo_i32_neon(samples: &mut [[i32; 2]], g_q31: i32) {
use core::arch::aarch64::*;
let gvec = vdupq_n_s32(g_q31);
let mut i = 0;
@@ -59,12 +59,12 @@ unsafe fn apply_gain_stereo_neon(samples: &mut [[i32; 2]], g_q31: i32) {
// reste scalaire
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
apply_gain_scalar(slice, g_q31);
apply_gain_i32_scalar(slice, g_q31);
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
#[inline(always)]
unsafe fn apply_gain_stereo_avx2(samples: &mut [[i32; 2]], g_q31: i32) {
unsafe fn apply_gain_stereo_i32_avx2(samples: &mut [[i32; 2]], g_q31: i32) {
use core::arch::x86_64::*;
let g = _mm256_set1_epi32(g_q31);
let mut i = 0;
@@ -80,12 +80,12 @@ unsafe fn apply_gain_stereo_avx2(samples: &mut [[i32; 2]], g_q31: i32) {
// reste scalaire
let slice = std::slice::from_raw_parts_mut(ptr.add(i), n - i);
apply_gain_scalar(slice, g_q31);
apply_gain_i32_scalar(slice, g_q31);
}
/// version mono utilisée pour le reste scalaire
#[inline(always)]
fn apply_gain_scalar(samples: &mut [i32], g_q31: i32) {
fn apply_gain_i32_scalar(samples: &mut [i32], g_q31: i32) {
for s in samples.iter_mut() {
let prod = (*s as i64 * g_q31 as i64 + (1 << 30)) >> 31;
*s = prod.clamp(i32::MIN as i64, i32::MAX as i64) as i32;

View File

@@ -1,5 +1,5 @@
use bytemuck::{cast_slice, cast_slice_mut};
use crate::BitDepth;
use bytemuck::{cast_slice, cast_slice_mut};
#[cfg(feature = "simd")]
use std::simd::num::{SimdFloat, SimdInt};
@@ -183,3 +183,307 @@ pub fn interleaved_f32_to_i32_stereo(
let input_pairs: &[[f32; 2]] = cast_slice(input_interleaved);
pairs_f32_to_i32_stereo(input_pairs, left, right, bit_depth);
}
/* ====================== CONVERSIONS I16 ↔ F32 SIMD ====================== */
/// Convertit deux canaux i16 (L/R) en pairs f32 normalisées [-1.0, 1.0]
#[cfg(feature = "simd")]
fn i16_stereo_to_pairs_f32_inner(
left: &[i16],
right: &[i16],
out_pairs: &mut [[f32; 2]],
max_value: f32,
) {
debug_assert_eq!(left.len(), right.len());
debug_assert_eq!(out_pairs.len(), left.len());
const LANES: usize = 8;
type Vf32 = Simd<f32, LANES>;
type Vi32 = Simd<i32, LANES>;
let scale = Vf32::splat(1.0 / max_value);
let (l_chunks, l_tail) = left.as_chunks::<LANES>();
let (r_chunks, r_tail) = right.as_chunks::<LANES>();
let (o_chunks, o_tail) = out_pairs.as_chunks_mut::<LANES>();
for (k, o) in o_chunks.iter_mut().enumerate() {
// Charger i16, caster en i32 puis en f32
let l_arr: [i32; LANES] = std::array::from_fn(|i| l_chunks[k][i] as i32);
let r_arr: [i32; LANES] = std::array::from_fn(|i| r_chunks[k][i] as i32);
let l = Vi32::from_array(l_arr).cast::<f32>() * scale;
let r = Vi32::from_array(r_arr).cast::<f32>() * scale;
for j in 0..LANES {
unsafe {
*o.get_unchecked_mut(j) = [l[j], r[j]];
}
}
}
let scale_scalar = 1.0 / max_value;
for (dst, (&l, &r)) in o_tail.iter_mut().zip(l_tail.iter().zip(r_tail.iter())) {
dst[0] = l as f32 * scale_scalar;
dst[1] = r as f32 * scale_scalar;
}
}
#[cfg(not(feature = "simd"))]
fn i16_stereo_to_pairs_f32_inner(
left: &[i16],
right: &[i16],
out_pairs: &mut [[f32; 2]],
max_value: f32,
) {
debug_assert_eq!(left.len(), right.len());
debug_assert_eq!(out_pairs.len(), left.len());
let scale = 1.0 / max_value;
for ((out, &l), &r) in out_pairs.iter_mut().zip(left).zip(right) {
out[0] = l as f32 * scale;
out[1] = r as f32 * scale;
}
}
/// Convertit deux canaux i16 (L/R) en pairs f32 normalisées [-1.0, 1.0]
pub fn i16_stereo_to_pairs_f32(
left: &[i16],
right: &[i16],
out_pairs: &mut [[f32; 2]],
) {
i16_stereo_to_pairs_f32_inner(left, right, out_pairs, 32768.0);
}
/// Convertit pairs f32 normalisées [-1.0, 1.0] en deux canaux i16 (L/R)
#[cfg(feature = "simd")]
fn pairs_f32_to_i16_stereo_inner(
input_pairs: &[[f32; 2]],
left: &mut [i16],
right: &mut [i16],
max_value: f32,
) {
debug_assert_eq!(input_pairs.len(), left.len());
debug_assert_eq!(input_pairs.len(), right.len());
const LANES: usize = 8;
type Vf32 = Simd<f32, LANES>;
let vmin = -max_value;
let vmax_clamp = max_value - 1.0;
let vscale = Vf32::splat(max_value);
let vminv = Vf32::splat(vmin);
let vmaxv = Vf32::splat(vmax_clamp);
let (in_chunks, in_tail) = input_pairs.as_chunks::<LANES>();
let (l_chunks, l_tail) = left.as_chunks_mut::<LANES>();
let (r_chunks, r_tail) = right.as_chunks_mut::<LANES>();
for (k, blk) in in_chunks.iter().enumerate() {
let mut l_arr = [0.0f32; LANES];
let mut r_arr = [0.0f32; LANES];
for j in 0..LANES {
let p = blk[j];
l_arr[j] = p[0];
r_arr[j] = p[1];
}
let lq = (Vf32::from_array(l_arr) * vscale)
.simd_clamp(vminv, vmaxv)
.round()
.cast::<i32>();
let rq = (Vf32::from_array(r_arr) * vscale)
.simd_clamp(vminv, vmaxv)
.round()
.cast::<i32>();
for j in 0..LANES {
l_chunks[k][j] = lq[j] as i16;
r_chunks[k][j] = rq[j] as i16;
}
}
for (j, (l, r)) in in_tail.iter().zip(l_tail.iter_mut().zip(r_tail.iter_mut())) {
let lx = (j[0] * max_value).clamp(vmin, vmax_clamp).round();
let rx = (j[1] * max_value).clamp(vmin, vmax_clamp).round();
*l = lx as i16;
*r = rx as i16;
}
}
#[cfg(not(feature = "simd"))]
fn pairs_f32_to_i16_stereo_inner(
input_pairs: &[[f32; 2]],
left: &mut [i16],
right: &mut [i16],
max_value: f32,
) {
debug_assert_eq!(input_pairs.len(), left.len());
debug_assert_eq!(input_pairs.len(), right.len());
let vmin = -max_value;
let vmax_clamp = max_value - 1.0;
for (i, pair) in input_pairs.iter().enumerate() {
let lx = (pair[0] * max_value).clamp(vmin, vmax_clamp).round();
let rx = (pair[1] * max_value).clamp(vmin, vmax_clamp).round();
left[i] = lx as i16;
right[i] = rx as i16;
}
}
/// Convertit pairs f32 normalisées [-1.0, 1.0] en deux canaux i16 (L/R)
pub fn pairs_f32_to_i16_stereo(
input_pairs: &[[f32; 2]],
left: &mut [i16],
right: &mut [i16],
) {
pairs_f32_to_i16_stereo_inner(input_pairs, left, right, 32768.0);
}
/* ====================== CONVERSIONS I24 ↔ F32 SIMD ====================== */
/// Convertit deux canaux i32 (contenant des valeurs I24) en pairs f32 normalisées
#[cfg(feature = "simd")]
fn i24_as_i32_stereo_to_pairs_f32_inner(
left: &[i32],
right: &[i32],
out_pairs: &mut [[f32; 2]],
max_value: f32,
) {
debug_assert_eq!(left.len(), right.len());
debug_assert_eq!(out_pairs.len(), left.len());
const LANES: usize = 8;
type Vf32 = Simd<f32, LANES>;
type Vi32 = Simd<i32, LANES>;
let scale = Vf32::splat(1.0 / max_value);
let (l_chunks, l_tail) = left.as_chunks::<LANES>();
let (r_chunks, r_tail) = right.as_chunks::<LANES>();
let (o_chunks, o_tail) = out_pairs.as_chunks_mut::<LANES>();
for (k, o) in o_chunks.iter_mut().enumerate() {
let l = Vi32::from_slice(&l_chunks[k]).cast::<f32>() * scale;
let r = Vi32::from_slice(&r_chunks[k]).cast::<f32>() * scale;
for j in 0..LANES {
unsafe {
*o.get_unchecked_mut(j) = [l[j], r[j]];
}
}
}
let scale_scalar = 1.0 / max_value;
for (dst, (&l, &r)) in o_tail.iter_mut().zip(l_tail.iter().zip(r_tail.iter())) {
dst[0] = l as f32 * scale_scalar;
dst[1] = r as f32 * scale_scalar;
}
}
#[cfg(not(feature = "simd"))]
fn i24_as_i32_stereo_to_pairs_f32_inner(
left: &[i32],
right: &[i32],
out_pairs: &mut [[f32; 2]],
max_value: f32,
) {
debug_assert_eq!(left.len(), right.len());
debug_assert_eq!(out_pairs.len(), left.len());
let scale = 1.0 / max_value;
for ((out, &l), &r) in out_pairs.iter_mut().zip(left).zip(right) {
out[0] = l as f32 * scale;
out[1] = r as f32 * scale;
}
}
/// Convertit deux canaux i32 (contenant des valeurs I24) en pairs f32 normalisées
pub fn i24_as_i32_stereo_to_pairs_f32(
left: &[i32],
right: &[i32],
out_pairs: &mut [[f32; 2]],
) {
i24_as_i32_stereo_to_pairs_f32_inner(left, right, out_pairs, 8388608.0);
}
/// Convertit pairs f32 normalisées en deux canaux i32 (valeurs I24 range)
#[cfg(feature = "simd")]
fn pairs_f32_to_i24_as_i32_stereo_inner(
input_pairs: &[[f32; 2]],
left: &mut [i32],
right: &mut [i32],
max_value: f32,
) {
debug_assert_eq!(input_pairs.len(), left.len());
debug_assert_eq!(input_pairs.len(), right.len());
const LANES: usize = 8;
type Vf32 = Simd<f32, LANES>;
let vmin = -max_value;
let vmax_clamp = max_value - 1.0;
let vscale = Vf32::splat(max_value);
let vminv = Vf32::splat(vmin);
let vmaxv = Vf32::splat(vmax_clamp);
let (in_chunks, in_tail) = input_pairs.as_chunks::<LANES>();
let (l_chunks, l_tail) = left.as_chunks_mut::<LANES>();
let (r_chunks, r_tail) = right.as_chunks_mut::<LANES>();
for (k, blk) in in_chunks.iter().enumerate() {
let mut l_arr = [0.0f32; LANES];
let mut r_arr = [0.0f32; LANES];
for j in 0..LANES {
let p = blk[j];
l_arr[j] = p[0];
r_arr[j] = p[1];
}
let lq = (Vf32::from_array(l_arr) * vscale)
.simd_clamp(vminv, vmaxv)
.round();
let rq = (Vf32::from_array(r_arr) * vscale)
.simd_clamp(vminv, vmaxv)
.round();
lq.cast::<i32>().copy_to_slice(&mut l_chunks[k]);
rq.cast::<i32>().copy_to_slice(&mut r_chunks[k]);
}
for (j, (l, r)) in in_tail.iter().zip(l_tail.iter_mut().zip(r_tail.iter_mut())) {
let lx = (j[0] * max_value).clamp(vmin, vmax_clamp).round();
let rx = (j[1] * max_value).clamp(vmin, vmax_clamp).round();
*l = lx as i32;
*r = rx as i32;
}
}
#[cfg(not(feature = "simd"))]
fn pairs_f32_to_i24_as_i32_stereo_inner(
input_pairs: &[[f32; 2]],
left: &mut [i32],
right: &mut [i32],
max_value: f32,
) {
debug_assert_eq!(input_pairs.len(), left.len());
debug_assert_eq!(input_pairs.len(), right.len());
let vmin = -max_value;
let vmax_clamp = max_value - 1.0;
for (i, pair) in input_pairs.iter().enumerate() {
let lx = (pair[0] * max_value).clamp(vmin, vmax_clamp).round();
let rx = (pair[1] * max_value).clamp(vmin, vmax_clamp).round();
left[i] = lx as i32;
right[i] = rx as i32;
}
}
/// Convertit pairs f32 normalisées en deux canaux i32 (valeurs I24 range)
pub fn pairs_f32_to_i24_as_i32_stereo(
input_pairs: &[[f32; 2]],
left: &mut [i32],
right: &mut [i32],
) {
pairs_f32_to_i24_as_i32_stereo_inner(input_pairs, left, right, 8388608.0);
}

View File

@@ -1,15 +1,21 @@
//! Module DSP pour les conversions et traitements audio optimisés (SIMD)
pub mod depth;
pub mod gain;
pub mod gain_16bits;
pub mod gain_24bits;
pub mod gain_32bits;
pub mod int_float;
pub mod resampling;
pub use depth::bitdepth_change_stereo;
pub use gain::apply_gain_stereo;
pub use gain_16bits::apply_gain_stereo_i16;
pub use gain_24bits::apply_gain_stereo_i24;
pub use gain_32bits::apply_gain_stereo_i32;
pub use int_float::{
i32_stereo_to_interleaved_f32, i32_stereo_to_pairs_f32, interleaved_f32_to_i32_stereo,
pairs_f32_to_i32_stereo,
i16_stereo_to_pairs_f32, i24_as_i32_stereo_to_pairs_f32, i32_stereo_to_interleaved_f32,
i32_stereo_to_pairs_f32, interleaved_f32_to_i32_stereo, pairs_f32_to_i16_stereo,
pairs_f32_to_i24_as_i32_stereo, pairs_f32_to_i32_stereo,
};
pub use resampling::resampling;

View File

@@ -81,12 +81,13 @@ async fn main() {
use std::simd::*;
mod audio_chunk;
pub mod events;
// mod nodes; // Temporairement déplacé hors du module
mod sync_marker;
mod audio_segment;
mod sample_types;
pub mod conversions;
pub mod events;
pub mod nodes;
mod sample_types;
mod sync_marker;
pub mod type_constraints;
#[macro_use]
mod macros;
@@ -94,18 +95,32 @@ pub mod bit_depth;
pub mod dsp;
pub use audio_segment::{AudioSegment, _AudioSegment};
pub use sync_marker::{SyncMarker};
pub use sync_marker::SyncMarker;
pub use audio_chunk::{AudioChunk, AudioChunkData, db_to_linear, gain_db_from_linear, gain_linear_from_db, linear_to_db};
pub use audio_chunk::{
gain_db_from_linear, gain_linear_from_db, AudioChunk, AudioChunkData, AudioFloatChunk,
AudioIntegerChunk,
};
pub use bit_depth::{Bit16, Bit24, Bit32, Bit8, BitDepth};
pub use sample_types::{I24, Sample};
pub use sample_types::{Sample, I24};
pub use type_constraints::{
check_compatibility, SampleType, TypeCategory, TypeMismatch, TypeRequirement,
};
pub use events::{
AudioDataEvent, EventPublisher, EventReceiver, NodeEvent, NodeListener, SourceNameUpdateEvent,
VolumeChangeEvent,
};
// Exports publics des nodes
pub use nodes::{
converter_nodes::{ToF32Node, ToF64Node, ToI16Node, ToI24Node, ToI32Node},
file_source::FileSource,
flac_file_sink::{FlacFileSink, FlacFileSinkStats},
http_source::HttpSource,
AudioError, AudioNode, MultiSubscriberNode, SingleSubscriberNode, TypedAudioNode,
};
// Nodes temporairement désactivés
/*
pub use nodes::{
@@ -114,13 +129,10 @@ pub use nodes::{
decoder_node::DecoderNode,
disk_sink::{AudioFileFormat, DiskSink, DiskSinkConfig, DiskSinkStats},
dsp_node::DspNode,
file_source::FileSource,
flac_file_sink::{FlacFileSink, FlacFileSinkStats},
mpd_sink::{MpdAudioFormat, MpdConfig, MpdHandle, MpdSink, MpdStats},
sink_node::{SinkNode, SinkStats},
source_node::SourceNode,
timer_node::{TimerHandle, TimerNode},
volume_node::{HardwareVolumeNode, VolumeHandle, VolumeNode},
AudioError, AudioNode, MultiSubscriberNode, SingleSubscriberNode,
};
*/

View File

@@ -14,12 +14,6 @@
/// ```
#[macro_export]
macro_rules! extract_chunk_data {
($chunk:expr, I8) => {
match $chunk {
$crate::AudioChunk::I8(data) => Some(data),
_ => None,
}
};
($chunk:expr, I16) => {
match $chunk {
$crate::AudioChunk::I16(data) => Some(data),
@@ -68,7 +62,6 @@ macro_rules! extract_chunk_data {
macro_rules! match_chunk {
($chunk:expr, $data:ident => $body:expr) => {
match $chunk {
$crate::AudioChunk::I8($data) => $body,
$crate::AudioChunk::I16($data) => $body,
$crate::AudioChunk::I24($data) => $body,
$crate::AudioChunk::I32($data) => $body,
@@ -94,24 +87,11 @@ macro_rules! match_chunk {
macro_rules! map_chunk {
($chunk:expr, $data:ident => $transform:expr) => {
match $chunk {
$crate::AudioChunk::I8($data) => {
$crate::AudioChunk::I8($transform)
}
$crate::AudioChunk::I16($data) => {
$crate::AudioChunk::I16($transform)
}
$crate::AudioChunk::I24($data) => {
$crate::AudioChunk::I24($transform)
}
$crate::AudioChunk::I32($data) => {
$crate::AudioChunk::I32($transform)
}
$crate::AudioChunk::F32($data) => {
$crate::AudioChunk::F32($transform)
}
$crate::AudioChunk::F64($data) => {
$crate::AudioChunk::F64($transform)
}
$crate::AudioChunk::I16($data) => $crate::AudioChunk::I16($transform),
$crate::AudioChunk::I24($data) => $crate::AudioChunk::I24($transform),
$crate::AudioChunk::I32($data) => $crate::AudioChunk::I32($transform),
$crate::AudioChunk::F32($data) => $crate::AudioChunk::F32($transform),
$crate::AudioChunk::F64($data) => $crate::AudioChunk::F64($transform),
}
};
}
@@ -130,9 +110,6 @@ macro_rules! map_chunk {
/// ```
#[macro_export]
macro_rules! is_chunk_type {
($chunk:expr, I8) => {
matches!($chunk, $crate::AudioChunk::I8(_))
};
($chunk:expr, I16) => {
matches!($chunk, $crate::AudioChunk::I16(_))
};
@@ -280,13 +257,15 @@ mod tests {
let segment = AudioSegment::new_hearbeat(1, 1.0);
assert!(extract_sync_marker!(&*segment).is_some());
let audio_segment = AudioSegment::new_chunk(0, 0.0, vec![[100i32, 200i32]], 44100, BitDepth::B32);
let audio_segment =
AudioSegment::new_chunk(0, 0.0, vec![[100i32, 200i32]], 44100, BitDepth::B32);
assert!(extract_sync_marker!(&*audio_segment).is_none());
}
#[test]
fn test_match_segment() {
let audio_segment = AudioSegment::new_chunk(0, 0.0, vec![[100i32, 200i32]], 44100, BitDepth::B32);
let audio_segment =
AudioSegment::new_chunk(0, 0.0, vec![[100i32, 200i32]], 44100, BitDepth::B32);
let result = match_segment!(&*audio_segment,
chunk => format!("audio: {}", chunk.type_name()),

View File

@@ -0,0 +1,472 @@
//! Nodes de conversion de type pour AudioChunk
//!
//! Ces nodes permettent de convertir les chunks audio d'un type vers un autre
//! (I16, I24, I32, F32, F64). Toutes les conversions utilisent les fonctions
//! DSP optimisées SIMD du module `crate::conversions`.
//!
//! Le designer de pipeline doit insérer manuellement ces nodes pour gérer
//! les incompatibilités de type entre producers et consumers.
use crate::{
nodes::{AudioError, MultiSubscriberNode, TypedAudioNode},
type_constraints::{SampleType, TypeRequirement},
AudioSegment,
};
use std::sync::Arc;
use tokio::sync::mpsc;
/// Node de conversion vers I16
///
/// Convertit n'importe quel type de chunk audio vers I16 (16-bit signed integer).
/// Utilise les conversions DSP SIMD optimisées.
pub struct ToI16Node {
rx: mpsc::Receiver<Arc<AudioSegment>>,
subscribers: MultiSubscriberNode,
}
impl ToI16Node {
/// Crée un nouveau node de conversion vers I16
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(16)
}
/// Crée un nouveau node avec une taille de buffer spécifique
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
};
(node, tx)
}
/// Ajoute un abonné qui recevra les segments audio convertis
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le traitement de conversion
pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(segment) = self.rx.recv().await {
// Si c'est un syncmarker, passer directement
if !segment.is_audio_chunk() {
self.subscribers.push(segment).await?;
continue;
}
// Convertir le chunk audio vers I16
let converted_segment = if let Some(chunk) = segment.as_chunk() {
let converted_chunk = chunk.to_i16();
Arc::new(AudioSegment {
order: segment.order,
timestamp_sec: segment.timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
})
} else {
segment
};
self.subscribers.push(converted_segment).await?;
}
Ok(())
}
}
impl TypedAudioNode for ToI16Node {
fn input_type(&self) -> Option<TypeRequirement> {
// Accepte n'importe quel type
Some(TypeRequirement::any())
}
fn output_type(&self) -> Option<TypeRequirement> {
// Produit uniquement I16
Some(TypeRequirement::specific(SampleType::I16))
}
}
impl Default for ToI16Node {
fn default() -> Self {
Self::new().0
}
}
/// Node de conversion vers I24
///
/// Convertit n'importe quel type de chunk audio vers I24 (24-bit signed integer).
/// Utilise les conversions DSP SIMD optimisées.
pub struct ToI24Node {
rx: mpsc::Receiver<Arc<AudioSegment>>,
subscribers: MultiSubscriberNode,
}
impl ToI24Node {
/// Crée un nouveau node de conversion vers I24
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(16)
}
/// Crée un nouveau node avec une taille de buffer spécifique
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
};
(node, tx)
}
/// Ajoute un abonné qui recevra les segments audio convertis
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le traitement de conversion
pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(segment) = self.rx.recv().await {
if !segment.is_audio_chunk() {
self.subscribers.push(segment).await?;
continue;
}
let converted_segment = if let Some(chunk) = segment.as_chunk() {
let converted_chunk = chunk.to_i24();
Arc::new(AudioSegment {
order: segment.order,
timestamp_sec: segment.timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
})
} else {
segment
};
self.subscribers.push(converted_segment).await?;
}
Ok(())
}
}
impl TypedAudioNode for ToI24Node {
fn input_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::any())
}
fn output_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::specific(SampleType::I24))
}
}
impl Default for ToI24Node {
fn default() -> Self {
Self::new().0
}
}
/// Node de conversion vers I32
///
/// Convertit n'importe quel type de chunk audio vers I32 (32-bit signed integer).
/// Utilise les conversions DSP SIMD optimisées.
pub struct ToI32Node {
rx: mpsc::Receiver<Arc<AudioSegment>>,
subscribers: MultiSubscriberNode,
}
impl ToI32Node {
/// Crée un nouveau node de conversion vers I32
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(16)
}
/// Crée un nouveau node avec une taille de buffer spécifique
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
};
(node, tx)
}
/// Ajoute un abonné qui recevra les segments audio convertis
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le traitement de conversion
pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(segment) = self.rx.recv().await {
if !segment.is_audio_chunk() {
self.subscribers.push(segment).await?;
continue;
}
let converted_segment = if let Some(chunk) = segment.as_chunk() {
let converted_chunk = chunk.to_i32();
Arc::new(AudioSegment {
order: segment.order,
timestamp_sec: segment.timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
})
} else {
segment
};
self.subscribers.push(converted_segment).await?;
}
Ok(())
}
}
impl TypedAudioNode for ToI32Node {
fn input_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::any())
}
fn output_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::specific(SampleType::I32))
}
}
impl Default for ToI32Node {
fn default() -> Self {
Self::new().0
}
}
/// Node de conversion vers F32
///
/// Convertit n'importe quel type de chunk audio vers F32 (32-bit floating point).
/// Utilise les conversions DSP SIMD optimisées.
pub struct ToF32Node {
rx: mpsc::Receiver<Arc<AudioSegment>>,
subscribers: MultiSubscriberNode,
}
impl ToF32Node {
/// Crée un nouveau node de conversion vers F32
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(16)
}
/// Crée un nouveau node avec une taille de buffer spécifique
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
};
(node, tx)
}
/// Ajoute un abonné qui recevra les segments audio convertis
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le traitement de conversion
pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(segment) = self.rx.recv().await {
if !segment.is_audio_chunk() {
self.subscribers.push(segment).await?;
continue;
}
let converted_segment = if let Some(chunk) = segment.as_chunk() {
let converted_chunk = chunk.to_f32();
Arc::new(AudioSegment {
order: segment.order,
timestamp_sec: segment.timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
})
} else {
segment
};
self.subscribers.push(converted_segment).await?;
}
Ok(())
}
}
impl TypedAudioNode for ToF32Node {
fn input_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::any())
}
fn output_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::specific(SampleType::F32))
}
}
impl Default for ToF32Node {
fn default() -> Self {
Self::new().0
}
}
/// Node de conversion vers F64
///
/// Convertit n'importe quel type de chunk audio vers F64 (64-bit floating point).
/// Utilise les conversions DSP SIMD optimisées.
pub struct ToF64Node {
rx: mpsc::Receiver<Arc<AudioSegment>>,
subscribers: MultiSubscriberNode,
}
impl ToF64Node {
/// Crée un nouveau node de conversion vers F64
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(16)
}
/// Crée un nouveau node avec une taille de buffer spécifique
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
};
(node, tx)
}
/// Ajoute un abonné qui recevra les segments audio convertis
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le traitement de conversion
pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(segment) = self.rx.recv().await {
if !segment.is_audio_chunk() {
self.subscribers.push(segment).await?;
continue;
}
let converted_segment = if let Some(chunk) = segment.as_chunk() {
let converted_chunk = chunk.to_f64();
Arc::new(AudioSegment {
order: segment.order,
timestamp_sec: segment.timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
})
} else {
segment
};
self.subscribers.push(converted_segment).await?;
}
Ok(())
}
}
impl TypedAudioNode for ToF64Node {
fn input_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::any())
}
fn output_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::specific(SampleType::F64))
}
}
impl Default for ToF64Node {
fn default() -> Self {
Self::new().0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AudioChunk, AudioChunkData};
#[tokio::test]
async fn test_to_f32_node_type_requirements() {
let (node, _tx) = ToF32Node::new();
// Vérifier les types d'entrée/sortie
assert_eq!(
node.input_type().unwrap().get_accepted_types().len(),
5,
"Should accept all 5 types"
);
assert_eq!(
node.output_type()
.unwrap()
.get_accepted_types()
.first()
.copied(),
Some(SampleType::F32),
"Should output F32 only"
);
}
#[tokio::test]
async fn test_to_i16_node_converts_from_i32() {
let (mut node, tx) = ToI16Node::new();
let (out_tx, mut out_rx) = mpsc::channel(16);
node.add_subscriber(out_tx);
// Lancer le node dans une tâche
let handle = tokio::spawn(async move { node.run().await });
// Créer et envoyer un chunk I32
let stereo = vec![[1_000_000i32 << 16, -500_000i32 << 16]; 100];
let chunk_data = AudioChunkData::new(stereo.clone(), 48_000, 0.0);
let chunk = AudioChunk::I32(chunk_data);
let segment = Arc::new(AudioSegment {
order: 0,
timestamp_sec: 0.0,
segment: crate::_AudioSegment::Chunk(Arc::new(chunk)),
});
tx.send(segment).await.unwrap();
drop(tx);
// Recevoir le chunk converti
let result = out_rx.recv().await.unwrap();
assert!(result.is_audio_chunk());
if let Some(converted) = result.as_chunk() {
assert_eq!(converted.type_name(), "i16");
assert_eq!(converted.len(), 100);
// Vérifier la conversion (downsampling de I32 vers I16)
if let AudioChunk::I16(data) = &**converted {
for (orig, converted_frame) in stereo.iter().zip(data.frames().iter()) {
let expected_l = (orig[0] >> 16) as i16;
let expected_r = (orig[1] >> 16) as i16;
assert_eq!(converted_frame[0], expected_l);
assert_eq!(converted_frame[1], expected_r);
}
}
}
handle.await.unwrap().unwrap();
}
#[tokio::test]
async fn test_syncmarkers_passthrough() {
let (mut node, tx) = ToI16Node::new();
let (out_tx, mut out_rx) = mpsc::channel(16);
node.add_subscriber(out_tx);
tokio::spawn(async move {
node.run().await.unwrap();
});
// Envoyer un syncmarker
let top_zero = AudioSegment::new_top_zero_sync();
tx.send(top_zero.clone()).await.unwrap();
drop(tx);
// Recevoir le syncmarker
let result = out_rx.recv().await.unwrap();
assert!(!result.is_audio_chunk());
assert!(result.as_sync_marker().is_some());
}
}

View File

@@ -0,0 +1,426 @@
use crate::{
nodes::{AudioError, MultiSubscriberNode, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
type_constraints::TypeRequirement,
AudioChunk, AudioChunkData, AudioSegment, I24,
};
use pmoflac::{decode_audio_stream, AudioFileMetadata, StreamInfo};
use pmometadata::{MemoryTrackMetadata, TrackMetadata};
use std::{path::PathBuf, sync::Arc, time::Duration};
use tokio::{fs::File, io::AsyncReadExt, sync::mpsc};
/// FileSource - Lit un fichier audio et publie des `AudioSegment`
///
/// Cette source utilise `pmoflac` pour décoder le fichier (FLAC/MP3/OGG/WAV/AIFF)
/// puis transforme les échantillons PCM en `AudioSegment` stéréo avec le type approprié
/// (I16, I24, ou I32) selon la profondeur de bit du fichier source.
///
/// Le node émet trois types de syncmarkers :
/// - `TopZeroSync` au début du flux
/// - `TrackBoundary` avec les métadonnées du fichier
/// - `EndOfStream` à la fin du flux
pub struct FileSource {
path: PathBuf,
chunk_frames: usize,
subscribers: MultiSubscriberNode,
}
impl FileSource {
/// Crée une nouvelle source de fichier avec calcul automatique de la taille des chunks.
///
/// La taille des chunks sera calculée automatiquement pour obtenir environ 50ms
/// de latence par chunk, en fonction du sample rate du fichier.
///
/// * `path` - chemin du fichier audio à lire
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
Self::with_chunk_size(path, 0) // 0 = auto-calculer
}
/// Crée une nouvelle source de fichier avec une taille de chunk spécifique.
///
/// * `path` - chemin du fichier audio à lire
/// * `chunk_frames` - nombre d'échantillons par canal par chunk (0 = auto)
pub fn with_chunk_size<P: Into<PathBuf>>(path: P, chunk_frames: usize) -> Self {
Self {
path: path.into(),
chunk_frames,
subscribers: MultiSubscriberNode::new(),
}
}
/// Ajoute un abonné qui recevra les segments audio.
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance la lecture du fichier et diffuse les segments audio.
pub async fn run(self) -> Result<(), AudioError> {
// Ouvrir le fichier
let file = File::open(&self.path).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to open {:?}: {}", self.path, e))
})?;
// Décoder le flux audio
let mut stream = decode_audio_stream(file)
.await
.map_err(|e| AudioError::ProcessingError(format!("Decode error: {}", e)))?;
let stream_info = stream.info().clone();
validate_stream(&stream_info)?;
// Calculer la taille des chunks si non spécifiée (0 = auto)
let chunk_frames = if self.chunk_frames == 0 {
// Calculer pour obtenir DEFAULT_CHUNK_DURATION_MS millisecondes
let frames =
(stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize;
// Arrondir à la puissance de 2 la plus proche pour optimiser les buffers
frames.next_power_of_two().max(256)
} else {
self.chunk_frames.max(1)
};
// Émettre TopZeroSync
let top_zero = AudioSegment::new_top_zero_sync();
self.subscribers.push(top_zero).await?;
// Extraire et émettre les métadonnées du fichier
match AudioFileMetadata::from_file(&self.path) {
Ok(file_metadata) => {
let mut metadata = MemoryTrackMetadata::new();
// Convertir AudioFileMetadata vers MemoryTrackMetadata
if let Some(title) = file_metadata.title {
let _ = metadata.set_title(Some(title)).await;
}
if let Some(artist) = file_metadata.artist {
let _ = metadata.set_artist(Some(artist)).await;
}
if let Some(album) = file_metadata.album {
let _ = metadata.set_album(Some(album)).await;
}
if let Some(year) = file_metadata.year {
let _ = metadata.set_year(Some(year)).await;
}
if let Some(duration_secs) = file_metadata.duration_secs {
let _ = metadata
.set_duration(Some(Duration::from_secs(duration_secs)))
.await;
}
// Émettre TrackBoundary
let track_boundary = AudioSegment::new_track_boundary(0, 0.0, Arc::new(metadata));
self.subscribers.push(track_boundary).await?;
}
Err(e) => {
eprintln!(
"Warning: Failed to extract metadata from {:?}: {}",
self.path, e
);
// Continuer sans métadonnées
}
}
// Préparer la lecture des chunks audio
let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize;
let chunk_byte_len = chunk_frames * frame_bytes;
let mut pending = Vec::new();
let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames)];
let mut chunk_index = 0u64;
let mut total_frames = 0u64;
// Lire et émettre les chunks audio
loop {
// Remplir le buffer
if pending.len() < chunk_byte_len {
let read = stream.read(&mut read_buf).await.map_err(|e| {
AudioError::ProcessingError(format!("I/O error while decoding: {}", e))
})?;
if read == 0 {
break;
}
pending.extend_from_slice(&read_buf[..read]);
}
if pending.is_empty() {
break;
}
// Extraire un chunk
let frames_in_pending = pending.len() / frame_bytes;
let frames_to_emit = frames_in_pending.min(chunk_frames);
let take_bytes = frames_to_emit * frame_bytes;
let chunk_bytes = pending.drain(..take_bytes).collect::<Vec<u8>>();
// Calculer le timestamp
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
// Créer le segment audio
let segment = bytes_to_segment(
&chunk_bytes,
&stream_info,
frames_to_emit,
chunk_index,
timestamp_sec,
)?;
self.subscribers.push(segment).await?;
chunk_index += 1;
total_frames += frames_to_emit as u64;
}
// Traiter le reste éventuel (moins qu'un chunk complet)
if !pending.is_empty() {
let frames = pending.len() / frame_bytes;
if frames > 0 {
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
let segment =
bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?;
self.subscribers.push(segment).await?;
total_frames += frames as u64;
chunk_index += 1;
}
}
// Émettre EndOfStream
let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64;
let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp);
self.subscribers.push(eos).await?;
// Attendre la fin du décodage
stream
.wait()
.await
.map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?;
Ok(())
}
}
fn validate_stream(info: &StreamInfo) -> Result<(), AudioError> {
if !(1..=2).contains(&info.channels) {
return Err(AudioError::ProcessingError(format!(
"Unsupported channel count: {}",
info.channels
)));
}
match info.bits_per_sample {
8 | 16 | 24 | 32 => Ok(()),
other => Err(AudioError::ProcessingError(format!(
"Unsupported bit depth: {}",
other
))),
}
}
/// Convertit des bytes PCM en AudioSegment avec le type approprié
fn bytes_to_segment(
chunk_bytes: &[u8],
info: &StreamInfo,
frames: usize,
order: u64,
timestamp_sec: f64,
) -> Result<Arc<AudioSegment>, AudioError> {
let bytes_per_sample = info.bytes_per_sample();
let channels = info.channels as usize;
let frame_bytes = bytes_per_sample * channels;
// Créer le chunk du bon type selon la profondeur de bit
let chunk = match info.bits_per_sample {
16 => {
// Type I16
let mut stereo = Vec::with_capacity(frames);
for frame_idx in 0..frames {
let base = frame_idx * frame_bytes;
let l = i16::from_le_bytes(
chunk_bytes[base..base + bytes_per_sample]
.try_into()
.unwrap(),
);
let r = if channels == 1 {
l
} else {
i16::from_le_bytes(
chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample]
.try_into()
.unwrap(),
)
};
stereo.push([l, r]);
}
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
AudioChunk::I16(chunk_data)
}
24 => {
// Type I24
let mut stereo = Vec::with_capacity(frames);
for frame_idx in 0..frames {
let base = frame_idx * frame_bytes;
let l_i32 = {
let mut buf = [0u8; 4];
buf[..3].copy_from_slice(&chunk_bytes[base..base + 3]);
// Sign extend
if chunk_bytes[base + 2] & 0x80 != 0 {
buf[3] = 0xFF;
}
i32::from_le_bytes(buf)
};
let l = I24::new(l_i32).ok_or_else(|| {
AudioError::ProcessingError(format!("Invalid I24 value: {}", l_i32))
})?;
let r = if channels == 1 {
l
} else {
let r_i32 = {
let mut buf = [0u8; 4];
buf[..3].copy_from_slice(
&chunk_bytes[base + bytes_per_sample..base + bytes_per_sample + 3],
);
// Sign extend
if chunk_bytes[base + bytes_per_sample + 2] & 0x80 != 0 {
buf[3] = 0xFF;
}
i32::from_le_bytes(buf)
};
I24::new(r_i32).ok_or_else(|| {
AudioError::ProcessingError(format!("Invalid I24 value: {}", r_i32))
})?
};
stereo.push([l, r]);
}
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
AudioChunk::I24(chunk_data)
}
32 => {
// Type I32
let mut stereo = Vec::with_capacity(frames);
for frame_idx in 0..frames {
let base = frame_idx * frame_bytes;
let l = i32::from_le_bytes(
chunk_bytes[base..base + bytes_per_sample]
.try_into()
.unwrap(),
);
let r = if channels == 1 {
l
} else {
i32::from_le_bytes(
chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample]
.try_into()
.unwrap(),
)
};
stereo.push([l, r]);
}
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
AudioChunk::I32(chunk_data)
}
other => {
return Err(AudioError::ProcessingError(format!(
"Unsupported bit depth: {}",
other
)))
}
};
// Créer le segment audio
Ok(Arc::new(AudioSegment {
order,
timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(chunk)),
}))
}
impl TypedAudioNode for FileSource {
fn input_type(&self) -> Option<TypeRequirement> {
// FileSource est une source, elle ne consomme pas d'audio
None
}
fn output_type(&self) -> Option<TypeRequirement> {
// FileSource peut produire n'importe quel type entier (I16, I24, I32)
// selon la profondeur de bit du fichier source
Some(TypeRequirement::any_integer())
}
}
#[cfg(test)]
mod tests {
use super::*;
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
use std::io::Cursor;
use tokio::io::AsyncWriteExt;
use tokio::sync::mpsc;
#[tokio::test]
async fn test_file_source_decodes_flac() {
let temp_dir = tempfile::tempdir().unwrap();
let flac_path = temp_dir.path().join("test.flac");
let sample_rate = 48_000;
let frames = 256;
let mut pcm = Vec::with_capacity(frames * 4);
for i in 0..frames {
let sample = ((i % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5; // simple ramp
let sample_i16 = (sample * 32767.0) as i16;
pcm.extend_from_slice(&sample_i16.to_le_bytes());
pcm.extend_from_slice(&sample_i16.to_le_bytes());
}
let format = PcmFormat {
sample_rate,
channels: 2,
bits_per_sample: 16,
};
let mut flac_stream =
encode_flac_stream(Cursor::new(pcm.clone()), format, EncoderOptions::default())
.await
.unwrap();
let mut file = File::create(&flac_path).await.expect("create flac file");
tokio::io::copy(&mut flac_stream, &mut file)
.await
.expect("write flac");
file.flush().await.expect("flush file");
flac_stream.wait().await.unwrap();
let mut source = FileSource::with_chunk_size(&flac_path, 64);
let (tx, mut rx) = mpsc::channel(16);
source.add_subscriber(tx);
tokio::spawn(async move {
source.run().await.unwrap();
});
let mut received_frames = 0usize;
let mut received_syncmarkers = 0usize;
let mut seen_top_zero = false;
let mut seen_eos = false;
while let Some(segment) = rx.recv().await {
if segment.is_audio_chunk() {
if let Some(chunk) = segment.as_chunk() {
received_frames += chunk.len();
assert_eq!(chunk.sample_rate(), sample_rate);
}
} else {
received_syncmarkers += 1;
if let Some(marker) = segment.as_sync_marker() {
match **marker {
crate::SyncMarker::TopZeroSync => seen_top_zero = true,
crate::SyncMarker::EndOfStream => seen_eos = true,
crate::SyncMarker::TrackBoundary { .. } => {}
_ => {}
}
}
}
}
// Vérifier que tous les frames ont été reçus
assert_eq!(received_frames, frames);
// Vérifier qu'on a bien reçu des syncmarkers
assert!(received_syncmarkers >= 2); // Au moins TopZeroSync et EndOfStream
assert!(seen_top_zero, "Should have received TopZeroSync");
assert!(seen_eos, "Should have received EndOfStream");
}
}

View File

@@ -0,0 +1,766 @@
use crate::{
nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE},
type_constraints::TypeRequirement,
AudioChunk, AudioSegment, SyncMarker,
};
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
use std::{
collections::VecDeque,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use tokio::{
fs::File,
io::{self, AsyncRead, AsyncWriteExt, ReadBuf},
sync::mpsc,
};
/// Sink qui encode les `AudioSegment` reçus au format FLAC.
///
/// Ce sink :
/// - Filtre les chunks audio et ignore les autres syncmarkers (sauf TrackBoundary et EndOfStream)
/// - Crée un nouveau fichier FLAC pour chaque TrackBoundary rencontré
/// - Adapte automatiquement l'encodage FLAC selon la profondeur de bit du chunk (8/16/24/32-bit)
/// - Termine l'encodage proprement quand il reçoit EndOfStream
pub struct FlacFileSink {
rx: mpsc::Receiver<Arc<AudioSegment>>,
base_path: PathBuf,
encoder_options: EncoderOptions,
pcm_buffer_capacity: usize,
}
impl FlacFileSink {
/// Crée un sink FLAC avec les options par défaut (compression 5, buffer de 16 segments).
///
/// # Arguments
///
/// * `base_path` - Chemin de base pour les fichiers FLAC. Si des TrackBoundary sont reçus,
/// des fichiers seront créés avec des suffixes (_01, _02, etc.)
pub fn new<P: Into<PathBuf>>(base_path: P) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(base_path, DEFAULT_CHANNEL_SIZE)
}
/// Crée un sink FLAC avec une taille de buffer MPSC personnalisée.
///
/// # Arguments
///
/// * `base_path` - Chemin de base pour les fichiers FLAC
/// * `channel_size` - Taille du buffer MPSC (nombre de segments en attente avant backpressure)
pub fn with_channel_size<P: Into<PathBuf>>(
base_path: P,
channel_size: usize,
) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_config(base_path, channel_size, EncoderOptions::default())
}
/// Crée un sink FLAC avec une configuration complète.
///
/// # Arguments
///
/// * `base_path` - Chemin de base pour les fichiers FLAC
/// * `channel_size` - Taille du buffer MPSC
/// * `encoder_options` - Options d'encodage FLAC (compression, etc.)
pub fn with_config<P: Into<PathBuf>>(
base_path: P,
channel_size: usize,
encoder_options: EncoderOptions,
) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let sink = Self {
rx,
base_path: base_path.into(),
encoder_options,
pcm_buffer_capacity: 8,
};
(sink, tx)
}
/// Lance l'encodage vers le(s) fichier(s) cible(s).
///
/// Cette méthode crée un nouveau fichier FLAC pour chaque TrackBoundary rencontré.
/// Les fichiers sont nommés selon la convention :
/// - Track 0 : base_path.flac
/// - Track 1 : base_path_01.flac
/// - Track 2 : base_path_02.flac, etc.
pub async fn run(self) -> Result<FlacFileSinkStats, AudioError> {
let FlacFileSink {
mut rx,
base_path,
encoder_options,
pcm_buffer_capacity,
} = self;
let mut all_tracks = Vec::new();
let mut track_number = 0;
loop {
// Attendre le premier chunk audio pour cette track, en capturant les métadonnées du TrackBoundary
let (first_segment, track_metadata) = match wait_for_first_audio_chunk_with_metadata(&mut rx).await {
Ok(result) => result,
Err(_) => {
// Plus d'audio disponible
if all_tracks.is_empty() {
return Err(AudioError::ProcessingError("No audio data received".into()));
}
break;
}
};
// Extraire les informations du premier chunk
let first_chunk = first_segment.as_chunk().unwrap();
let sample_rate = first_chunk.sample_rate();
let bits_per_sample = get_chunk_bit_depth(first_chunk);
let format = PcmFormat {
sample_rate,
channels: 2,
bits_per_sample,
};
if let Err(err) = format.validate() {
return Err(AudioError::ProcessingError(format!(
"Invalid PCM format: {}",
err
)));
}
// Générer le chemin du fichier pour cette track
let track_path = generate_track_path(&base_path, track_number);
// Créer le pipeline d'encodage pour cette track
let (pcm_tx, pcm_rx) = mpsc::channel::<Vec<u8>>(pcm_buffer_capacity);
// Préparer les options d'encodage avec les métadonnées du TrackBoundary
let mut options_with_metadata = encoder_options.clone();
options_with_metadata.metadata = track_metadata;
// Créer l'encoder et le fichier
let reader = ByteStreamReader::new(pcm_rx);
let mut flac_stream = encode_flac_stream(reader, format, options_with_metadata)
.await
.map_err(|e| {
AudioError::ProcessingError(format!("FLAC encode init failed: {}", e))
})?;
let mut output = File::create(&track_path).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to create {:?}: {}", track_path, e))
})?;
// Exécuter pump et copy en parallèle avec tokio::select! en boucle
let pump_future =
pump_track_segments(first_segment, &mut rx, pcm_tx, bits_per_sample, sample_rate);
let copy_future = async {
let copy_result = tokio::io::copy(&mut flac_stream, &mut output).await;
let flush_result = output.flush().await;
let wait_result = flac_stream.wait().await;
copy_result.map_err(|e| {
AudioError::ProcessingError(format!("FLAC write failed: {}", e))
})?;
flush_result
.map_err(|e| AudioError::ProcessingError(format!("Failed to flush: {}", e)))?;
wait_result
.map_err(|e| AudioError::ProcessingError(format!("Encoder failed: {}", e)))?;
Ok::<_, AudioError>(())
};
// Attendre les deux tâches en parallèle
let (copy_result, pump_result) = tokio::join!(copy_future, pump_future);
copy_result?;
let (chunks, samples, duration_sec, stop_reason) = pump_result?;
// Ajouter les stats de cette track
all_tracks.push(TrackStats {
path: track_path,
track_number,
chunks_received: chunks,
total_samples: samples,
total_duration_sec: duration_sec,
});
// Vérifier le stop_reason pour savoir si on continue
match stop_reason {
StopReason::TrackBoundary(_metadata) => {
// Continuer avec la prochaine track
track_number += 1;
continue;
}
StopReason::EndOfStream | StopReason::ChannelClosed => {
// Fin de l'encodage
break;
}
}
}
Ok(FlacFileSinkStats { tracks: all_tracks })
}
}
/// Génère le chemin de fichier pour une track donnée.
/// - track 0 → base_path.flac
/// - track 1 → base_path_01.flac
/// - track 2 → base_path_02.flac, etc.
fn generate_track_path(base_path: &Path, track_number: usize) -> PathBuf {
if track_number == 0 {
base_path.to_path_buf()
} else {
let stem = base_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
let extension = base_path
.extension()
.and_then(|s| s.to_str())
.unwrap_or("flac");
let parent = base_path.parent().unwrap_or(Path::new("."));
parent.join(format!("{}_{:02}.{}", stem, track_number, extension))
}
}
/// Signal retourné par pump_segments indiquant pourquoi l'encodage s'est arrêté.
enum StopReason {
TrackBoundary(Arc<dyn pmometadata::TrackMetadata + Send + Sync>),
EndOfStream,
ChannelClosed,
}
/// Attend et retourne le premier chunk audio avec les métadonnées du TrackBoundary si présent.
/// Retourne une erreur si EndOfStream est reçu avant tout audio.
async fn wait_for_first_audio_chunk_with_metadata(
rx: &mut mpsc::Receiver<Arc<AudioSegment>>,
) -> Result<(Arc<AudioSegment>, Option<Arc<dyn pmometadata::TrackMetadata + Send + Sync>>), AudioError> {
let mut track_metadata: Option<Arc<dyn pmometadata::TrackMetadata + Send + Sync>> = None;
loop {
let segment = rx
.recv()
.await
.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))?;
match &segment.segment {
crate::_AudioSegment::Chunk(chunk) => {
if chunk.len() == 0 {
return Err(AudioError::ProcessingError("Received empty chunk".into()));
}
return Ok((segment, track_metadata));
}
crate::_AudioSegment::Sync(marker) => {
match **marker {
SyncMarker::TrackBoundary { ref metadata, .. } => {
// Capturer les métadonnées du TrackBoundary
track_metadata = Some(metadata.clone());
continue;
}
SyncMarker::EndOfStream => {
return Err(AudioError::ProcessingError(
"EndOfStream received before any audio".into(),
));
}
_ => {
// Ignorer TopZeroSync, Heartbeat, etc.
continue;
}
}
}
}
}
}
/// Pompe les segments pour une seule track (s'arrête au TrackBoundary).
async fn pump_track_segments(
first_segment: Arc<AudioSegment>,
rx: &mut mpsc::Receiver<Arc<AudioSegment>>,
pcm_tx: mpsc::Sender<Vec<u8>>,
bits_per_sample: u8,
expected_rate: u32,
) -> Result<(u64, u64, f64, StopReason), AudioError> {
let mut chunks = 0u64;
let mut samples = 0u64;
let mut duration_sec = 0.0f64;
// Traiter le premier segment
if let Some(chunk) = first_segment.as_chunk() {
let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?;
if !pcm_bytes.is_empty() {
pcm_tx
.send(pcm_bytes)
.await
.map_err(|_| AudioError::SendError)?;
chunks += 1;
samples += chunk.len() as u64;
duration_sec += chunk.len() as f64 / expected_rate as f64;
}
}
// Boucle sur les segments suivants
loop {
let segment = match rx.recv().await {
Some(seg) => seg,
None => {
drop(pcm_tx); // Fermer le channel PCM
return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed));
}
};
match &segment.segment {
crate::_AudioSegment::Chunk(chunk) => {
// Vérifier la cohérence du sample rate
if chunk.sample_rate() != expected_rate {
return Err(AudioError::ProcessingError(format!(
"FlacFileSink: inconsistent sample rate ({} vs {})",
chunk.sample_rate(),
expected_rate
)));
}
let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?;
if pcm_bytes.is_empty() {
continue;
}
pcm_tx
.send(pcm_bytes)
.await
.map_err(|_| AudioError::SendError)?;
chunks += 1;
samples += chunk.len() as u64;
duration_sec += chunk.len() as f64 / expected_rate as f64;
}
crate::_AudioSegment::Sync(marker) => {
match &**marker {
SyncMarker::TrackBoundary { metadata, .. } => {
drop(pcm_tx); // Fermer le channel PCM
return Ok((
chunks,
samples,
duration_sec,
StopReason::TrackBoundary(metadata.clone()),
));
}
SyncMarker::EndOfStream => {
drop(pcm_tx); // Fermer le channel PCM
return Ok((chunks, samples, duration_sec, StopReason::EndOfStream));
}
_ => {} // Ignorer les autres syncmarkers
}
}
}
}
}
/// Détermine la profondeur de bit d'un chunk audio
fn get_chunk_bit_depth(chunk: &AudioChunk) -> u8 {
match chunk {
AudioChunk::I16(_) => 16,
AudioChunk::I24(_) => 24,
AudioChunk::I32(_) => 32,
AudioChunk::F32(_) => 32, // Les flottants seront convertis en 32-bit
AudioChunk::F64(_) => 32, // Les flottants seront convertis en 32-bit
}
}
/// Convertit un chunk audio en bytes PCM avec la profondeur de bit spécifiée
fn chunk_to_pcm_bytes(chunk: &AudioChunk, bits_per_sample: u8) -> Result<Vec<u8>, AudioError> {
// Vérifier que le chunk est de type entier
match chunk {
AudioChunk::F32(_) | AudioChunk::F64(_) => {
return Err(AudioError::ProcessingError(
"FlacFileSink only supports integer audio chunks (I16, I24, I32)".into(),
));
}
_ => {}
}
let len = chunk.len();
let bytes_per_frame = (bits_per_sample / 8) as usize * 2; // 2 channels
let mut bytes = Vec::with_capacity(len * bytes_per_frame);
// Convertir selon le type du chunk
match (chunk, bits_per_sample) {
// I16 source
(AudioChunk::I16(data), 16) => {
for frame in data.frames() {
bytes.extend_from_slice(&frame[0].to_le_bytes());
bytes.extend_from_slice(&frame[1].to_le_bytes());
}
}
(AudioChunk::I16(data), 24) => {
for frame in data.frames() {
let left = (frame[0] as i32) << 8;
let right = (frame[1] as i32) << 8;
bytes.extend_from_slice(&left.to_le_bytes()[..3]);
bytes.extend_from_slice(&right.to_le_bytes()[..3]);
}
}
(AudioChunk::I16(data), 32) => {
for frame in data.frames() {
let left = (frame[0] as i32) << 16;
let right = (frame[1] as i32) << 16;
bytes.extend_from_slice(&left.to_le_bytes());
bytes.extend_from_slice(&right.to_le_bytes());
}
}
// I24 source
(AudioChunk::I24(data), 16) => {
for frame in data.frames() {
let left = (frame[0].as_i32() >> 8) as i16;
let right = (frame[1].as_i32() >> 8) as i16;
bytes.extend_from_slice(&left.to_le_bytes());
bytes.extend_from_slice(&right.to_le_bytes());
}
}
(AudioChunk::I24(data), 24) => {
for frame in data.frames() {
bytes.extend_from_slice(&frame[0].as_i32().to_le_bytes()[..3]);
bytes.extend_from_slice(&frame[1].as_i32().to_le_bytes()[..3]);
}
}
(AudioChunk::I24(data), 32) => {
for frame in data.frames() {
let left = frame[0].as_i32() << 8;
let right = frame[1].as_i32() << 8;
bytes.extend_from_slice(&left.to_le_bytes());
bytes.extend_from_slice(&right.to_le_bytes());
}
}
// I32 source
(AudioChunk::I32(data), 16) => {
for frame in data.frames() {
let left = (frame[0] >> 16) as i16;
let right = (frame[1] >> 16) as i16;
bytes.extend_from_slice(&left.to_le_bytes());
bytes.extend_from_slice(&right.to_le_bytes());
}
}
(AudioChunk::I32(data), 24) => {
for frame in data.frames() {
let left = frame[0] >> 8;
let right = frame[1] >> 8;
bytes.extend_from_slice(&left.to_le_bytes()[..3]);
bytes.extend_from_slice(&right.to_le_bytes()[..3]);
}
}
(AudioChunk::I32(data), 32) => {
for frame in data.frames() {
bytes.extend_from_slice(&frame[0].to_le_bytes());
bytes.extend_from_slice(&frame[1].to_le_bytes());
}
}
_ => {
return Err(AudioError::ProcessingError(format!(
"Unsupported bits_per_sample: {}",
bits_per_sample
)));
}
}
Ok(bytes)
}
struct ByteStreamReader {
rx: mpsc::Receiver<Vec<u8>>,
buffer: VecDeque<u8>,
finished: bool,
}
impl ByteStreamReader {
fn new(rx: mpsc::Receiver<Vec<u8>>) -> Self {
Self {
rx,
buffer: VecDeque::new(),
finished: false,
}
}
}
impl AsyncRead for ByteStreamReader {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
loop {
if !self.buffer.is_empty() {
let to_copy = self.buffer.len().min(buf.remaining());
if to_copy == 0 {
return Poll::Ready(Ok(()));
}
// VecDeque::make_contiguous pour copier efficacement
let slice = self.buffer.make_contiguous();
buf.put_slice(&slice[..to_copy]);
self.buffer.drain(..to_copy);
return Poll::Ready(Ok(()));
}
if self.finished {
return Poll::Ready(Ok(()));
}
match Pin::new(&mut self.rx).poll_recv(cx) {
Poll::Ready(Some(bytes)) => {
if bytes.is_empty() {
continue;
}
self.buffer.extend(bytes);
}
Poll::Ready(None) => {
self.finished = true;
return Poll::Ready(Ok(()));
}
Poll::Pending => return Poll::Pending,
}
}
}
}
/// Statistiques pour une track individuelle.
#[derive(Debug, Clone)]
pub struct TrackStats {
pub path: PathBuf,
pub track_number: usize,
pub chunks_received: u64,
pub total_samples: u64,
pub total_duration_sec: f64,
}
/// Statistiques produites par le `FlacFileSink`.
#[derive(Debug, Clone)]
pub struct FlacFileSinkStats {
pub tracks: Vec<TrackStats>,
}
impl TypedAudioNode for FlacFileSink {
fn input_type(&self) -> Option<TypeRequirement> {
// FlacFileSink accepte n'importe quel type entier (I16, I24, I32)
// mais rejette les chunks flottants
Some(TypeRequirement::any_integer())
}
fn output_type(&self) -> Option<TypeRequirement> {
// FlacFileSink est un sink, il ne produit pas d'audio
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use pmoflac::{decode_flac_stream, AudioFileMetadata};
use pmometadata::{MemoryTrackMetadata, TrackMetadata};
use tokio::io::AsyncReadExt;
#[tokio::test]
async fn test_flac_file_sink_writes_metadata() {
let temp_dir = tempfile::tempdir().unwrap();
let output_path = temp_dir.path().join("output_with_metadata.flac");
let sample_rate = 44_100;
let frames = 256;
// Créer le sink
let (sink, tx) = FlacFileSink::with_channel_size(&output_path, 16);
let sink_handle = tokio::spawn(async move { sink.run().await.unwrap() });
// Envoyer des segments avec métadonnées
tokio::spawn(async move {
// TopZeroSync
tx.send(crate::AudioSegment::new_top_zero_sync())
.await
.unwrap();
// TrackBoundary avec métadonnées
let mut metadata = MemoryTrackMetadata::new();
metadata.set_title(Some("Test Track Title".to_string())).await.unwrap();
metadata.set_artist(Some("Test Artist".to_string())).await.unwrap();
metadata.set_album(Some("Test Album".to_string())).await.unwrap();
metadata.set_year(Some(2024)).await.unwrap();
let track_boundary =
crate::AudioSegment::new_track_boundary(0, 0.0, std::sync::Arc::new(metadata));
tx.send(track_boundary).await.unwrap();
// Générer et envoyer des chunks audio
let chunk_frames = 64;
let mut order = 0u64;
let mut total_frames = 0u64;
for chunk_start in (0..frames).step_by(chunk_frames) {
let chunk_len = (frames - chunk_start).min(chunk_frames);
let mut stereo = Vec::with_capacity(chunk_len);
for i in 0..chunk_len {
let frame_idx = chunk_start + i;
let sample = ((frame_idx % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5;
let sample_i16 = (sample * 32767.0) as i16;
stereo.push([sample_i16, sample_i16]);
}
let timestamp = total_frames as f64 / sample_rate as f64;
let chunk_data = crate::AudioChunkData::new(stereo, sample_rate, 0.0);
let chunk = crate::AudioChunk::I16(chunk_data);
let segment = crate::AudioSegment {
order,
timestamp_sec: timestamp,
segment: crate::_AudioSegment::Chunk(std::sync::Arc::new(chunk)),
};
tx.send(std::sync::Arc::new(segment)).await.unwrap();
total_frames += chunk_len as u64;
order += 1;
}
// EndOfStream
let final_timestamp = total_frames as f64 / sample_rate as f64;
tx.send(crate::AudioSegment::new_end_of_stream(
order,
final_timestamp,
))
.await
.unwrap();
drop(tx);
});
sink_handle.await.unwrap();
// Vérifier que le fichier a été créé et contient les métadonnées
assert!(output_path.exists(), "Output file should exist");
// Lire les métadonnées du fichier FLAC généré
let file_metadata = AudioFileMetadata::from_file(&output_path).unwrap();
// Vérifier que les métadonnées ont été correctement écrites
assert_eq!(file_metadata.title, Some("Test Track Title".to_string()));
assert_eq!(file_metadata.artist, Some("Test Artist".to_string()));
assert_eq!(file_metadata.album, Some("Test Album".to_string()));
assert_eq!(file_metadata.year, Some(2024));
}
#[tokio::test]
async fn test_flac_file_sink_writes_audio() {
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
use std::io::Cursor;
let temp_dir = tempfile::tempdir().unwrap();
let input_path = temp_dir.path().join("input.flac");
let output_path = temp_dir.path().join("output.flac");
// Créer un petit fichier FLAC de test (comme dans file_source test)
let sample_rate = 44_100;
let frames = 512;
let mut pcm = Vec::with_capacity(frames * 4);
for i in 0..frames {
let sample = ((i % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5;
let sample_i16 = (sample * 32767.0) as i16;
pcm.extend_from_slice(&sample_i16.to_le_bytes());
pcm.extend_from_slice(&sample_i16.to_le_bytes());
}
let format = PcmFormat {
sample_rate,
channels: 2,
bits_per_sample: 16,
};
let mut flac_stream =
encode_flac_stream(Cursor::new(pcm.clone()), format, EncoderOptions::default())
.await
.unwrap();
let mut input_file = File::create(&input_path).await.unwrap();
tokio::io::copy(&mut flac_stream, &mut input_file)
.await
.unwrap();
input_file.flush().await.unwrap();
flac_stream.wait().await.unwrap();
// Maintenant utiliser FlacFileSink pour réécrire le fichier
let (sink, tx) = FlacFileSink::with_channel_size(&output_path, 16);
let sink_handle = tokio::spawn(async move { sink.run().await.unwrap() });
// Lire le fichier input et envoyer les segments au sink
tokio::spawn(async move {
let source_file = File::open(&input_path).await.unwrap();
let mut decode_stream = pmoflac::decode_audio_stream(source_file).await.unwrap();
let info = decode_stream.info().clone();
// TopZeroSync
tx.send(crate::AudioSegment::new_top_zero_sync())
.await
.unwrap();
// Lire et envoyer les chunks
let mut buffer = vec![0u8; info.bytes_per_sample() * info.channels as usize * 256];
let mut total_frames = 0u64;
let mut order = 0u64;
loop {
let read = decode_stream.read(&mut buffer).await.unwrap();
if read == 0 {
break;
}
let chunk_frames = read / (info.bytes_per_sample() * info.channels as usize);
let timestamp = total_frames as f64 / info.sample_rate as f64;
// Créer un segment I16
let mut stereo = Vec::with_capacity(chunk_frames);
for i in 0..chunk_frames {
let offset = i * info.bytes_per_sample() * info.channels as usize;
let l = i16::from_le_bytes([buffer[offset], buffer[offset + 1]]);
let r = i16::from_le_bytes([buffer[offset + 2], buffer[offset + 3]]);
stereo.push([l, r]);
}
let chunk_data = crate::AudioChunkData::new(stereo, info.sample_rate, 0.0);
let chunk = crate::AudioChunk::I16(chunk_data);
let segment = crate::AudioSegment {
order,
timestamp_sec: timestamp,
segment: crate::_AudioSegment::Chunk(std::sync::Arc::new(chunk)),
};
tx.send(std::sync::Arc::new(segment)).await.unwrap();
total_frames += chunk_frames as u64;
order += 1;
}
// EndOfStream
let final_timestamp = total_frames as f64 / info.sample_rate as f64;
tx.send(crate::AudioSegment::new_end_of_stream(
order,
final_timestamp,
))
.await
.unwrap();
drop(tx);
decode_stream.wait().await.unwrap();
});
let stats = sink_handle.await.unwrap();
assert_eq!(stats.tracks.len(), 1);
assert!(stats.tracks[0].chunks_received > 0);
assert_eq!(stats.tracks[0].total_samples, frames as u64);
// Vérifier que le fichier de sortie est valide
let file = File::open(&output_path).await.unwrap();
let mut stream = decode_flac_stream(file).await.unwrap();
let info = stream.info().clone();
assert_eq!(info.channels, 2);
assert_eq!(info.sample_rate, sample_rate);
assert_eq!(info.bits_per_sample, 16);
let mut decoded = Vec::new();
stream.read_to_end(&mut decoded).await.unwrap();
stream.wait().await.unwrap();
assert!(decoded.len() > 0);
}
}

View File

@@ -0,0 +1,827 @@
use crate::{
nodes::{AudioError, MultiSubscriberNode, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
type_constraints::TypeRequirement,
AudioChunk, AudioChunkData, AudioSegment, I24,
};
use futures_util::StreamExt;
use pmoflac::{decode_audio_stream, StreamInfo};
use pmometadata::{MemoryTrackMetadata, TrackMetadata};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_util::io::StreamReader;
/// HttpSource - Récupère un fichier audio via HTTP et publie des `AudioSegment`
///
/// Cette source télécharge un fichier audio depuis une URL HTTP/HTTPS,
/// utilise `pmoflac` pour le décoder (FLAC/MP3/OGG/WAV/AIFF) puis transforme
/// les échantillons PCM en `AudioSegment` stéréo avec le type approprié.
///
/// Le node émet trois types de syncmarkers :
/// - `TopZeroSync` au début du flux
/// - `TrackBoundary` avec les métadonnées extraites des headers HTTP
/// - `EndOfStream` à la fin du flux
///
/// # Métadonnées HTTP
///
/// Les métadonnées suivantes sont extraites des headers HTTP lorsqu'elles sont disponibles:
/// - `icy-name`: nom du stream (Icecast/Shoutcast) → utilisé comme titre
/// - `icy-url`: URL du stream source
/// - `content-type`: type MIME du contenu (ex: audio/flac, audio/mpeg)
///
/// Si aucun header `icy-name` n'est présent, le nom du fichier est extrait de l'URL
/// et utilisé comme titre.
///
/// # Exemples
///
/// ## Lecture d'un fichier FLAC distant
///
/// ```no_run
/// use pmoaudio::HttpSource;
/// use tokio::sync::mpsc;
///
/// #[tokio::main]
/// async fn main() {
/// let mut source = HttpSource::new("http://example.com/audio.flac");
/// let (tx, mut rx) = mpsc::channel(16);
/// source.add_subscriber(tx);
///
/// // Lancer la lecture dans une tâche séparée
/// tokio::spawn(async move {
/// source.run().await.unwrap();
/// });
///
/// // Recevoir et traiter les segments audio
/// while let Some(segment) = rx.recv().await {
/// if segment.is_audio_chunk() {
/// println!("Chunk reçu à {}s", segment.timestamp_sec);
/// }
/// }
/// }
/// ```
///
/// ## Stream Icecast/Shoutcast
///
/// ```no_run
/// use pmoaudio::HttpSource;
/// use tokio::sync::mpsc;
///
/// #[tokio::main]
/// async fn main() {
/// // Les métadonnées icy-name seront extraites automatiquement
/// let mut source = HttpSource::new("http://stream.example.com:8000/stream");
/// let (tx, rx) = mpsc::channel(32);
/// source.add_subscriber(tx);
///
/// tokio::spawn(async move {
/// source.run().await.unwrap();
/// });
/// }
/// ```
///
/// # Gestion des erreurs
///
/// La méthode `run()` peut retourner les erreurs suivantes:
/// - `AudioError::ProcessingError`: échec de connexion HTTP, status code non-200,
/// erreur de décodage audio, ou format non supporté
///
/// # Performance
///
/// - Le téléchargement et le décodage sont effectués en streaming
/// - Pas de buffering complet du fichier en mémoire
/// - La taille des chunks audio est calculée automatiquement pour ~50ms de latence
/// - Compatible avec les streams infinis (radios web, etc.)
pub struct HttpSource {
url: String,
chunk_frames: usize,
subscribers: MultiSubscriberNode,
}
impl HttpSource {
/// Crée une nouvelle source HTTP avec calcul automatique de la taille des chunks.
///
/// La taille des chunks sera calculée automatiquement pour obtenir environ 50ms
/// de latence par chunk, en fonction du sample rate du fichier distant.
///
/// # Arguments
///
/// * `url` - URL HTTP ou HTTPS du fichier audio à télécharger
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::HttpSource;
///
/// let source = HttpSource::new("http://example.com/music.flac");
/// ```
pub fn new<S: Into<String>>(url: S) -> Self {
Self::with_chunk_size(url, 0)
}
/// Crée une nouvelle source HTTP avec une taille de chunk spécifique.
///
/// # Arguments
///
/// * `url` - URL HTTP ou HTTPS du fichier audio à télécharger
/// * `chunk_frames` - nombre d'échantillons par canal par chunk (0 = auto-calcul)
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::HttpSource;
///
/// // Utiliser des chunks de 2048 frames
/// let source = HttpSource::with_chunk_size("http://example.com/music.mp3", 2048);
/// ```
pub fn with_chunk_size<S: Into<String>>(url: S, chunk_frames: usize) -> Self {
Self {
url: url.into(),
chunk_frames,
subscribers: MultiSubscriberNode::new(),
}
}
/// Ajoute un abonné qui recevra les segments audio.
///
/// Chaque abonné recevra une copie (via `Arc`) de tous les segments audio
/// produits par cette source, y compris les syncmarkers.
///
/// # Arguments
///
/// * `tx` - Channel sender pour recevoir les `AudioSegment`
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::HttpSource;
/// use tokio::sync::mpsc;
///
/// let mut source = HttpSource::new("http://example.com/audio.flac");
/// let (tx, rx) = mpsc::channel(16);
/// source.add_subscriber(tx);
/// ```
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le téléchargement et la lecture du flux audio.
///
/// Cette méthode consomme `self` et exécute le pipeline complet:
/// 1. Effectue la requête HTTP GET vers l'URL spécifiée
/// 2. Vérifie le status HTTP (doit être 2xx)
/// 3. Extrait les métadonnées des headers HTTP
/// 4. Décode le flux audio en streaming
/// 5. Émet les syncmarkers et chunks audio vers les abonnés
///
/// La méthode se termine quand le flux est complètement lu ou en cas d'erreur.
///
/// # Erreurs
///
/// Retourne `AudioError::ProcessingError` si:
/// - La requête HTTP échoue (réseau, DNS, etc.)
/// - Le serveur retourne un status code non-2xx
/// - Le format audio n'est pas supporté
/// - Le décodage échoue
/// - Le fichier a un nombre de canaux non supporté (doit être 1 ou 2)
/// - La profondeur de bit n'est pas supportée (doit être 8, 16, 24 ou 32 bits)
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::HttpSource;
/// use tokio::sync::mpsc;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut source = HttpSource::new("http://example.com/audio.flac");
/// let (tx, mut rx) = mpsc::channel(16);
/// source.add_subscriber(tx);
///
/// let handle = tokio::spawn(async move {
/// source.run().await
/// });
///
/// // Traiter les segments
/// while let Some(segment) = rx.recv().await {
/// println!("Segment reçu: order={}", segment.order);
/// }
///
/// handle.await??;
/// Ok(())
/// }
/// ```
pub async fn run(self) -> Result<(), AudioError> {
// Effectuer la requête HTTP
let response = reqwest::get(&self.url)
.await
.map_err(|e| {
AudioError::ProcessingError(format!("HTTP request failed for {}: {}", self.url, e))
})?;
// Vérifier le status
if !response.status().is_success() {
return Err(AudioError::ProcessingError(format!(
"HTTP request returned status {}: {}",
response.status(),
self.url
)));
}
// Extraire les métadonnées depuis les headers HTTP
let metadata = extract_metadata_from_headers(&response, &self.url).await;
// Convertir le stream de bytes en AsyncRead
let bytes_stream = response.bytes_stream();
let stream_reader = StreamReader::new(bytes_stream.map(|result| {
result.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
}));
// Décoder le flux audio
let mut stream = decode_audio_stream(stream_reader)
.await
.map_err(|e| AudioError::ProcessingError(format!("Decode error: {}", e)))?;
let stream_info = stream.info().clone();
validate_stream(&stream_info)?;
// Calculer la taille des chunks si non spécifiée (0 = auto)
let chunk_frames = if self.chunk_frames == 0 {
let frames =
(stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize;
frames.next_power_of_two().max(256)
} else {
self.chunk_frames.max(1)
};
// Émettre TopZeroSync
let top_zero = AudioSegment::new_top_zero_sync();
self.subscribers.push(top_zero).await?;
// Émettre TrackBoundary avec les métadonnées HTTP
let track_boundary = AudioSegment::new_track_boundary(0, 0.0, Arc::new(metadata));
self.subscribers.push(track_boundary).await?;
// Préparer la lecture des chunks audio
let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize;
let chunk_byte_len = chunk_frames * frame_bytes;
let mut pending = Vec::new();
let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames)];
let mut chunk_index = 0u64;
let mut total_frames = 0u64;
// Lire et émettre les chunks audio
loop {
// Remplir le buffer
if pending.len() < chunk_byte_len {
use tokio::io::AsyncReadExt;
let read = stream.read(&mut read_buf).await.map_err(|e| {
AudioError::ProcessingError(format!("I/O error while decoding: {}", e))
})?;
if read == 0 {
break;
}
pending.extend_from_slice(&read_buf[..read]);
}
if pending.is_empty() {
break;
}
// Extraire un chunk
let frames_in_pending = pending.len() / frame_bytes;
let frames_to_emit = frames_in_pending.min(chunk_frames);
let take_bytes = frames_to_emit * frame_bytes;
let chunk_bytes = pending.drain(..take_bytes).collect::<Vec<u8>>();
// Calculer le timestamp
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
// Créer le segment audio
let segment = bytes_to_segment(
&chunk_bytes,
&stream_info,
frames_to_emit,
chunk_index,
timestamp_sec,
)?;
self.subscribers.push(segment).await?;
chunk_index += 1;
total_frames += frames_to_emit as u64;
}
// Traiter le reste éventuel
if !pending.is_empty() {
let frames = pending.len() / frame_bytes;
if frames > 0 {
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
let segment =
bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?;
self.subscribers.push(segment).await?;
total_frames += frames as u64;
chunk_index += 1;
}
}
// Émettre EndOfStream
let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64;
let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp);
self.subscribers.push(eos).await?;
// Attendre la fin du décodage
stream
.wait()
.await
.map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?;
Ok(())
}
}
/// Extrait les métadonnées disponibles depuis les headers HTTP
async fn extract_metadata_from_headers(
response: &reqwest::Response,
url: &str,
) -> MemoryTrackMetadata {
let mut metadata = MemoryTrackMetadata::new();
let headers = response.headers();
// Icecast/Shoutcast stream name
if let Some(name) = headers.get("icy-name").and_then(|v| v.to_str().ok()) {
let _ = metadata.set_title(Some(name.to_string())).await;
}
// Icecast/Shoutcast stream URL (peut être utilisé comme source)
if let Some(stream_url) = headers.get("icy-url").and_then(|v| v.to_str().ok()) {
// On pourrait stocker ça dans un champ custom si nécessaire
eprintln!("Stream URL: {}", stream_url);
}
// Content-Type pour déterminer le format
if let Some(content_type) = headers.get("content-type").and_then(|v| v.to_str().ok()) {
eprintln!("Content-Type: {}", content_type);
// On pourrait utiliser ça pour valider le format attendu
}
// Si aucune métadonnée spécifique n'est trouvée, utiliser l'URL comme titre
if metadata.get_title().await.ok().flatten().is_none() {
// Extraire le nom du fichier depuis l'URL
if let Some(filename) = url.rsplit('/').next() {
if !filename.is_empty() {
let _ = metadata.set_title(Some(filename.to_string())).await;
}
}
}
metadata
}
fn validate_stream(info: &StreamInfo) -> Result<(), AudioError> {
if !(1..=2).contains(&info.channels) {
return Err(AudioError::ProcessingError(format!(
"Unsupported channel count: {}",
info.channels
)));
}
match info.bits_per_sample {
8 | 16 | 24 | 32 => Ok(()),
other => Err(AudioError::ProcessingError(format!(
"Unsupported bit depth: {}",
other
))),
}
}
/// Convertit des bytes PCM en AudioSegment avec le type approprié
fn bytes_to_segment(
chunk_bytes: &[u8],
info: &StreamInfo,
frames: usize,
order: u64,
timestamp_sec: f64,
) -> Result<Arc<AudioSegment>, AudioError> {
let bytes_per_sample = info.bytes_per_sample();
let channels = info.channels as usize;
let frame_bytes = bytes_per_sample * channels;
// Créer le chunk du bon type selon la profondeur de bit
let chunk = match info.bits_per_sample {
16 => {
// Type I16
let mut stereo = Vec::with_capacity(frames);
for frame_idx in 0..frames {
let base = frame_idx * frame_bytes;
let l = i16::from_le_bytes(
chunk_bytes[base..base + bytes_per_sample]
.try_into()
.unwrap(),
);
let r = if channels == 1 {
l
} else {
i16::from_le_bytes(
chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample]
.try_into()
.unwrap(),
)
};
stereo.push([l, r]);
}
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
AudioChunk::I16(chunk_data)
}
24 => {
// Type I24
let mut stereo = Vec::with_capacity(frames);
for frame_idx in 0..frames {
let base = frame_idx * frame_bytes;
let l_i32 = {
let mut buf = [0u8; 4];
buf[..3].copy_from_slice(&chunk_bytes[base..base + 3]);
// Sign extend
if chunk_bytes[base + 2] & 0x80 != 0 {
buf[3] = 0xFF;
}
i32::from_le_bytes(buf)
};
let l = I24::new(l_i32).ok_or_else(|| {
AudioError::ProcessingError(format!("Invalid I24 value: {}", l_i32))
})?;
let r = if channels == 1 {
l
} else {
let r_i32 = {
let mut buf = [0u8; 4];
buf[..3].copy_from_slice(
&chunk_bytes[base + bytes_per_sample..base + bytes_per_sample + 3],
);
// Sign extend
if chunk_bytes[base + bytes_per_sample + 2] & 0x80 != 0 {
buf[3] = 0xFF;
}
i32::from_le_bytes(buf)
};
I24::new(r_i32).ok_or_else(|| {
AudioError::ProcessingError(format!("Invalid I24 value: {}", r_i32))
})?
};
stereo.push([l, r]);
}
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
AudioChunk::I24(chunk_data)
}
32 => {
// Type I32
let mut stereo = Vec::with_capacity(frames);
for frame_idx in 0..frames {
let base = frame_idx * frame_bytes;
let l = i32::from_le_bytes(
chunk_bytes[base..base + bytes_per_sample]
.try_into()
.unwrap(),
);
let r = if channels == 1 {
l
} else {
i32::from_le_bytes(
chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample]
.try_into()
.unwrap(),
)
};
stereo.push([l, r]);
}
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
AudioChunk::I32(chunk_data)
}
other => {
return Err(AudioError::ProcessingError(format!(
"Unsupported bit depth: {}",
other
)))
}
};
// Créer le segment audio
Ok(Arc::new(AudioSegment {
order,
timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(chunk)),
}))
}
impl TypedAudioNode for HttpSource {
fn input_type(&self) -> Option<TypeRequirement> {
// HttpSource est une source, elle ne consomme pas d'audio
None
}
fn output_type(&self) -> Option<TypeRequirement> {
// HttpSource peut produire n'importe quel type entier (I16, I24, I32)
// selon la profondeur de bit du fichier source
Some(TypeRequirement::any_integer())
}
}
#[cfg(test)]
mod tests {
use super::*;
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
use std::io::Cursor;
use tokio::sync::mpsc;
use wiremock::{
matchers::{method, path},
Mock, MockServer, ResponseTemplate,
};
/// Test de création basique de HttpSource
#[test]
fn test_http_source_creation() {
let source = HttpSource::new("http://example.com/audio.flac");
assert_eq!(source.url, "http://example.com/audio.flac");
assert_eq!(source.chunk_frames, 0);
}
/// Test de création avec taille de chunk personnalisée
#[test]
fn test_http_source_with_chunk_size() {
let source = HttpSource::with_chunk_size("http://example.com/audio.mp3", 1024);
assert_eq!(source.url, "http://example.com/audio.mp3");
assert_eq!(source.chunk_frames, 1024);
}
/// Test de téléchargement et décodage d'un fichier FLAC via HTTP
#[tokio::test]
async fn test_http_source_downloads_and_decodes_flac() {
// Créer un serveur HTTP mock
let mock_server = MockServer::start().await;
// Générer un petit fichier FLAC de test
let sample_rate = 48_000;
let frames = 256;
let mut pcm = Vec::with_capacity(frames * 4);
for i in 0..frames {
let sample = ((i % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5;
let sample_i16 = (sample * 32767.0) as i16;
pcm.extend_from_slice(&sample_i16.to_le_bytes());
pcm.extend_from_slice(&sample_i16.to_le_bytes());
}
let format = PcmFormat {
sample_rate,
channels: 2,
bits_per_sample: 16,
};
let mut flac_stream =
encode_flac_stream(Cursor::new(pcm.clone()), format, EncoderOptions::default())
.await
.unwrap();
// Lire le FLAC encodé dans un buffer
let mut flac_data = Vec::new();
tokio::io::copy(&mut flac_stream, &mut flac_data)
.await
.unwrap();
flac_stream.wait().await.unwrap();
// Configurer le mock pour servir le fichier FLAC
Mock::given(method("GET"))
.and(path("/test.flac"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(flac_data)
.insert_header("content-type", "audio/flac"),
)
.mount(&mock_server)
.await;
// Créer la source HTTP pointant vers le mock
let url = format!("{}/test.flac", mock_server.uri());
let mut source = HttpSource::with_chunk_size(&url, 64);
let (tx, mut rx) = mpsc::channel(16);
source.add_subscriber(tx);
// Lancer le téléchargement et le décodage
tokio::spawn(async move {
source.run().await.unwrap();
});
// Vérifier les segments reçus
let mut received_frames = 0usize;
let mut seen_top_zero = false;
let mut seen_track_boundary = false;
let mut seen_eos = false;
while let Some(segment) = rx.recv().await {
if segment.is_audio_chunk() {
if let Some(chunk) = segment.as_chunk() {
received_frames += chunk.len();
assert_eq!(chunk.sample_rate(), sample_rate);
}
} else if let Some(marker) = segment.as_sync_marker() {
match **marker {
crate::SyncMarker::TopZeroSync => seen_top_zero = true,
crate::SyncMarker::TrackBoundary { .. } => seen_track_boundary = true,
crate::SyncMarker::EndOfStream => seen_eos = true,
_ => {}
}
}
}
// Vérifications
assert_eq!(received_frames, frames, "Tous les frames doivent être reçus");
assert!(seen_top_zero, "TopZeroSync doit être émis");
assert!(seen_track_boundary, "TrackBoundary doit être émis");
assert!(seen_eos, "EndOfStream doit être émis");
}
/// Test de l'extraction des métadonnées depuis les headers HTTP
#[tokio::test]
async fn test_http_source_extracts_icy_metadata() {
let mock_server = MockServer::start().await;
// Créer un fichier FLAC minimal
let sample_rate = 48_000;
let frames = 128;
let mut pcm = Vec::with_capacity(frames * 4);
for i in 0..frames {
let sample_i16 = ((i % 100) as i16) * 100;
pcm.extend_from_slice(&sample_i16.to_le_bytes());
pcm.extend_from_slice(&sample_i16.to_le_bytes());
}
let format = PcmFormat {
sample_rate,
channels: 2,
bits_per_sample: 16,
};
let mut flac_stream = encode_flac_stream(Cursor::new(pcm), format, EncoderOptions::default())
.await
.unwrap();
let mut flac_data = Vec::new();
tokio::io::copy(&mut flac_stream, &mut flac_data)
.await
.unwrap();
flac_stream.wait().await.unwrap();
// Configurer le mock avec headers Icecast
Mock::given(method("GET"))
.and(path("/stream"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(flac_data)
.insert_header("content-type", "audio/flac")
.insert_header("icy-name", "Test Radio Stream")
.insert_header("icy-url", "http://example.com/radio"),
)
.mount(&mock_server)
.await;
let url = format!("{}/stream", mock_server.uri());
let mut source = HttpSource::new(&url);
let (tx, mut rx) = mpsc::channel(16);
source.add_subscriber(tx);
tokio::spawn(async move {
source.run().await.unwrap();
});
// Chercher le TrackBoundary pour vérifier les métadonnées
let mut found_metadata = false;
while let Some(segment) = rx.recv().await {
if let Some(marker) = segment.as_sync_marker() {
if let crate::SyncMarker::TrackBoundary { metadata, .. } = &**marker {
// Vérifier que le titre extrait est "Test Radio Stream"
if let Some(title) = metadata.get_title().await.ok().flatten() {
assert_eq!(title, "Test Radio Stream");
found_metadata = true;
}
}
}
}
assert!(found_metadata, "Les métadonnées ICY doivent être extraites");
}
/// Test du comportement en cas d'erreur HTTP 404
#[tokio::test]
async fn test_http_source_handles_404_error() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/notfound.flac"))
.respond_with(ResponseTemplate::new(404))
.mount(&mock_server)
.await;
let url = format!("{}/notfound.flac", mock_server.uri());
let mut source = HttpSource::new(&url);
let (tx, _rx) = mpsc::channel(16);
source.add_subscriber(tx);
let result = source.run().await;
assert!(result.is_err(), "Doit retourner une erreur pour HTTP 404");
if let Err(AudioError::ProcessingError(msg)) = result {
assert!(msg.contains("404"), "Le message d'erreur doit mentionner le code 404");
} else {
panic!("Le type d'erreur doit être ProcessingError");
}
}
/// Test du comportement avec un format audio invalide
#[tokio::test]
async fn test_http_source_handles_invalid_audio_format() {
let mock_server = MockServer::start().await;
// Envoyer des données invalides (pas un fichier audio)
Mock::given(method("GET"))
.and(path("/invalid.flac"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(b"This is not a valid audio file")
.insert_header("content-type", "audio/flac"),
)
.mount(&mock_server)
.await;
let url = format!("{}/invalid.flac", mock_server.uri());
let mut source = HttpSource::new(&url);
let (tx, _rx) = mpsc::channel(16);
source.add_subscriber(tx);
let result = source.run().await;
assert!(
result.is_err(),
"Doit retourner une erreur pour un format invalide"
);
}
/// Test de l'extraction du nom de fichier depuis l'URL quand pas de header icy-name
#[tokio::test]
async fn test_http_source_uses_filename_as_title() {
let mock_server = MockServer::start().await;
let sample_rate = 48_000;
let frames = 128;
let mut pcm = Vec::with_capacity(frames * 4);
for i in 0..frames {
let sample_i16 = (i % 100) as i16;
pcm.extend_from_slice(&sample_i16.to_le_bytes());
pcm.extend_from_slice(&sample_i16.to_le_bytes());
}
let format = PcmFormat {
sample_rate,
channels: 2,
bits_per_sample: 16,
};
let mut flac_stream = encode_flac_stream(Cursor::new(pcm), format, EncoderOptions::default())
.await
.unwrap();
let mut flac_data = Vec::new();
tokio::io::copy(&mut flac_stream, &mut flac_data)
.await
.unwrap();
flac_stream.wait().await.unwrap();
// Sans header icy-name
Mock::given(method("GET"))
.and(path("/my-song.flac"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(flac_data)
.insert_header("content-type", "audio/flac"),
)
.mount(&mock_server)
.await;
let url = format!("{}/my-song.flac", mock_server.uri());
let mut source = HttpSource::new(&url);
let (tx, mut rx) = mpsc::channel(16);
source.add_subscriber(tx);
tokio::spawn(async move {
source.run().await.unwrap();
});
let mut found_title = false;
while let Some(segment) = rx.recv().await {
if let Some(marker) = segment.as_sync_marker() {
if let crate::SyncMarker::TrackBoundary { metadata, .. } = &**marker {
if let Some(title) = metadata.get_title().await.ok().flatten() {
assert_eq!(title, "my-song.flac");
found_title = true;
}
}
}
}
assert!(found_title, "Le nom du fichier doit être utilisé comme titre");
}
}

View File

@@ -6,20 +6,38 @@
use std::sync::Arc;
use tokio::sync::mpsc;
use crate::type_constraints::{TypeMismatch, TypeRequirement};
use crate::AudioSegment;
/// Taille par défaut du buffer de channel MPSC pour les nodes
/// Cette valeur détermine combien de segments audio peuvent être mis en attente
/// avant que le producteur soit bloqué (backpressure).
pub const DEFAULT_CHANNEL_SIZE: usize = 16;
/// Durée par défaut des chunks audio en millisecondes
/// Cette valeur détermine la latence de traitement et le compromis efficacité/réactivité.
/// 50ms offre un bon équilibre pour la plupart des applications de lecture audio.
pub const DEFAULT_CHUNK_DURATION_MS: f64 = 50.0;
// Modules actifs
pub mod converter_nodes;
pub mod file_source;
pub mod flac_file_sink;
pub mod http_source;
// Modules temporairement désactivés
/*
pub mod buffer_node;
pub mod chromecast_sink;
pub mod decoder_node;
pub mod disk_sink;
pub mod dsp_node;
pub mod file_source;
pub mod flac_file_sink;
pub mod mpd_sink;
pub mod sink_node;
pub mod source_node;
pub mod timer_node;
pub mod volume_node;
*/
/// Trait de base pour tous les nodes audio
///
@@ -38,6 +56,59 @@ pub trait AudioNode: Send + Sync {
async fn close(&mut self);
}
/// Trait pour les nodes qui déclarent leurs types acceptés/produits
///
/// Ce trait permet de vérifier la compatibilité des types entre nodes
/// avant de les connecter dans un pipeline.
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::{FileSource, FlacFileSink, TypedAudioNode};
/// use pmoaudio::type_constraints::check_compatibility;
///
/// // Vérifier la compatibilité avant de connecter
/// let source = FileSource::new("input.flac");
/// let (sink, tx) = FlacFileSink::new("output.flac");
///
/// let source_output = source.output_type();
/// let sink_input = sink.input_type();
///
/// match check_compatibility(&source_output, &sink_input) {
/// Ok(()) => println!("Types compatibles!"),
/// Err(e) => eprintln!("Types incompatibles: {}", e),
/// }
/// ```
pub trait TypedAudioNode {
/// Retourne les types que ce node peut accepter en entrée
///
/// Pour les sources (qui ne consomment rien), retourne `None`.
fn input_type(&self) -> Option<TypeRequirement>;
/// Retourne les types que ce node peut produire en sortie
///
/// Pour les sinks (qui ne produisent rien), retourne `None`.
fn output_type(&self) -> Option<TypeRequirement>;
/// Vérifie si ce node peut accepter les chunks d'un producer donné
///
/// # Erreurs
///
/// Retourne `AudioError::TypeMismatch` si les types sont incompatibles
fn can_accept_from(&self, producer: &dyn TypedAudioNode) -> Result<(), AudioError> {
match (producer.output_type(), self.input_type()) {
(Some(prod), Some(cons)) => crate::type_constraints::check_compatibility(&prod, &cons)
.map_err(|e| AudioError::TypeMismatch(e)),
(None, Some(_)) => Err(AudioError::TypeMismatch(TypeMismatch {
producer: TypeRequirement::any(), // Placeholder
consumer: self.input_type().unwrap(),
incompatible_type: None,
})),
_ => Ok(()), // Si pas de contrainte, toujours compatible
}
}
}
/// Node avec un seul abonné (pas de clone inutile)
///
/// Optimisé pour les cas où un node n'a qu'un seul destinataire.
@@ -135,6 +206,8 @@ pub enum AudioError {
ReceiveError,
/// Erreur de traitement avec message descriptif
ProcessingError(String),
/// Incompatibilité de types entre nodes
TypeMismatch(TypeMismatch),
}
impl std::fmt::Display for AudioError {
@@ -143,6 +216,7 @@ impl std::fmt::Display for AudioError {
AudioError::SendError => write!(f, "Failed to send audio chunk"),
AudioError::ReceiveError => write!(f, "Failed to receive audio chunk"),
AudioError::ProcessingError(msg) => write!(f, "Processing error: {}", msg),
AudioError::TypeMismatch(tm) => write!(f, "{}", tm),
}
}
}

View File

@@ -175,23 +175,6 @@ impl TryFrom<i32> for I24 {
// Implémentations du trait Sample pour tous les types
// ============================================================================
impl Sample for i8 {
const NAME: &'static str = "i8";
const MIN: Self = i8::MIN;
const MAX: Self = i8::MAX;
const ZERO: Self = 0;
#[inline]
fn to_f64(self) -> f64 {
self as f64 / 128.0
}
#[inline]
fn from_f64(value: f64) -> Self {
(value * 127.0).clamp(-128.0, 127.0).round() as i8
}
}
impl Sample for i16 {
const NAME: &'static str = "i16";
const MIN: Self = i16::MIN;
@@ -222,7 +205,9 @@ impl Sample for I24 {
#[inline]
fn from_f64(value: f64) -> Self {
let scaled = (value * 8_388_607.0).clamp(-8_388_608.0, 8_388_607.0).round() as i32;
let scaled = (value * 8_388_607.0)
.clamp(-8_388_608.0, 8_388_607.0)
.round() as i32;
I24(scaled)
}
}
@@ -240,7 +225,9 @@ impl Sample for i32 {
#[inline]
fn from_f64(value: f64) -> Self {
(value * 2_147_483_647.0).clamp(-2_147_483_648.0, 2_147_483_647.0).round() as i32
(value * 2_147_483_647.0)
.clamp(-2_147_483_648.0, 2_147_483_647.0)
.round() as i32
}
}

View File

@@ -0,0 +1,385 @@
//! Système de contraintes de types pour les nodes audio
//!
//! Ce module définit les types et structures permettant de vérifier la compatibilité
//! entre les producers et consumers de chunks audio dans un pipeline.
use crate::AudioChunk;
use std::fmt;
/// Type d'échantillon supporté
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SampleType {
/// Entier 16-bit
I16,
/// Entier 24-bit (I24)
I24,
/// Entier 32-bit
I32,
/// Flottant 32-bit
F32,
/// Flottant 64-bit
F64,
}
impl SampleType {
/// Vérifie si le type est un entier
pub fn is_integer(&self) -> bool {
matches!(self, SampleType::I16 | SampleType::I24 | SampleType::I32)
}
/// Vérifie si le type est un flottant
pub fn is_float(&self) -> bool {
matches!(self, SampleType::F32 | SampleType::F64)
}
/// Retourne la profondeur de bit
pub fn bit_depth(&self) -> u8 {
match self {
SampleType::I16 => 16,
SampleType::I24 => 24,
SampleType::I32 | SampleType::F32 => 32,
SampleType::F64 => 64,
}
}
/// Extrait le type d'un AudioChunk
pub fn from_audio_chunk(chunk: &AudioChunk) -> Self {
match chunk {
AudioChunk::I16(_) => SampleType::I16,
AudioChunk::I24(_) => SampleType::I24,
AudioChunk::I32(_) => SampleType::I32,
AudioChunk::F32(_) => SampleType::F32,
AudioChunk::F64(_) => SampleType::F64,
}
}
}
impl fmt::Display for SampleType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SampleType::I16 => write!(f, "I16"),
SampleType::I24 => write!(f, "I24"),
SampleType::I32 => write!(f, "I32"),
SampleType::F32 => write!(f, "F32"),
SampleType::F64 => write!(f, "F64"),
}
}
}
/// Catégorie de type acceptée
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeCategory {
/// N'importe quel type entier (I16, I24, I32)
AnyInteger,
/// N'importe quel type flottant (F32, F64)
AnyFloat,
/// Un type spécifique uniquement
Specific(SampleType),
/// N'importe quel type (entier ou flottant)
Any,
}
impl TypeCategory {
/// Vérifie si cette catégorie accepte le type donné
pub fn accepts(&self, sample_type: SampleType) -> bool {
match self {
TypeCategory::AnyInteger => sample_type.is_integer(),
TypeCategory::AnyFloat => sample_type.is_float(),
TypeCategory::Specific(t) => *t == sample_type,
TypeCategory::Any => true,
}
}
/// Retourne tous les types possibles pour cette catégorie
pub fn possible_types(&self) -> Vec<SampleType> {
match self {
TypeCategory::AnyInteger => vec![SampleType::I16, SampleType::I24, SampleType::I32],
TypeCategory::AnyFloat => vec![SampleType::F32, SampleType::F64],
TypeCategory::Specific(t) => vec![*t],
TypeCategory::Any => vec![
SampleType::I16,
SampleType::I24,
SampleType::I32,
SampleType::F32,
SampleType::F64,
],
}
}
}
impl fmt::Display for TypeCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TypeCategory::AnyInteger => write!(f, "AnyInteger (I16|I24|I32)"),
TypeCategory::AnyFloat => write!(f, "AnyFloat (F32|F64)"),
TypeCategory::Specific(t) => write!(f, "{}", t),
TypeCategory::Any => write!(f, "Any"),
}
}
}
/// Contrainte de type pour un node
#[derive(Debug, Clone)]
pub struct TypeRequirement {
/// Catégorie de type acceptée
pub category: TypeCategory,
/// Types spécifiques acceptés (pour contraintes plus fines)
/// Si vide, utilise category.possible_types()
pub accepted_types: Vec<SampleType>,
}
impl TypeRequirement {
/// Crée une contrainte pour n'importe quel type
pub fn any() -> Self {
Self {
category: TypeCategory::Any,
accepted_types: vec![],
}
}
/// Crée une contrainte pour n'importe quel entier
pub fn any_integer() -> Self {
Self {
category: TypeCategory::AnyInteger,
accepted_types: vec![],
}
}
/// Crée une contrainte pour n'importe quel flottant
pub fn any_float() -> Self {
Self {
category: TypeCategory::AnyFloat,
accepted_types: vec![],
}
}
/// Crée une contrainte pour un type spécifique
pub fn specific(sample_type: SampleType) -> Self {
Self {
category: TypeCategory::Specific(sample_type),
accepted_types: vec![sample_type],
}
}
/// Crée une contrainte avec une liste explicite de types acceptés
pub fn from_list(types: Vec<SampleType>) -> Self {
// Déterminer la catégorie la plus appropriée
let all_integer = types.iter().all(|t| t.is_integer());
let all_float = types.iter().all(|t| t.is_float());
let category = if types.len() == 1 {
TypeCategory::Specific(types[0])
} else if all_integer && types.len() == 3 {
TypeCategory::AnyInteger
} else if all_float && types.len() == 2 {
TypeCategory::AnyFloat
} else if types.len() == 5 {
TypeCategory::Any
} else {
// Catégorie personnalisée - on garde la liste explicite
TypeCategory::Any
};
Self {
category,
accepted_types: types,
}
}
/// Vérifie si cette contrainte accepte le type donné
pub fn accepts(&self, sample_type: SampleType) -> bool {
if !self.accepted_types.is_empty() {
// Si une liste explicite est fournie, utiliser celle-ci
self.accepted_types.contains(&sample_type)
} else {
// Sinon, utiliser la catégorie
self.category.accepts(sample_type)
}
}
/// Retourne tous les types acceptés par cette contrainte
pub fn get_accepted_types(&self) -> Vec<SampleType> {
if !self.accepted_types.is_empty() {
self.accepted_types.clone()
} else {
self.category.possible_types()
}
}
/// Vérifie si cette contrainte est plus restrictive qu'une autre
pub fn is_more_restrictive_than(&self, other: &TypeRequirement) -> bool {
let my_types = self.get_accepted_types();
let other_types = other.get_accepted_types();
// Je suis plus restrictif si tous mes types sont dans other_types
// et que j'en ai moins
my_types.iter().all(|t| other_types.contains(t)) && my_types.len() < other_types.len()
}
}
impl fmt::Display for TypeRequirement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.accepted_types.is_empty() && self.accepted_types.len() < 5 {
write!(
f,
"{}",
self.accepted_types
.iter()
.map(|t| t.to_string())
.collect::<Vec<_>>()
.join("|")
)
} else {
write!(f, "{}", self.category)
}
}
}
/// Vérifie la compatibilité entre un producer et un consumer
///
/// # Règles de compatibilité :
///
/// 1. Si le producer produit un type spécifique et que le consumer l'accepte → compatible
/// 2. Si le producer peut produire plusieurs types (ex: AnyInteger) et que le consumer
/// accepte un type spécifique (ex: I24), le producer POURRAIT produire un type
/// incompatible → **incompatible** (nécessite conversion explicite)
/// 3. Si le producer produit un type spécifique et que le consumer accepte une catégorie
/// contenant ce type → compatible
///
/// # Exemples :
///
/// - Producer(Specific(I24)) + Consumer(AnyInteger) → Compatible ✓
/// - Producer(AnyInteger) + Consumer(Specific(I24)) → Incompatible ✗ (producer peut produire I16)
/// - Producer(Specific(I24)) + Consumer(Specific(I24)) → Compatible ✓
/// - Producer(AnyInteger) + Consumer(AnyInteger) → Compatible ✓
pub fn check_compatibility(
producer: &TypeRequirement,
consumer: &TypeRequirement,
) -> Result<(), TypeMismatch> {
let producer_types = producer.get_accepted_types();
let consumer_types = consumer.get_accepted_types();
// Vérifier si tous les types que le producer peut produire sont acceptés par le consumer
for prod_type in &producer_types {
if !consumer_types.contains(prod_type) {
return Err(TypeMismatch {
producer: producer.clone(),
consumer: consumer.clone(),
incompatible_type: Some(*prod_type),
});
}
}
Ok(())
}
/// Erreur de compatibilité de types
#[derive(Debug, Clone)]
pub struct TypeMismatch {
/// Type requirement du producer
pub producer: TypeRequirement,
/// Type requirement du consumer
pub consumer: TypeRequirement,
/// Type spécifique incompatible (si identifié)
pub incompatible_type: Option<SampleType>,
}
impl fmt::Display for TypeMismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Type mismatch: producer produces {} but consumer only accepts {}",
self.producer, self.consumer
)?;
if let Some(incomp_type) = self.incompatible_type {
write!(f, " (incompatible type: {})", incomp_type)?;
}
Ok(())
}
}
impl std::error::Error for TypeMismatch {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sample_type_is_integer() {
assert!(SampleType::I16.is_integer());
assert!(SampleType::I24.is_integer());
assert!(SampleType::I32.is_integer());
assert!(!SampleType::F32.is_integer());
assert!(!SampleType::F64.is_integer());
}
#[test]
fn test_sample_type_is_float() {
assert!(!SampleType::I16.is_float());
assert!(SampleType::F32.is_float());
assert!(SampleType::F64.is_float());
}
#[test]
fn test_type_category_accepts() {
let any_int = TypeCategory::AnyInteger;
assert!(any_int.accepts(SampleType::I16));
assert!(any_int.accepts(SampleType::I24));
assert!(any_int.accepts(SampleType::I32));
assert!(!any_int.accepts(SampleType::F32));
let specific = TypeCategory::Specific(SampleType::I24);
assert!(!specific.accepts(SampleType::I16));
assert!(specific.accepts(SampleType::I24));
assert!(!specific.accepts(SampleType::I32));
}
#[test]
fn test_compatibility_specific_to_category() {
// Producer(Specific(I24)) + Consumer(AnyInteger) → Compatible
let producer = TypeRequirement::specific(SampleType::I24);
let consumer = TypeRequirement::any_integer();
assert!(check_compatibility(&producer, &consumer).is_ok());
}
#[test]
fn test_compatibility_category_to_specific() {
// Producer(AnyInteger) + Consumer(Specific(I24)) → Incompatible
let producer = TypeRequirement::any_integer();
let consumer = TypeRequirement::specific(SampleType::I24);
assert!(check_compatibility(&producer, &consumer).is_err());
}
#[test]
fn test_compatibility_same_specific() {
// Producer(Specific(I24)) + Consumer(Specific(I24)) → Compatible
let producer = TypeRequirement::specific(SampleType::I24);
let consumer = TypeRequirement::specific(SampleType::I24);
assert!(check_compatibility(&producer, &consumer).is_ok());
}
#[test]
fn test_compatibility_same_category() {
// Producer(AnyInteger) + Consumer(AnyInteger) → Compatible
let producer = TypeRequirement::any_integer();
let consumer = TypeRequirement::any_integer();
assert!(check_compatibility(&producer, &consumer).is_ok());
}
#[test]
fn test_compatibility_integer_to_float() {
// Producer(AnyInteger) + Consumer(AnyFloat) → Incompatible
let producer = TypeRequirement::any_integer();
let consumer = TypeRequirement::any_float();
assert!(check_compatibility(&producer, &consumer).is_err());
}
#[test]
fn test_type_requirement_from_list() {
let req = TypeRequirement::from_list(vec![SampleType::I24, SampleType::I32]);
assert!(req.accepts(SampleType::I24));
assert!(req.accepts(SampleType::I32));
assert!(!req.accepts(SampleType::I16));
assert!(!req.accepts(SampleType::F32));
}
}

View File

@@ -1,5 +1,5 @@
use std::{
ffi::c_void,
ffi::{c_void, CString},
io,
pin::Pin,
task::{Context, Poll},
@@ -85,8 +85,24 @@ impl tokio::io::AsyncRead for FlacEncodedStream {
}
}
use std::sync::Arc;
/// Extracted metadata values for FLAC encoding.
///
/// This is a simple struct containing the extracted values from TrackMetadata,
/// used to pass metadata into the blocking encoder task.
#[derive(Debug, Clone, Default)]
struct ExtractedMetadata {
title: Option<String>,
artist: Option<String>,
album: Option<String>,
year: Option<u32>,
genre: Option<String>,
track_number: Option<u32>,
}
/// Options for configuring FLAC encoding.
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct EncoderOptions {
/// Compression level (0-12). Higher means better compression but slower.
/// Default: 5 (balanced)
@@ -102,6 +118,10 @@ pub struct EncoderOptions {
/// Block size in samples (optional). If None, libFLAC chooses automatically.
/// Typical values: 1152, 2304, 4096.
pub block_size: Option<u32>,
/// Metadata to embed in the FLAC file (Vorbis Comments).
/// Default: None (no metadata)
pub metadata: Option<Arc<dyn pmometadata::TrackMetadata + Send + Sync>>,
}
impl Default for EncoderOptions {
@@ -111,10 +131,23 @@ impl Default for EncoderOptions {
verify: false,
total_samples: None,
block_size: None,
metadata: None,
}
}
}
impl std::fmt::Debug for EncoderOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EncoderOptions")
.field("compression_level", &self.compression_level)
.field("verify", &self.verify)
.field("total_samples", &self.total_samples)
.field("block_size", &self.block_size)
.field("metadata", &self.metadata.as_ref().map(|_| "Some(...)"))
.finish()
}
}
/// Encodes PCM audio data into a FLAC stream.
///
/// This function spawns background tasks to perform the encoding asynchronously.
@@ -212,6 +245,33 @@ where
));
}
// Extract metadata before spawn_blocking (since TrackMetadata has async methods)
let extracted_metadata = if let Some(metadata) = &options.metadata {
let title = metadata.get_title().await.ok().flatten();
let artist = metadata.get_artist().await.ok().flatten();
let album = metadata.get_album().await.ok().flatten();
let year = metadata.get_year().await.ok().flatten();
// Try to extract genre and track_number from extra fields
let extra = metadata.get_extra().await.ok().flatten();
let genre = extra.as_ref().and_then(|e| e.get("genre").cloned());
let track_number = extra.as_ref().and_then(|e| {
e.get("track_number")
.and_then(|s| s.parse::<u32>().ok())
});
Some(ExtractedMetadata {
title,
artist,
album,
year,
genre,
track_number,
})
} else {
None
};
let (pcm_tx, pcm_rx) = mpsc::channel::<Result<PcmChunk, FlacError>>(CHANNEL_CAPACITY);
let format_for_reader = format;
tokio::spawn(async move {
@@ -228,6 +288,7 @@ where
run_encoder(
format_for_encoder,
options_for_encoder,
extracted_metadata,
pcm_rx,
flac_tx,
init_tx,
@@ -306,9 +367,112 @@ where
Ok(())
}
/// RAII guard for FLAC metadata block
struct MetadataGuard {
metadata: *mut libflac_sys::FLAC__StreamMetadata,
}
impl Drop for MetadataGuard {
fn drop(&mut self) {
unsafe {
if !self.metadata.is_null() {
libflac_sys::FLAC__metadata_object_delete(self.metadata);
}
}
}
}
/// Sets up Vorbis Comment metadata for the FLAC encoder
unsafe fn setup_metadata(
encoder: *mut libflac_sys::FLAC__StreamEncoder,
metadata: &ExtractedMetadata,
) -> Result<MetadataGuard, FlacError> {
use libflac_sys::*;
// Create a Vorbis Comment block
let meta = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT);
if meta.is_null() {
return Err(FlacError::LibFlacInit(
"Failed to create metadata block".into(),
));
}
let guard = MetadataGuard { metadata: meta };
// Helper to append a Vorbis comment
let append_comment = |field_name: &str, value: &str| -> Result<(), FlacError> {
let c_field_name = CString::new(field_name).map_err(|_| {
FlacError::LibFlacInit("Failed to create CString for field name".into())
})?;
let c_value = CString::new(value).map_err(|_| {
FlacError::LibFlacInit("Failed to create CString for field value".into())
})?;
let mut entry: FLAC__StreamMetadata_VorbisComment_Entry = std::mem::zeroed();
let success = FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(
&mut entry as *mut _,
c_field_name.as_ptr(),
c_value.as_ptr(),
);
if success == 0 {
return Err(FlacError::LibFlacInit(format!(
"Failed to create metadata entry for {}",
field_name
)));
}
let append_success =
FLAC__metadata_object_vorbiscomment_append_comment(meta, entry, 0 /* copy */);
if append_success == 0 {
return Err(FlacError::LibFlacInit(format!(
"Failed to append metadata entry for {}",
field_name
)));
}
Ok(())
};
// Add all available metadata fields
if let Some(title) = &metadata.title {
append_comment("TITLE", title)?;
}
if let Some(artist) = &metadata.artist {
append_comment("ARTIST", artist)?;
}
if let Some(album) = &metadata.album {
append_comment("ALBUM", album)?;
}
if let Some(year) = metadata.year {
append_comment("DATE", &year.to_string())?;
}
if let Some(genre) = &metadata.genre {
append_comment("GENRE", genre)?;
}
if let Some(track_number) = metadata.track_number {
append_comment("TRACKNUMBER", &track_number.to_string())?;
}
// Set the metadata on the encoder
let mut metadata_array = [meta];
let set_success =
FLAC__stream_encoder_set_metadata(encoder, metadata_array.as_mut_ptr(), 1);
if set_success == 0 {
return Err(FlacError::LibFlacInit(
"Failed to set metadata on encoder".into(),
));
}
Ok(guard)
}
fn run_encoder(
format: PcmFormat,
options: EncoderOptions,
metadata: Option<ExtractedMetadata>,
mut rx: mpsc::Receiver<Result<PcmChunk, FlacError>>,
tx: mpsc::Sender<Result<Vec<u8>, FlacError>>,
init_tx: oneshot::Sender<Result<(), FlacError>>,
@@ -375,6 +539,13 @@ fn run_encoder(
)?;
}
// Setup metadata if provided
let _metadata_guard = if let Some(meta) = metadata {
Some(setup_metadata(encoder, &meta)?)
} else {
None
};
let init_status = FLAC__stream_encoder_init_stream(
encoder,
Some(write_callback),

View File

@@ -15,8 +15,9 @@ use tokio::io::{AsyncRead, AsyncReadExt, BufReader, ReadBuf};
use crate::{
autodetect::{decode_audio_stream, DecodeAudioError, DecodedAudioStream},
encode_flac_stream, prefixed_reader::PrefixedReader, EncoderOptions, FlacEncodedStream,
FlacError, PcmFormat, StreamInfo,
encode_flac_stream,
prefixed_reader::PrefixedReader,
EncoderOptions, FlacEncodedStream, FlacError, PcmFormat, StreamInfo,
};
const READ_CHUNK: usize = 4096;

View File

@@ -32,13 +32,13 @@
//! ```
#![allow(async_fn_in_trait)]
use async_trait::async_trait;
use std::sync::Arc;
use std::{
collections::HashMap,
time::{Duration, SystemTime},
};
use tokio::sync::RwLock;
use std::sync::Arc;
use async_trait::async_trait;
/// Helper macro for copying a single metadata field.
macro_rules! copy_a_metadata {
@@ -157,7 +157,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_title(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
async fn set_title(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -165,7 +165,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_artist(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
async fn set_artist(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -173,7 +173,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_album(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
async fn set_album(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -181,7 +181,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_year(&self) -> MetadataResult<u32> {
Err(MetadataError::NotImplemented)
}
async fn set_year(&mut self, _value: Option<u32>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -189,7 +189,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_duration(&self) -> MetadataResult<Duration> {
Err(MetadataError::NotImplemented)
}
async fn set_duration(&mut self, _value: Option<Duration>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -197,7 +197,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_track_id(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
async fn set_track_id(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -205,7 +205,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_channel_id(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
async fn set_channel_id(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -213,7 +213,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_event(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
async fn set_event(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -221,7 +221,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_rating(&self) -> MetadataResult<f32> {
Err(MetadataError::NotImplemented)
}
async fn set_rating(&mut self, _value: Option<f32>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -229,7 +229,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_cover_url(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
async fn set_cover_url(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -237,7 +237,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_cover_pk(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
async fn set_cover_pk(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -245,7 +245,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_extra(&self) -> MetadataResult<HashMap<String, String>> {
Err(MetadataError::NotImplemented)
}
async fn set_extra(&mut self, _value: Option<HashMap<String, String>>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -253,7 +253,7 @@ pub trait TrackMetadata: Send + Sync {
async fn get_updated_at(&self) -> MetadataResult<SystemTime> {
Err(MetadataError::NotImplemented)
}
async fn touch(&mut self) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
@@ -318,8 +318,8 @@ where
let src_guard = src.read().await;
copy_metadata!(
src_guard, dest, title, artist, album, year, duration, track_id,
channel_id, event, rating, cover_url, cover_pk, extra
src_guard, dest, title, artist, album, year, duration, track_id, channel_id, event, rating,
cover_url, cover_pk, extra
);
// Try to update the timestamp, but ignore transient errors
@@ -359,7 +359,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_title(&self) -> MetadataResult<String> {
Ok(self.title.clone())
}
async fn set_title(&mut self, value: Option<String>) -> MetadataResult<()> {
self.title = value;
self.touch().await?;
@@ -369,7 +369,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_artist(&self) -> MetadataResult<String> {
Ok(self.artist.clone())
}
async fn set_artist(&mut self, value: Option<String>) -> MetadataResult<()> {
self.artist = value;
self.touch().await?;
@@ -379,7 +379,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_album(&self) -> MetadataResult<String> {
Ok(self.album.clone())
}
async fn set_album(&mut self, value: Option<String>) -> MetadataResult<()> {
self.album = value;
self.touch().await?;
@@ -389,7 +389,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_year(&self) -> MetadataResult<u32> {
Ok(self.year)
}
async fn set_year(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.year = value;
self.touch().await?;
@@ -399,7 +399,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_duration(&self) -> MetadataResult<Duration> {
Ok(self.duration)
}
async fn set_duration(&mut self, value: Option<Duration>) -> MetadataResult<()> {
self.duration = value;
self.touch().await?;
@@ -409,7 +409,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_track_id(&self) -> MetadataResult<String> {
Ok(self.track_id.clone())
}
async fn set_track_id(&mut self, value: Option<String>) -> MetadataResult<()> {
self.track_id = value;
self.touch().await?;
@@ -419,7 +419,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_channel_id(&self) -> MetadataResult<String> {
Ok(self.channel_id.clone())
}
async fn set_channel_id(&mut self, value: Option<String>) -> MetadataResult<()> {
self.channel_id = value;
self.touch().await?;
@@ -429,7 +429,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_event(&self) -> MetadataResult<String> {
Ok(self.event.clone())
}
async fn set_event(&mut self, value: Option<String>) -> MetadataResult<()> {
self.event = value;
self.touch().await?;
@@ -439,7 +439,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_rating(&self) -> MetadataResult<f32> {
Ok(self.rating)
}
async fn set_rating(&mut self, value: Option<f32>) -> MetadataResult<()> {
self.rating = value;
self.touch().await?;
@@ -449,7 +449,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_cover_url(&self) -> MetadataResult<String> {
Ok(self.cover_url.clone())
}
async fn set_cover_url(&mut self, value: Option<String>) -> MetadataResult<()> {
self.cover_url = value;
self.touch().await?;
@@ -459,7 +459,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_cover_pk(&self) -> MetadataResult<String> {
Ok(self.cover_pk.clone())
}
async fn set_cover_pk(&mut self, value: Option<String>) -> MetadataResult<()> {
self.cover_pk = value;
self.touch().await?;
@@ -469,7 +469,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_extra(&self) -> MetadataResult<HashMap<String, String>> {
Ok(self.extra.clone())
}
async fn set_extra(&mut self, value: Option<HashMap<String, String>>) -> MetadataResult<()> {
self.extra = value;
self.touch().await?;
@@ -479,7 +479,7 @@ impl TrackMetadata for MemoryTrackMetadata {
async fn get_updated_at(&self) -> MetadataResult<SystemTime> {
Ok(self.updated_at)
}
async fn touch(&mut self) -> MetadataResult<()> {
self.updated_at = Some(SystemTime::now());
Ok(Some(()))
@@ -518,7 +518,10 @@ mod tests {
async fn test_memory_metadata_set_get_artist() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_artist(Some("Artist Name".to_string())).await.unwrap();
metadata
.set_artist(Some("Artist Name".to_string()))
.await
.unwrap();
assert_eq!(
metadata.get_artist().await.unwrap(),
Some("Artist Name".to_string())
@@ -529,7 +532,10 @@ mod tests {
async fn test_memory_metadata_set_get_album() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_album(Some("Album Name".to_string())).await.unwrap();
metadata
.set_album(Some("Album Name".to_string()))
.await
.unwrap();
assert_eq!(
metadata.get_album().await.unwrap(),
Some("Album Name".to_string())
@@ -565,7 +571,10 @@ mod tests {
async fn test_memory_metadata_set_get_track_id() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_track_id(Some("track123".to_string())).await.unwrap();
metadata
.set_track_id(Some("track123".to_string()))
.await
.unwrap();
assert_eq!(
metadata.get_track_id().await.unwrap(),
Some("track123".to_string())
@@ -576,7 +585,10 @@ mod tests {
async fn test_memory_metadata_set_get_channel_id() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_channel_id(Some("channel456".to_string())).await.unwrap();
metadata
.set_channel_id(Some("channel456".to_string()))
.await
.unwrap();
assert_eq!(
metadata.get_channel_id().await.unwrap(),
Some("channel456".to_string())
@@ -587,7 +599,10 @@ mod tests {
async fn test_memory_metadata_set_get_event() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_event(Some("event789".to_string())).await.unwrap();
metadata
.set_event(Some("event789".to_string()))
.await
.unwrap();
assert_eq!(
metadata.get_event().await.unwrap(),
Some("event789".to_string())
@@ -598,7 +613,10 @@ mod tests {
async fn test_memory_metadata_set_get_cover_url() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_cover_url(Some("https://example.com/cover.jpg".to_string())).await.unwrap();
metadata
.set_cover_url(Some("https://example.com/cover.jpg".to_string()))
.await
.unwrap();
assert_eq!(
metadata.get_cover_url().await.unwrap(),
Some("https://example.com/cover.jpg".to_string())
@@ -609,7 +627,10 @@ mod tests {
async fn test_memory_metadata_set_get_cover_pk() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_cover_pk(Some("pk123".to_string())).await.unwrap();
metadata
.set_cover_pk(Some("pk123".to_string()))
.await
.unwrap();
assert_eq!(
metadata.get_cover_pk().await.unwrap(),
Some("pk123".to_string())
@@ -632,7 +653,10 @@ mod tests {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_title(Some("Title".to_string())).await.unwrap();
assert_eq!(metadata.get_title().await.unwrap(), Some("Title".to_string()));
assert_eq!(
metadata.get_title().await.unwrap(),
Some("Title".to_string())
);
metadata.set_title(None).await.unwrap();
assert_eq!(metadata.get_title().await.unwrap(), None);
@@ -670,8 +694,12 @@ mod tests {
#[tokio::test]
async fn test_copy_metadata_into_basic() {
let mut src = MemoryTrackMetadata::new();
src.set_title(Some("Source Title".to_string())).await.unwrap();
src.set_artist(Some("Source Artist".to_string())).await.unwrap();
src.set_title(Some("Source Title".to_string()))
.await
.unwrap();
src.set_artist(Some("Source Artist".to_string()))
.await
.unwrap();
src.set_year(Some(2024)).await.unwrap();
let dest = MemoryTrackMetadata::new();
@@ -682,8 +710,14 @@ mod tests {
copy_metadata_into(&src_lock, &dest_lock).await.unwrap();
let dest_guard = dest_lock.read().await;
assert_eq!(dest_guard.get_title().await.unwrap(), Some("Source Title".to_string()));
assert_eq!(dest_guard.get_artist().await.unwrap(), Some("Source Artist".to_string()));
assert_eq!(
dest_guard.get_title().await.unwrap(),
Some("Source Title".to_string())
);
assert_eq!(
dest_guard.get_artist().await.unwrap(),
Some("Source Artist".to_string())
);
assert_eq!(dest_guard.get_year().await.unwrap(), Some(2024));
}
@@ -701,7 +735,10 @@ mod tests {
copy_metadata_into(&src_lock, &dest_lock).await.unwrap();
let dest_guard = dest_lock.read().await;
assert_eq!(dest_guard.get_title().await.unwrap(), Some("Title".to_string()));
assert_eq!(
dest_guard.get_title().await.unwrap(),
Some("Title".to_string())
);
assert_eq!(dest_guard.get_artist().await.unwrap(), None);
}
@@ -729,12 +766,20 @@ mod tests {
src.set_artist(Some("Artist".to_string())).await.unwrap();
src.set_album(Some("Album".to_string())).await.unwrap();
src.set_year(Some(2024)).await.unwrap();
src.set_duration(Some(Duration::from_secs(180))).await.unwrap();
src.set_track_id(Some("track123".to_string())).await.unwrap();
src.set_channel_id(Some("channel456".to_string())).await.unwrap();
src.set_duration(Some(Duration::from_secs(180)))
.await
.unwrap();
src.set_track_id(Some("track123".to_string()))
.await
.unwrap();
src.set_channel_id(Some("channel456".to_string()))
.await
.unwrap();
src.set_event(Some("event789".to_string())).await.unwrap();
src.set_rating(Some(4.5)).await.unwrap();
src.set_cover_url(Some("https://example.com/cover.jpg".to_string())).await.unwrap();
src.set_cover_url(Some("https://example.com/cover.jpg".to_string()))
.await
.unwrap();
src.set_cover_pk(Some("pk123".to_string())).await.unwrap();
let mut extra = HashMap::new();
@@ -749,17 +794,44 @@ mod tests {
copy_metadata_into(&src_lock, &dest_lock).await.unwrap();
let dest_guard = dest_lock.read().await;
assert_eq!(dest_guard.get_title().await.unwrap(), Some("Title".to_string()));
assert_eq!(dest_guard.get_artist().await.unwrap(), Some("Artist".to_string()));
assert_eq!(dest_guard.get_album().await.unwrap(), Some("Album".to_string()));
assert_eq!(
dest_guard.get_title().await.unwrap(),
Some("Title".to_string())
);
assert_eq!(
dest_guard.get_artist().await.unwrap(),
Some("Artist".to_string())
);
assert_eq!(
dest_guard.get_album().await.unwrap(),
Some("Album".to_string())
);
assert_eq!(dest_guard.get_year().await.unwrap(), Some(2024));
assert_eq!(dest_guard.get_duration().await.unwrap(), Some(Duration::from_secs(180)));
assert_eq!(dest_guard.get_track_id().await.unwrap(), Some("track123".to_string()));
assert_eq!(dest_guard.get_channel_id().await.unwrap(), Some("channel456".to_string()));
assert_eq!(dest_guard.get_event().await.unwrap(), Some("event789".to_string()));
assert_eq!(
dest_guard.get_duration().await.unwrap(),
Some(Duration::from_secs(180))
);
assert_eq!(
dest_guard.get_track_id().await.unwrap(),
Some("track123".to_string())
);
assert_eq!(
dest_guard.get_channel_id().await.unwrap(),
Some("channel456".to_string())
);
assert_eq!(
dest_guard.get_event().await.unwrap(),
Some("event789".to_string())
);
assert_eq!(dest_guard.get_rating().await.unwrap(), Some(4.5));
assert_eq!(dest_guard.get_cover_url().await.unwrap(), Some("https://example.com/cover.jpg".to_string()));
assert_eq!(dest_guard.get_cover_pk().await.unwrap(), Some("pk123".to_string()));
assert_eq!(
dest_guard.get_cover_url().await.unwrap(),
Some("https://example.com/cover.jpg".to_string())
);
assert_eq!(
dest_guard.get_cover_pk().await.unwrap(),
Some("pk123".to_string())
);
assert_eq!(dest_guard.get_extra().await.unwrap(), Some(extra));
}
@@ -781,7 +853,13 @@ mod tests {
// Verify the data was copied
let dest_guard = dest_lock.read().await;
assert_eq!(dest_guard.get_title().await.unwrap(), Some("Title".to_string()));
assert_eq!(dest_guard.get_artist().await.unwrap(), Some("Artist".to_string()));
assert_eq!(
dest_guard.get_title().await.unwrap(),
Some("Title".to_string())
);
assert_eq!(
dest_guard.get_artist().await.unwrap(),
Some("Artist".to_string())
);
}
}