Merge pull request #8 from recurse-chat/fix-linux-audio-and-crypto

BUG: Fix linux audio and crypto
This commit is contained in:
2026-08-28 14:28:02 -04:00
committed by GitHub
4 changed files with 532 additions and 76 deletions
+271 -18
View File
@@ -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]]
+1 -1
View File
@@ -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"
+254 -46
View File
@@ -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<VoiceStateInner>,
) -> Result<cpal::Stream, String> {
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::<f32>(device, config, producer, state_inner),
SampleFormat::I16 => build_input_stream_with::<i16>(device, config, producer, state_inner),
SampleFormat::I32 => build_input_stream_with::<i32>(device, config, producer, state_inner),
SampleFormat::I64 => build_input_stream_with::<i64>(device, config, producer, state_inner),
SampleFormat::U8 => build_input_stream_with::<u8>(device, config, producer, state_inner),
SampleFormat::U16 => build_input_stream_with::<u16>(device, config, producer, state_inner),
SampleFormat::U32 => build_input_stream_with::<u32>(device, config, producer, state_inner),
SampleFormat::U64 => build_input_stream_with::<u64>(device, config, producer, state_inner),
format => Err(format!("Unsupported input sample format: {format}")),
}
}
fn build_input_stream_with<T>(
device: &cpal::Device,
config: cpal::StreamConfig,
producer: AudioProducer,
state_inner: Arc<VoiceStateInner>,
) -> Result<cpal::Stream, String>
where
T: SizedSample + Send + 'static,
f32: FromSample<T>,
{
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::<f32>().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::<f32>()).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<VoiceStateInner>,
) -> Result<cpal::Stream, String> {
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::<f32>(device, config, consumer, state_inner),
SampleFormat::I16 => build_output_stream_with::<i16>(device, config, consumer, state_inner),
SampleFormat::I32 => build_output_stream_with::<i32>(device, config, consumer, state_inner),
SampleFormat::I64 => build_output_stream_with::<i64>(device, config, consumer, state_inner),
SampleFormat::U8 => build_output_stream_with::<u8>(device, config, consumer, state_inner),
SampleFormat::U16 => build_output_stream_with::<u16>(device, config, consumer, state_inner),
SampleFormat::U32 => build_output_stream_with::<u32>(device, config, consumer, state_inner),
SampleFormat::U64 => build_output_stream_with::<u64>(device, config, consumer, state_inner),
format => Err(format!("Unsupported output sample format: {format}")),
}
}
fn build_output_stream_with<T>(
device: &cpal::Device,
config: cpal::StreamConfig,
consumer: AudioConsumer,
state_inner: Arc<VoiceStateInner>,
) -> Result<cpal::Stream, String>
where
T: SizedSample + Send + 'static,
T: FromSample<f32>,
{
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<Vec<String>, 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();
+6 -11
View File
@@ -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<Vec<u8>, 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<Vec<u8>, String> {
pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, 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