From 0fd06766e37c5cf175e40467228f849991adb7b0 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 28 Aug 2026 11:31:24 -0400 Subject: [PATCH 1/3] Added pipewire and pulseaudio --- src-tauri/Cargo.lock | 289 ++++++++++++++++++++++++++++++++++++++++--- src-tauri/Cargo.toml | 2 +- 2 files changed, 272 insertions(+), 19 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 01365dc..4198d48 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -73,6 +73,22 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.104" @@ -230,7 +246,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -290,6 +306,25 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "annotate-snippets", + "bitflags 2.13.1", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -439,7 +474,7 @@ checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" dependencies = [ "glib-sys", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -491,7 +526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -500,6 +535,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfb" version = "0.7.3" @@ -518,7 +562,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", - "target-lexicon", + "target-lexicon 0.12.16", +] + +[[package]] +name = "cfg-expr" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4ece8474b5f766c63426647e7b4b316b67431ade1036a8313cee24a03ae917" +dependencies = [ + "smallvec", + "target-lexicon 0.13.5", ] [[package]] @@ -573,6 +627,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + [[package]] name = "cmov" version = "0.5.4" @@ -608,6 +673,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" + [[package]] name = "core-foundation" version = "0.10.1" @@ -672,6 +743,7 @@ dependencies = [ "block2", "coreaudio-rs", "dasp_sample", + "futures", "jni 0.22.4", "js-sys", "libc", @@ -687,6 +759,9 @@ dependencies = [ "objc2-core-audio-types", "objc2-core-foundation", "objc2-foundation", + "pipewire", + "portable-atomic", + "pulseaudio", "web-sys", "windows 0.62.2", "windows-core 0.62.2", @@ -1058,6 +1133,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "embed-resource" version = "3.0.11" @@ -1099,6 +1180,17 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enum-primitive-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c" +dependencies = [ + "num-traits", + "quote", + "syn 2.0.119", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -1256,6 +1348,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.34" @@ -1263,6 +1370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1330,6 +1438,7 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1378,7 +1487,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -1395,7 +1504,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -1409,7 +1518,7 @@ dependencies = [ "gobject-sys", "libc", "pkg-config", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -1435,7 +1544,7 @@ dependencies = [ "gdk-sys", "glib-sys", "libc", - "system-deps", + "system-deps 6.2.2", "x11", ] @@ -1512,7 +1621,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 6.2.2", "winapi", ] @@ -1560,7 +1669,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" dependencies = [ "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -1577,7 +1686,7 @@ checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ "glib-sys", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -1616,7 +1725,7 @@ dependencies = [ "gobject-sys", "libc", "pango-sys", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -1979,6 +2088,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2005,7 +2123,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -2199,7 +2317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -2228,6 +2346,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libredox" version = "0.1.20" @@ -2237,6 +2365,33 @@ dependencies = [ "libc", ] +[[package]] +name = "libspa" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882f7427e7989dcc9d388b7f05c4630390a1d7696f9ffa469cd4a7a48f0b4c40" +dependencies = [ + "bitflags 2.13.1", + "cc", + "cookie-factory", + "libc", + "libspa-sys", + "nom 8.0.0", + "rustix", + "system-deps 7.0.8", +] + +[[package]] +name = "libspa-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b6e17bdaf63ed0d5e4144022624032b41fd9733112e8c74ac26fc9bf1291924" +dependencies = [ + "bindgen", + "cc", + "system-deps 7.0.8", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2302,6 +2457,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2319,6 +2480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -2380,6 +2542,25 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -2748,7 +2929,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -2856,6 +3037,31 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pipewire" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde71084c4e25959d68f1ea54daa75e5ecdb338e5caf0b5510143b79baa32d5c" +dependencies = [ + "bitflags 2.13.1", + "libc", + "libspa", + "libspa-sys", + "pipewire-sys", + "rustix", +] + +[[package]] +name = "pipewire-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce653f53e63e5b93853218092ee9a8906a5d082c92f3f1db26316955dd63ce0" +dependencies = [ + "bindgen", + "libspa-sys", + "system-deps 7.0.8", +] + [[package]] name = "pkg-config" version = "0.3.34" @@ -3032,6 +3238,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulseaudio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70623bd7967a9ca4c2ae0e807fc380b291f98480fc037042305ec643a4d3373" +dependencies = [ + "bitflags 2.13.1", + "byteorder", + "enum-primitive-derive", + "futures", + "log", + "mio", + "num-traits", + "thiserror 1.0.69", +] + [[package]] name = "quick-xml" version = "0.41.0" @@ -3526,6 +3748,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shlex" version = "2.0.1" @@ -3637,7 +3865,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -3751,13 +3979,26 @@ version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ - "cfg-expr", + "cfg-expr 0.15.8", "heck 0.5.0", "pkg-config", "toml 0.8.2", "version-compare", ] +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr 0.20.9", + "heck 0.5.0", + "pkg-config", + "toml 1.1.4+spec-1.1.0", + "version-compare", +] + [[package]] name = "tao" version = "0.35.3" @@ -3815,6 +4056,12 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tauri" version = "2.11.5" @@ -4520,6 +4767,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "universal-hash" version = "0.6.1" @@ -4781,7 +5034,7 @@ dependencies = [ "libc", "pkg-config", "soup3-sys", - "system-deps", + "system-deps 6.2.2", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4555b87..26f6187 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,7 +22,7 @@ tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -cpal = "0.18.2" +cpal = { version = "0.18.2", features = ["pipewire", "pulseaudio"] } ringbuf = "0.5.1" rubato = "5.0.0" chacha20poly1305 = "0.11.0" From 12fb17c9c3899d215fe229b334d3f4e3ef442dc6 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 28 Aug 2026 14:24:20 -0400 Subject: [PATCH 2/3] Fixed linux audio --- src-tauri/src/commands/audio.rs | 300 +++++++++++++++++++++++++++----- 1 file changed, 254 insertions(+), 46 deletions(-) diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs index 7e6b5cf..1b3a3c5 100644 --- a/src-tauri/src/commands/audio.rs +++ b/src-tauri/src/commands/audio.rs @@ -10,7 +10,10 @@ use std::{ }; use chacha20poly1305::{aead::KeyInit, ChaCha20Poly1305, Key}; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + FromSample, SampleFormat, SizedSample, +}; use ringbuf::{ storage::Heap, traits::{Consumer, Observer, Producer, Split}, @@ -178,24 +181,67 @@ fn build_input_stream( producer: AudioProducer, state_inner: Arc, ) -> Result { + let sample_format = device + .default_input_config() + .map_err(|e| format!("Failed to get input config: {e}"))? + .sample_format(); + + match sample_format { + SampleFormat::F32 => build_input_stream_with::(device, config, producer, state_inner), + SampleFormat::I16 => build_input_stream_with::(device, config, producer, state_inner), + SampleFormat::I32 => build_input_stream_with::(device, config, producer, state_inner), + SampleFormat::I64 => build_input_stream_with::(device, config, producer, state_inner), + SampleFormat::U8 => build_input_stream_with::(device, config, producer, state_inner), + SampleFormat::U16 => build_input_stream_with::(device, config, producer, state_inner), + SampleFormat::U32 => build_input_stream_with::(device, config, producer, state_inner), + SampleFormat::U64 => build_input_stream_with::(device, config, producer, state_inner), + format => Err(format!("Unsupported input sample format: {format}")), + } +} + +fn build_input_stream_with( + device: &cpal::Device, + config: cpal::StreamConfig, + producer: AudioProducer, + state_inner: Arc, +) -> Result +where + T: SizedSample + Send + 'static, + f32: FromSample, +{ let native_sample_rate = config.sample_rate; let channels = config.channels as usize; let mut resampler = LinearResampler::new(); let mut mono_buffer = Vec::with_capacity(2048); let mut resampled_buffer = Vec::with_capacity(2048); + let mut callback_count: u64 = 0; let inner_input_err = Arc::clone(&state_inner); device .build_input_stream( config, - move |data: &[f32], _| { + move |data: &[T], _| { + callback_count += 1; + if callback_count % 200 == 0 { + let peak: f32 = data + .iter() + .map(|&s| s.to_sample::().abs()) + .fold(0.0f32, f32::max); + eprintln!( + "[vc] input callback #{callback_count}: {} frames, {} ch, peak {:.4}", + data.len() / channels, + channels, + peak + ); + } + mono_buffer.clear(); resampled_buffer.clear(); - // Downmix channels to mono + // Downmix channels to mono, converting to f32 for chunk in data.chunks_exact(channels) { - let sum: f32 = chunk.iter().sum(); + let sum: f32 = chunk.iter().map(|&s| s.to_sample::()).sum(); mono_buffer.push(sum / channels as f32); } @@ -213,13 +259,21 @@ fn build_input_stream( }, move |err| { eprintln!("[vc] Input error: {err}. Attempting recovery..."); - if let Ok(mut lock) = inner_input_err.session.lock() { + let rec = Arc::clone(&inner_input_err); + thread::spawn(move || { + // Give the errored stream a moment to unwind before rebuilding. + thread::sleep(Duration::from_millis(10)); + let Ok(mut lock) = rec.session.lock() else { + return; + }; if let Some(session) = lock.as_mut() { + if session.shutdown.load(Ordering::SeqCst) { + return; + } let target_device = session.current_input_device.clone(); - let _ = session - .update_input_device(target_device, Arc::clone(&inner_input_err)); + let _ = session.update_input_device(target_device, Arc::clone(&rec)); } - } + }); }, None, ) @@ -233,25 +287,64 @@ fn build_output_stream( consumer: AudioConsumer, state_inner: Arc, ) -> Result { + let sample_format = device + .default_output_config() + .map_err(|e| format!("Failed to get output config: {e}"))? + .sample_format(); + + match sample_format { + SampleFormat::F32 => build_output_stream_with::(device, config, consumer, state_inner), + SampleFormat::I16 => build_output_stream_with::(device, config, consumer, state_inner), + SampleFormat::I32 => build_output_stream_with::(device, config, consumer, state_inner), + SampleFormat::I64 => build_output_stream_with::(device, config, consumer, state_inner), + SampleFormat::U8 => build_output_stream_with::(device, config, consumer, state_inner), + SampleFormat::U16 => build_output_stream_with::(device, config, consumer, state_inner), + SampleFormat::U32 => build_output_stream_with::(device, config, consumer, state_inner), + SampleFormat::U64 => build_output_stream_with::(device, config, consumer, state_inner), + format => Err(format!("Unsupported output sample format: {format}")), + } +} + +fn build_output_stream_with( + device: &cpal::Device, + config: cpal::StreamConfig, + consumer: AudioConsumer, + state_inner: Arc, +) -> Result +where + T: SizedSample + Send + 'static, + T: FromSample, +{ let native_sample_rate = config.sample_rate; let channels = config.channels as usize; let mut resampler = LinearResampler::new(); let mut raw_mono_samples = Vec::with_capacity(2048); let mut resampled_mono = Vec::with_capacity(2048); let mut last_sample = 0.0f32; + let mut callback_count: u64 = 0; let inner_output_err = Arc::clone(&state_inner); device .build_output_stream( config, - move |data: &mut [f32], _| { + move |data: &mut [T], _| { + callback_count += 1; + if callback_count % 200 == 0 { + eprintln!( + "[vc] output callback #{callback_count}: {} frames, {} ch", + data.len() / channels, + channels + ); + } + let required_mono_samples = (data.len() / channels) * TARGET_SAMPLE_RATE as usize / native_sample_rate as usize; raw_mono_samples.clear(); resampled_mono.clear(); + let mut underflow_count = 0usize; if let Ok(mut cons) = consumer.lock() { for _ in 0..required_mono_samples { if let Some(s) = cons.try_pop() { @@ -261,9 +354,18 @@ fn build_output_stream( // Exponential decay to prevent clicking when underflowing last_sample *= 0.92; raw_mono_samples.push(last_sample); + underflow_count += 1; } } } + if callback_count % 200 == 0 { + eprintln!( + "[vc] output: got {}/{} mono samples ({} underflow)", + required_mono_samples - underflow_count, + required_mono_samples, + underflow_count + ); + } // Resample from 48kHz mono to target native output rate resampler.process( @@ -280,7 +382,7 @@ fn build_output_stream( let mono_val = resampled_mono[res_idx]; for ch in 0..channels { if out_idx + ch < data.len() { - data[out_idx + ch] = mono_val; + data[out_idx + ch] = T::from_sample(mono_val); } } out_idx += channels; @@ -289,13 +391,20 @@ fn build_output_stream( }, move |err| { eprintln!("[vc] Output error: {err}. Attempting recovery..."); - if let Ok(mut lock) = inner_output_err.session.lock() { + let rec = Arc::clone(&inner_output_err); + thread::spawn(move || { + thread::sleep(Duration::from_millis(10)); + let Ok(mut lock) = rec.session.lock() else { + return; + }; if let Some(session) = lock.as_mut() { + if session.shutdown.load(Ordering::SeqCst) { + return; + } let target_device = session.current_output_device.clone(); - let _ = session - .update_output_device(target_device, Arc::clone(&inner_output_err)); + let _ = session.update_output_device(target_device, Arc::clone(&rec)); } - } + }); }, None, ) @@ -324,18 +433,22 @@ pub fn list_output_devices() -> Result, String> { #[tauri::command] pub fn disconnect_from_vc(voice_state: State<'_, VoiceState>) -> Result<(), String> { - let mut lock = voice_state - .inner - .session - .lock() - .map_err(|e| e.to_string())?; + let session = { + let mut lock = voice_state + .inner + .session + .lock() + .map_err(|e| e.to_string())?; + lock.take() + }; - if let Some(session) = lock.take() { + if let Some(session) = session { session.shutdown.store(true, Ordering::SeqCst); let _ = session.input_stream.pause(); if let Ok(output) = session.output_stream.lock() { let _ = output.pause(); } + eprintln!("[vc] disconnected and paused streams"); } Ok(()) @@ -349,59 +462,102 @@ pub fn connect_to_vc( config_state: State<'_, ConfigState>, voice_state: State<'_, VoiceState>, ) -> Result<(), String> { - disconnect_from_vc(voice_state.clone())?; + eprintln!("[vc] connect_to_vc called hostname={hostname} pin={pin}"); - let key = Key::try_from(shared_secret.as_slice()).map_err(|v| v.to_string())?; + if let Err(e) = disconnect_from_vc(voice_state.clone()) { + eprintln!("[vc] disconnect_from_vc failed: {e}"); + } + + let key = match Key::try_from(shared_secret.as_slice()) { + Ok(k) => k, + Err(v) => { + let msg = v.to_string(); + eprintln!("[vc] invalid key length: {msg}"); + return Err(msg); + } + }; let cipher = Arc::new(Mutex::new(SessionCipher::new(ChaCha20Poly1305::new(&key)))); let config = config_state.0.lock().unwrap().clone(); let state_inner = Arc::clone(&voice_state.inner); - let socket = Arc::new( - UdpSocket::bind("0.0.0.0:0").map_err(|e| format!("Failed to bind UDP socket: {e}"))?, - ); - socket - .connect(&hostname) - .map_err(|e| format!("Failed to connect UDP socket: {e}"))?; - socket - .set_read_timeout(Some(Duration::from_millis(5))) - .map_err(|e| e.to_string())?; - socket - .send(&pin.to_be_bytes()) - .map_err(|e| format!("Failed to send pin: {e}"))?; + let socket = match UdpSocket::bind("0.0.0.0:0") { + Ok(s) => s, + Err(e) => { + let msg = format!("Failed to bind UDP socket: {e}"); + eprintln!("[vc] {msg}"); + return Err(msg); + } + }; + let socket = Arc::new(socket); + if let Err(e) = socket.connect(&hostname) { + let msg = format!("Failed to connect UDP socket: {e}"); + eprintln!("[vc] {msg}"); + return Err(msg); + } + if let Err(e) = socket.set_read_timeout(Some(Duration::from_millis(5))) { + let msg = e.to_string(); + eprintln!("[vc] set_read_timeout failed: {msg}"); + return Err(msg); + } + if let Err(e) = socket.send(&pin.to_be_bytes()) { + let msg = format!("Failed to send pin: {e}"); + eprintln!("[vc] {msg}"); + return Err(msg); + } let host = cpal::default_host(); let input_device = match &config.input_device_name { Some(name) => host .input_devices() - .map_err(|e| e.to_string())? + .map_err(|e| { + let msg = e.to_string(); + eprintln!("[vc] failed to enumerate input devices: {msg}"); + msg + })? .find(|d| { d.description() .ok() .map(|x| x.name() == *name) .unwrap_or(false) }) - .ok_or("Input device not found")?, - None => host - .default_input_device() - .ok_or("No default input device")?, + .ok_or_else(|| { + let msg = format!("Input device not found: {name}"); + eprintln!("[vc] {msg}"); + msg + })?, + None => host.default_input_device().ok_or_else(|| { + let msg = "No default input device".to_string(); + eprintln!("[vc] {msg}"); + msg + })?, }; let output_device = match &config.output_device_name { Some(name) => host .output_devices() - .map_err(|e| e.to_string())? + .map_err(|e| { + let msg = e.to_string(); + eprintln!("[vc] failed to enumerate output devices: {msg}"); + msg + })? .find(|d| { d.description() .ok() .map(|x| x.name() == *name) .unwrap_or(false) }) - .ok_or("Output device not found")?, - None => host - .default_output_device() - .ok_or("No default output device")?, + .ok_or_else(|| { + let msg = format!("Output device not found: {name}"); + eprintln!("[vc] {msg}"); + msg + })?, + None => host.default_output_device().ok_or_else(|| { + let msg = "No default output device".to_string(); + eprintln!("[vc] {msg}"); + msg + })?, }; let shutdown = Arc::new(AtomicBool::new(false)); @@ -417,6 +573,12 @@ pub fn connect_to_vc( .map_err(|e| format!("Failed to get default output config: {e}"))? .config(); + eprintln!( + "[vc] Output device {:?} config: {} Hz, {} ch", + output_device.description().map(|d| d.name().to_string()), + output_config.sample_rate, + output_config.channels + ); let output_stream = build_output_stream( &output_device, output_config, @@ -437,6 +599,12 @@ pub fn connect_to_vc( .map_err(|e| format!("Failed to get default input config: {e}"))? .config(); + eprintln!( + "[vc] Input device {:?} config: {} Hz, {} ch", + input_device.description().map(|d| d.name().to_string()), + input_config.sample_rate, + input_config.channels + ); let input_stream = build_input_stream( &input_device, input_config, @@ -454,6 +622,9 @@ pub fn connect_to_vc( let mut sequence = 0u32; let mut frame_buf = vec![0.0f32; PACKET_SAMPLES]; let mut hangover_counter = 0; + let mut loop_count: u64 = 0; + // Adaptive VAD: track a slow-moving noise floor so quiet mics still trigger. + let mut noise_floor: f32 = 0.0; while !shutdown.load(Ordering::Relaxed) { if consumer_in.occupied_len() >= PACKET_SAMPLES { @@ -462,7 +633,17 @@ pub fn connect_to_vc( let sum_squares: f32 = frame_buf.iter().map(|&s| s * s).sum(); let rms = (sum_squares / PACKET_SAMPLES as f32).sqrt(); - let is_speaking = if rms >= VAD_THRESHOLD { + // Update noise floor (attack fast, release slow). + if noise_floor == 0.0 { + noise_floor = rms; + } else if rms < noise_floor { + noise_floor = noise_floor * 0.9 + rms * 0.1; + } else { + noise_floor = noise_floor * 0.999; + } + let threshold = (noise_floor * 4.0).max(VAD_THRESHOLD); + + let is_speaking = if rms >= threshold { hangover_counter = VAD_HANGOVER_FRAMES; true } else if hangover_counter > 0 { @@ -472,6 +653,14 @@ pub fn connect_to_vc( false }; + loop_count += 1; + if loop_count % 200 == 0 { + eprintln!( + "[vc] sender: buffered {} samples, frame rms {rms:.4}, floor {noise_floor:.4}, thr {threshold:.4}, speaking {is_speaking}, seq {sequence}", + consumer_in.occupied_len() + ); + } + if is_speaking { // sequence goes INSIDE the plaintext now, prefixed before the PCM let mut plaintext = Vec::with_capacity(4 + PACKET_SAMPLES * 2); @@ -488,7 +677,15 @@ pub fn connect_to_vc( continue; }; - let _ = input_socket.send(&net_packet); + let sent = input_socket.send(&net_packet); + if let Err(e) = sent { + eprintln!("[vc] sender: send failed: {e}"); + } else if loop_count % 200 == 0 { + eprintln!( + "[vc] sender: sent packet seq {sequence} ({} bytes)", + net_packet.len() + ); + } sequence = sequence.wrapping_add(1); } } else { @@ -511,9 +708,13 @@ pub fn connect_to_vc( let mut last_good_frame = vec![0.0f32; PACKET_SAMPLES]; let mut udp_buffer = [0u8; MAX_PACKET_SIZE]; let mut next_frame_time = Instant::now(); + let mut recv_count: u64 = 0; + let mut loop_count: u64 = 0; while !shutdown.load(Ordering::Relaxed) { + loop_count += 1; if let Ok(len) = socket.recv(&mut udp_buffer) { + recv_count += 1; match cipher.lock().unwrap().decrypt(&udp_buffer[..len]) { Ok(plaintext) => { if plaintext.len() < 4 { @@ -536,6 +737,13 @@ pub fn connect_to_vc( } } + if loop_count % 2000 == 0 { + eprintln!( + "[vc] receiver: received {recv_count} packets total, {} buffered, prebuffering {is_prebuffering}, expected {expected:?}", + packets.len() + ); + } + if is_prebuffering { if packets.len() >= INITIAL_PACKET_CUSHION { expected = packets.keys().next().copied(); From acf76cd82fb4cb93845e67780d32feb0131d69af Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 28 Aug 2026 14:27:35 -0400 Subject: [PATCH 3/3] Fixed crypto --- src-tauri/src/crypto.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/crypto.rs b/src-tauri/src/crypto.rs index e9e6509..1c19bdc 100644 --- a/src-tauri/src/crypto.rs +++ b/src-tauri/src/crypto.rs @@ -3,7 +3,6 @@ use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, Nonce}; pub struct SessionCipher { cipher: ChaCha20Poly1305, send_counter: u64, - recv_counter: u64, } impl SessionCipher { @@ -11,11 +10,10 @@ impl SessionCipher { Self { cipher, send_counter: 0, - recv_counter: 0, } } - pub fn next_send_nonce(&mut self) -> [u8; 12] { + fn next_send_nonce(&mut self) -> [u8; 12] { let mut nonce = [0u8; 12]; nonce[..8].copy_from_slice(&self.send_counter.to_be_bytes()); // top bit distinguishes "send" direction from "recv" direction, @@ -26,13 +24,6 @@ impl SessionCipher { nonce } - pub fn next_recv_nonce(&mut self) -> [u8; 12] { - let mut nonce = [0u8; 12]; - nonce[..8].copy_from_slice(&self.recv_counter.to_be_bytes()); - self.recv_counter += 1; - nonce - } - pub fn encrypt(&mut self, plaintext: &[u8]) -> Result, String> { let nonce_bytes = self.next_send_nonce(); let nonce = Nonce::try_from(nonce_bytes).map_err(|v| v.to_string())?; @@ -48,11 +39,15 @@ impl SessionCipher { Ok(out) } - pub fn decrypt(&mut self, data: &[u8]) -> Result, String> { + pub fn decrypt(&self, data: &[u8]) -> Result, String> { if data.len() < 12 { return Err("message too short to contain a nonce".to_string()); } let (nonce_bytes, ciphertext) = data.split_at(12); + + // Use the nonce that travelled with this packet rather than a locally + // tracked counter. This makes decryption immune to UDP packet loss or + // reordering, because we never desync from the sender's counter. let nonce = Nonce::try_from(nonce_bytes).map_err(|v| v.to_string())?; self.cipher