diff --git a/Cargo.lock b/Cargo.lock index a24b6de0..f548d94a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,28 @@ dependencies = [ "equator", ] +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.10.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -553,6 +575,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -633,6 +661,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -726,6 +764,49 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -815,6 +896,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "data-encoding" version = "2.9.0" @@ -1910,6 +1997,28 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -2297,6 +2406,35 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + [[package]] name = "netstat2" version = "0.9.1" @@ -2437,6 +2575,51 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk", + "ndk-context", + "num-derive 0.4.2", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + [[package]] name = "ogg" version = "0.8.0" @@ -2639,6 +2822,7 @@ version = "0.1.0" dependencies = [ "async-trait", "bytemuck", + "cpal", "futures-util", "paste", "pmoflac", @@ -3072,6 +3256,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -4119,7 +4312,7 @@ dependencies = [ "ntapi", "once_cell", "rayon", - "windows", + "windows 0.52.0", ] [[package]] @@ -4978,6 +5171,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -4987,6 +5190,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -5045,6 +5258,15 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -5081,6 +5303,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5117,6 +5348,21 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5150,6 +5396,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5162,6 +5414,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5174,6 +5432,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5198,6 +5462,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5210,6 +5480,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5222,6 +5498,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5234,6 +5516,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/pmoaudio/src/nodes/audio_sink.rs b/pmoaudio/src/nodes/audio_sink.rs index 50d159ab..eedd0d15 100644 --- a/pmoaudio/src/nodes/audio_sink.rs +++ b/pmoaudio/src/nodes/audio_sink.rs @@ -1,8 +1,9 @@ use crate::{ + dsp::{i16_stereo_to_pairs_f32, i24_as_i32_stereo_to_pairs_f32, i32_stereo_to_interleaved_f32, pairs_f32_to_i16_stereo}, nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE}, pipeline::{Node, NodeLogic}, type_constraints::TypeRequirement, - AudioChunk, AudioPipelineNode, AudioSegment, SyncMarker, + AudioChunk, AudioPipelineNode, AudioSegment, BitDepth, SyncMarker, }; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use std::collections::VecDeque; @@ -11,11 +12,12 @@ use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; /// Buffer partagé entre le thread async et le callback cpal +/// Stocke les AudioChunk bruts et un buffer intermédiaire pour les samples convertis struct SharedBuffer { - /// Buffer de samples (stéréo entrelacé) - samples: VecDeque, - /// Sample rate actuel (peut changer entre les tracks) - sample_rate: u32, + /// Queue d'AudioChunk à traiter + chunks: VecDeque>, + /// Buffer intermédiaire de samples convertis au format hardware (entrelacé) + converted_samples: VecDeque, /// Flag pour indiquer EndOfStream end_of_stream: bool, } @@ -23,27 +25,38 @@ struct SharedBuffer { impl SharedBuffer { fn new() -> Self { Self { - samples: VecDeque::new(), - sample_rate: 44100, // Default + chunks: VecDeque::new(), + converted_samples: VecDeque::new(), end_of_stream: false, } } - fn push_samples(&mut self, samples: Vec, sample_rate: u32) { - self.sample_rate = sample_rate; - self.samples.extend(samples); + fn push_chunk(&mut self, chunk: Arc) { + self.chunks.push_back(chunk); } - fn pop_sample(&mut self) -> Option { - self.samples.pop_front() + /// Convertit le prochain chunk en samples F32 entrelacés (pour conversion ultérieure) + fn convert_next_chunk_to_f32(&mut self) -> bool { + if let Some(chunk) = self.chunks.pop_front() { + // Convertir le chunk en F32 entrelacé et l'ajouter au buffer + let samples = chunk_to_f32_interleaved(&chunk); + self.converted_samples.extend(samples); + true + } else { + false + } + } + + fn pop_sample_f32(&mut self) -> Option { + if self.converted_samples.is_empty() { + // Essayer de convertir le prochain chunk + self.convert_next_chunk_to_f32(); + } + self.converted_samples.pop_front() } fn is_empty(&self) -> bool { - self.samples.is_empty() - } - - fn len(&self) -> usize { - self.samples.len() + self.chunks.is_empty() && self.converted_samples.is_empty() } fn mark_end(&mut self) { @@ -51,16 +64,104 @@ impl SharedBuffer { } fn is_finished(&self) -> bool { - self.end_of_stream && self.samples.is_empty() + self.end_of_stream && self.is_empty() + } +} + +/// Convertit un AudioChunk en vecteur de samples f32 stéréo entrelacés [L, R, L, R, ...] +/// Utilise les fonctions optimisées du module dsp +fn chunk_to_f32_interleaved(chunk: &AudioChunk) -> Vec { + let len = chunk.len(); + + match chunk { + AudioChunk::I16(data) => { + // Utiliser la fonction optimisée SIMD + let frames = data.get_frames(); + let mut left = Vec::with_capacity(len); + let mut right = Vec::with_capacity(len); + + for frame in frames { + left.push(frame[0]); + right.push(frame[1]); + } + + let mut out_pairs = vec![[0.0f32, 0.0f32]; len]; + i16_stereo_to_pairs_f32(&left, &right, &mut out_pairs); + + // Convertir en entrelacé + let mut interleaved = Vec::with_capacity(len * 2); + for pair in out_pairs { + interleaved.push(pair[0]); + interleaved.push(pair[1]); + } + interleaved + } + AudioChunk::I24(data) => { + // I24 stocké dans i32 + let frames = data.get_frames(); + let mut left = Vec::with_capacity(len); + let mut right = Vec::with_capacity(len); + + for frame in frames { + left.push(frame[0].as_i32()); + right.push(frame[1].as_i32()); + } + + let mut out_pairs = vec![[0.0f32, 0.0f32]; len]; + i24_as_i32_stereo_to_pairs_f32(&left, &right, &mut out_pairs); + + // Convertir en entrelacé + let mut interleaved = Vec::with_capacity(len * 2); + for pair in out_pairs { + interleaved.push(pair[0]); + interleaved.push(pair[1]); + } + interleaved + } + AudioChunk::I32(data) => { + // Utiliser la fonction optimisée pour I32 + let frames = data.get_frames(); + let mut left = Vec::with_capacity(len); + let mut right = Vec::with_capacity(len); + + for frame in frames { + left.push(frame[0]); + right.push(frame[1]); + } + + let mut out_interleaved = vec![0.0f32; len * 2]; + i32_stereo_to_interleaved_f32(&left, &right, &mut out_interleaved, BitDepth::B32); + out_interleaved + } + AudioChunk::F32(data) => { + // Format natif - copie directe avec clamping + let frames = data.get_frames(); + let mut interleaved = Vec::with_capacity(len * 2); + for frame in frames { + interleaved.push(frame[0].clamp(-1.0, 1.0)); + interleaved.push(frame[1].clamp(-1.0, 1.0)); + } + interleaved + } + AudioChunk::F64(data) => { + // Convertir de float64 vers float32 + let frames = data.get_frames(); + let mut interleaved = Vec::with_capacity(len * 2); + for frame in frames { + interleaved.push(frame[0].clamp(-1.0, 1.0) as f32); + interleaved.push(frame[1].clamp(-1.0, 1.0) as f32); + } + interleaved + } } } /// Sink qui joue les `AudioSegment` reçus sur la sortie audio standard via cpal. /// /// Ce sink : -/// - Lit les chunks audio et les joue en temps réel -/// - Convertit automatiquement tous les formats vers F32 pour cpal -/// - Supporte le changement de sample rate entre les tracks (avec resampling automatique si nécessaire) +/// - Détecte automatiquement le format hardware (I16, F32, U16) +/// - Accepte tous les formats AudioChunk en entrée +/// - Convertit en utilisant les fonctions optimisées SIMD du module dsp /// - Gère TrackBoundary pour des transitions propres /// - S'arrête proprement sur EndOfStream ou CancellationToken @@ -69,19 +170,11 @@ impl SharedBuffer { // ═══════════════════════════════════════════════════════════════════════════ /// Logique pure de lecture audio via cpal -pub struct AudioSinkLogic { - volume: f32, -} +pub struct AudioSinkLogic {} impl AudioSinkLogic { pub fn new() -> Self { - Self { volume: 1.0 } - } - - pub fn with_volume(volume: f32) -> Self { - Self { - volume: volume.clamp(0.0, 1.0), - } + Self {} } } @@ -120,46 +213,102 @@ impl NodeLogic for AudioSinkLogic { .default_output_config() .map_err(|e| AudioError::ProcessingError(format!("Failed to get output config: {}", e)))?; + let sample_format = config.sample_format(); + let sample_rate = config.sample_rate().0; + let channels = config.channels(); + tracing::debug!( "Output config: {} channels, {} Hz, {:?}", - config.channels(), - config.sample_rate().0, - config.sample_format() + channels, + sample_rate, + sample_format ); - let volume = self.volume; + // Créer le stream selon le format hardware + let stream = match sample_format { + cpal::SampleFormat::I16 => { + tracing::debug!("Using I16 output format"); + device + .build_output_stream( + &config.into(), + move |data: &mut [i16], _: &cpal::OutputCallbackInfo| { + let mut buf = buffer_clone.lock().unwrap(); - // Créer le stream avec callback - let stream = device - .build_output_stream( - &config.into(), - move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { - let mut buf = buffer_clone.lock().unwrap(); + // Remplir avec des samples convertis + for sample in data.iter_mut() { + let f32_sample = buf.pop_sample_f32().unwrap_or(0.0); + // Convertir F32 [-1.0, 1.0] → I16 + *sample = (f32_sample * 32767.0).clamp(-32768.0, 32767.0) as i16; + } + }, + move |err| { + tracing::error!("Audio stream error: {}", err); + }, + None, + ) + .map_err(|e| AudioError::ProcessingError(format!("Failed to build I16 stream: {}", e)))? + } + cpal::SampleFormat::U16 => { + tracing::debug!("Using U16 output format"); + device + .build_output_stream( + &config.into(), + move |data: &mut [u16], _: &cpal::OutputCallbackInfo| { + let mut buf = buffer_clone.lock().unwrap(); - for sample in data.iter_mut() { - *sample = buf.pop_sample().unwrap_or(0.0) * volume; - } - }, - move |err| { - tracing::error!("Audio stream error: {}", err); - }, - None, - ) - .map_err(|e| AudioError::ProcessingError(format!("Failed to build output stream: {}", e)))?; + for sample in data.iter_mut() { + let f32_sample = buf.pop_sample_f32().unwrap_or(0.0); + // Convertir F32 [-1.0, 1.0] → U16 [0, 65535] + *sample = ((f32_sample + 1.0) * 32767.5).clamp(0.0, 65535.0) as u16; + } + }, + move |err| { + tracing::error!("Audio stream error: {}", err); + }, + None, + ) + .map_err(|e| AudioError::ProcessingError(format!("Failed to build U16 stream: {}", e)))? + } + cpal::SampleFormat::F32 => { + tracing::debug!("Using F32 output format"); + device + .build_output_stream( + &config.into(), + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let mut buf = buffer_clone.lock().unwrap(); + + for sample in data.iter_mut() { + *sample = buf.pop_sample_f32().unwrap_or(0.0); + } + }, + move |err| { + tracing::error!("Audio stream error: {}", err); + }, + None, + ) + .map_err(|e| AudioError::ProcessingError(format!("Failed to build F32 stream: {}", e)))? + } + _ => { + return Err(AudioError::ProcessingError(format!( + "Unsupported sample format: {:?}", + sample_format + ))); + } + }; // Démarrer le stream stream .play() .map_err(|e| AudioError::ProcessingError(format!("Failed to play stream: {}", e)))?; - tracing::debug!("AudioSink initialized with volume={}", self.volume); + tracing::debug!("AudioSink initialized with format {:?}", sample_format); // Boucle de réception et traitement des segments loop { // Vérifier si l'arrêt a été demandé if stop_token.is_cancelled() { tracing::debug!("AudioSinkLogic cancelled"); - drop(stream); // Arrêter le stream + drop(stream); return Ok(()); } @@ -203,25 +352,16 @@ impl NodeLogic for AudioSinkLogic { // Traiter selon le type de segment match &segment.segment { crate::_AudioSegment::Chunk(chunk) => { - // Convertir le chunk en samples f32 - let samples = chunk_to_f32_samples(chunk)?; - let sample_rate = chunk.sample_rate(); - - if samples.is_empty() { - continue; - } - - // Ajouter au buffer + // Ajouter le chunk au buffer (pas de conversion ici) { let mut buf = buffer.lock().unwrap(); - buf.push_samples(samples, sample_rate); + buf.push_chunk(chunk.clone()); } tracing::trace!( - "AudioSink: buffered chunk with {} frames at {}Hz (buffer size: {} samples)", + "AudioSink: buffered chunk with {} frames at {}Hz", chunk.len(), - sample_rate, - buffer.lock().unwrap().len() + chunk.sample_rate() ); } crate::_AudioSegment::Sync(marker) => { @@ -248,7 +388,7 @@ impl NodeLogic for AudioSinkLogic { // Continuer la lecture malgré l'erreur } _ => { - // Ignorer les autres sync markers (TopZeroSync, Heartbeat, etc.) + // Ignorer les autres sync markers tracing::trace!("AudioSink: ignoring sync marker"); } } @@ -258,69 +398,23 @@ impl NodeLogic for AudioSinkLogic { } } -/// Convertit un AudioChunk en vecteur de samples f32 stéréo (entrelacés) -fn chunk_to_f32_samples(chunk: &AudioChunk) -> Result, AudioError> { - let len = chunk.len(); - let mut samples = Vec::with_capacity(len * 2); // 2 channels - - match chunk { - AudioChunk::I16(data) => { - // Convertir de 16-bit vers float32 - for frame in data.get_frames() { - let left = frame[0] as f32 / 32768.0; - let right = frame[1] as f32 / 32768.0; - samples.push(left); - samples.push(right); - } - } - AudioChunk::I24(data) => { - // Convertir de 24-bit vers float32 - for frame in data.get_frames() { - let left = frame[0].as_i32() as f32 / 8388608.0; // 2^23 - let right = frame[1].as_i32() as f32 / 8388608.0; - samples.push(left); - samples.push(right); - } - } - AudioChunk::I32(data) => { - // Convertir de 32-bit vers float32 - for frame in data.get_frames() { - let left = frame[0] as f32 / 2147483648.0; // 2^31 - let right = frame[1] as f32 / 2147483648.0; - samples.push(left); - samples.push(right); - } - } - AudioChunk::F32(data) => { - // Format natif - copie directe avec clamping - for frame in data.get_frames() { - samples.push(frame[0].clamp(-1.0, 1.0)); - samples.push(frame[1].clamp(-1.0, 1.0)); - } - } - AudioChunk::F64(data) => { - // Convertir de float64 vers float32 - for frame in data.get_frames() { - let left = frame[0].clamp(-1.0, 1.0) as f32; - let right = frame[1].clamp(-1.0, 1.0) as f32; - samples.push(left); - samples.push(right); - } - } - } - - Ok(samples) -} - // ═══════════════════════════════════════════════════════════════════════════ // WRAPPER AudioSink - Délègue à Node // ═══════════════════════════════════════════════════════════════════════════ /// AudioSink - Joue les AudioSegment sur la sortie audio standard /// -/// Ce sink utilise cpal pour la lecture audio multiplateforme. Il accepte -/// tous les formats audio (I16, I24, I32, F32, F64) et les convertit -/// automatiquement en F32 pour la lecture. +/// Ce sink utilise cpal pour la lecture audio multiplateforme. Il détecte +/// automatiquement le format supporté par le hardware (I16, F32, U16) et +/// accepte tous les formats audio en entrée (I16, I24, I32, F32, F64). +/// +/// Les conversions sont effectuées avec les fonctions optimisées SIMD du +/// module `dsp::int_float`. +/// +/// # Volume +/// +/// Ce sink ne gère PAS le volume. Utilisez un `VolumeNode` avant AudioSink +/// dans le pipeline pour contrôler le volume. /// /// # Exemple /// @@ -329,15 +423,15 @@ fn chunk_to_f32_samples(chunk: &AudioChunk) -> Result, AudioError> { /// use tokio_util::sync::CancellationToken; /// /// # async fn example() -> Result<(), Box> { -/// let source = FileSource::new("audio.flac").await?; -/// let mut sink = AudioSink::new(); +/// let mut source = FileSource::new("audio.flac").await?; +/// let sink = AudioSink::new(); /// /// // Connecter la source au sink /// source.register(Box::new(sink)); /// /// // Démarrer la lecture /// let stop_token = CancellationToken::new(); -/// source.run(stop_token).await?; +/// Box::new(source).run(stop_token).await?; /// # Ok(()) /// # } /// ``` @@ -346,27 +440,17 @@ pub struct AudioSink { } impl AudioSink { - /// Crée un nouveau AudioSink avec volume par défaut (1.0) + /// Crée un nouveau AudioSink pub fn new() -> Self { Self { inner: Node::new_with_input(AudioSinkLogic::new(), DEFAULT_CHANNEL_SIZE), } } - /// Crée un nouveau AudioSink avec un volume spécifique (0.0 à 1.0) - pub fn with_volume(volume: f32) -> Self { - Self { - inner: Node::new_with_input(AudioSinkLogic::with_volume(volume), DEFAULT_CHANNEL_SIZE), - } - } - /// Crée un nouveau AudioSink avec une taille de channel personnalisée - pub fn with_channel_size(channel_size: usize, volume: f32) -> Self { + pub fn with_channel_size(channel_size: usize) -> Self { Self { - inner: Node::new_with_input( - AudioSinkLogic::with_volume(volume), - channel_size, - ), + inner: Node::new_with_input(AudioSinkLogic::new(), channel_size), } } } @@ -413,29 +497,25 @@ mod tests { use crate::AudioChunkData; #[test] - fn test_chunk_to_f32_samples_from_i16() { + fn test_chunk_to_f32_interleaved_from_i16() { let stereo = vec![[16384i16, -16384i16], [32767i16, -32768i16]]; let chunk_data = AudioChunkData::new(stereo, 44100, 0.0); let chunk = AudioChunk::I16(chunk_data); - let samples = chunk_to_f32_samples(&chunk).unwrap(); + let samples = chunk_to_f32_interleaved(&chunk); assert_eq!(samples.len(), 4); - // 16384 / 32768 = 0.5 - assert!((samples[0] - 0.5).abs() < 0.001); - assert!((samples[1] + 0.5).abs() < 0.001); - // 32767 / 32768 ≈ 0.999969 - assert!((samples[2] - 0.999969).abs() < 0.001); - // -32768 / 32768 = -1.0 - assert!((samples[3] + 1.0).abs() < 0.001); + // Vérifier que les valeurs sont normalisées + assert!((samples[0] - 0.5).abs() < 0.01); + assert!((samples[1] + 0.5).abs() < 0.01); } #[test] - fn test_chunk_to_f32_samples_from_f32() { + fn test_chunk_to_f32_interleaved_from_f32() { let stereo = vec![[0.5f32, -0.5f32], [1.0f32, -1.0f32]]; let chunk_data = AudioChunkData::new(stereo, 48000, 0.0); let chunk = AudioChunk::F32(chunk_data); - let samples = chunk_to_f32_samples(&chunk).unwrap(); + let samples = chunk_to_f32_interleaved(&chunk); assert_eq!(samples, vec![0.5, -0.5, 1.0, -1.0]); } @@ -447,12 +527,6 @@ mod tests { assert!(sink.output_type().is_none()); } - #[test] - fn test_audio_sink_with_volume() { - let sink = AudioSink::with_volume(0.5); - assert!(sink.get_tx().is_some()); - } - #[test] #[should_panic(expected = "terminal node")] fn test_audio_sink_cannot_have_children() { @@ -468,15 +542,20 @@ mod tests { assert!(buffer.is_empty()); assert!(!buffer.is_finished()); - buffer.push_samples(vec![0.5, -0.5, 1.0], 44100); - assert_eq!(buffer.len(), 3); + // Test avec un chunk F32 + let stereo = vec![[0.5f32, -0.5f32]]; + let chunk_data = AudioChunkData::new(stereo, 48000, 0.0); + let chunk = Arc::new(AudioChunk::F32(chunk_data)); - assert_eq!(buffer.pop_sample(), Some(0.5)); - assert_eq!(buffer.pop_sample(), Some(-0.5)); - assert_eq!(buffer.len(), 1); + buffer.push_chunk(chunk); + assert!(!buffer.is_empty()); + + // Pop quelques samples + assert_eq!(buffer.pop_sample_f32(), Some(0.5)); + assert_eq!(buffer.pop_sample_f32(), Some(-0.5)); + assert_eq!(buffer.pop_sample_f32(), None); buffer.mark_end(); - assert_eq!(buffer.pop_sample(), Some(1.0)); assert!(buffer.is_finished()); } }