Compare commits

...

19 Commits

Author SHA1 Message Date
David
f6acc181a6 use sample windows with overlap for fft, fixes beep_where_voice_detected 2025-11-21 23:55:30 +01:00
David
c78baaf71f improves voice detector 2025-11-15 01:06:24 +01:00
David
f602047b3c New voice detector, made test more fuzzy
Good news tests work now
2025-11-14 21:34:36 +01:00
David Kleingeld
d372473e1e wip 2025-10-17 23:25:24 +02:00
David Kleingeld
4cef8743b1 Changes samplerate to 44100 + denoiser now handles resampling
We did some testing and found out that our voices sound quite bad if you
cut off frequencies above 8k. We might want to retrain the denoiser so
it can deal with 44100 as that would save 2 resample passes which are
quite expensive.

Co-authored-by: Nia <nia@zed.dev>
2025-10-16 19:16:10 +02:00
David Kleingeld
5e6dd2b5d4 Replace broken test files with new ones :)
Co-authored-by: Nia <nia@zed.dev>
2025-10-13 21:48:55 +02:00
David Kleingeld
b01378cbb4 update rodio 2025-10-13 21:22:13 +02:00
David Kleingeld
f4c397a18c Re-add missing test files + fix resampling latency
Co-authored-by: Nia <nia@zed.dev>
2025-10-13 18:58:46 +02:00
David Kleingeld
c4db4c5e59 add resampler tests fix resampler skipping samples
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-10-10 21:54:10 +02:00
David Kleingeld
b9832bca1e fix audio test not 'testing' basic voice detector. Add tests for test 2025-10-09 15:12:54 +02:00
David Kleingeld
0d17281cad use pre-converted files to include resampler and channelconvertor in test 2025-10-09 14:39:37 +02:00
David Kleingeld
2627309c27 fmt 2025-10-08 22:07:16 +02:00
David Kleingeld
11251105eb make end to end test check harmonic ratios and fundamental freqs 2025-10-08 22:05:10 +02:00
David Kleingeld
04f4c780b1 fix denoiser hanging if next called after it returned None 2025-10-08 22:04:34 +02:00
David Kleingeld
ef9c9a718f adds end to end test for audio in audio crate 2025-10-02 18:08:34 +02:00
David Kleingeld
b3c20c2d5c fix tests 2025-09-29 12:53:29 +02:00
David Kleingeld
e84660c0ad workspace hack 2025-09-29 12:46:32 +02:00
David Kleingeld
dfc70b3020 fix resampler not resetting next + refactor 2025-09-29 12:45:51 +02:00
David Kleingeld
dc319c1895 adds hifi resampler 2025-09-29 11:42:10 +02:00
23 changed files with 1869 additions and 471 deletions

230
Cargo.lock generated
View File

@@ -798,6 +798,48 @@ dependencies = [
"zbus",
]
[[package]]
name = "askama"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
dependencies = [
"askama_derive",
"itoa",
"percent-encoding",
"serde",
"serde_json",
]
[[package]]
name = "askama_derive"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
dependencies = [
"askama_parser",
"basic-toml",
"memchr",
"proc-macro2",
"quote",
"rustc-hash 2.1.1",
"serde",
"serde_derive",
"syn 2.0.101",
]
[[package]]
name = "askama_parser"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
dependencies = [
"memchr",
"serde",
"serde_derive",
"winnow",
]
[[package]]
name = "askpass"
version = "0.1.0"
@@ -1407,13 +1449,18 @@ dependencies = [
"crossbeam",
"denoise",
"gpui",
"itertools 0.14.0",
"libwebrtc",
"log",
"parking_lot",
"plotly",
"rand 0.9.1",
"rodio",
"rubato",
"serde",
"settings",
"smol",
"spectrum-analyzer",
"thiserror 2.0.12",
"util",
"workspace-hack",
@@ -2136,6 +2183,15 @@ version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
[[package]]
name = "basic-toml"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
dependencies = [
"serde",
]
[[package]]
name = "bedrock"
version = "0.1.0"
@@ -2728,7 +2784,7 @@ dependencies = [
"cap-primitives",
"cap-std",
"io-lifetimes",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2757,7 +2813,7 @@ dependencies = [
"maybe-owned",
"rustix 1.0.7",
"rustix-linux-procfs",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
"winx",
]
@@ -4489,8 +4545,18 @@ version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.20.11",
"darling_macro 0.20.11",
]
[[package]]
name = "darling"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core 0.21.3",
"darling_macro 0.21.3",
]
[[package]]
@@ -4507,13 +4573,38 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "darling_core"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.101",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"darling_core 0.20.11",
"quote",
"syn 2.0.101",
]
[[package]]
name = "darling_macro"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core 0.21.3",
"quote",
"syn 2.0.101",
]
@@ -5489,7 +5580,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -5856,7 +5947,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix 1.0.7",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -5992,6 +6083,15 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]]
name = "float-cmp"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
dependencies = [
"num-traits",
]
[[package]]
name = "float-ord"
version = "0.3.2"
@@ -6244,7 +6344,7 @@ checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
dependencies = [
"io-lifetimes",
"rustix 1.0.7",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -8147,7 +8247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
dependencies = [
"io-lifetimes",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -8220,7 +8320,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.0",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -8974,9 +9074,9 @@ dependencies = [
[[package]]
name = "libm"
version = "0.2.11"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libmimalloc-sys"
@@ -9684,6 +9784,17 @@ dependencies = [
"paste",
]
[[package]]
name = "microfft"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b6673eb0cc536241d6734c2ca45abfdbf90e9e7791c66a36a7ba3c315b76cf"
dependencies = [
"cfg-if",
"num-complex",
"static_assertions",
]
[[package]]
name = "migrator"
version = "0.1.0"
@@ -11801,6 +11912,40 @@ dependencies = [
"time",
]
[[package]]
name = "plotly"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05411c8f90fef914076e5a1a879ba8dbd40fa088f83924f1c1a6d8c5df5b98fb"
dependencies = [
"askama",
"dyn-clone",
"erased-serde",
"once_cell",
"plotly_derive",
"rand 0.9.1",
"serde",
"serde-wasm-bindgen",
"serde_json",
"serde_repr",
"serde_with",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "plotly_derive"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35f11c16698ff961f06499508442ddc83fd29aa8661fe5558334b7f9676e7c80"
dependencies = [
"darling 0.21.3",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "plotters"
version = "0.3.7"
@@ -12581,7 +12726,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -13422,12 +13567,14 @@ dependencies = [
[[package]]
name = "rodio"
version = "0.21.1"
source = "git+https://github.com/RustAudio/rodio#e2074c6c2acf07b57cf717e076bdda7a9ac6e70b"
source = "git+https://github.com/RustAudio/rodio?rev=be453f2#be453f22dbf6e903a15c23e1fba5ee6431bc53ed"
dependencies = [
"cpal",
"dasp_sample",
"hound",
"num-rational",
"rand 0.9.1",
"rand_distr",
"rtrb",
"symphonia",
"thiserror 2.0.12",
@@ -13510,6 +13657,18 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba"
[[package]]
name = "rubato"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"realfft",
]
[[package]]
name = "rules_library"
version = "0.1.0"
@@ -13664,7 +13823,7 @@ dependencies = [
"errno 0.3.11",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -13677,7 +13836,7 @@ dependencies = [
"errno 0.3.11",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -13799,7 +13958,7 @@ dependencies = [
"security-framework 3.2.0",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -14296,6 +14455,17 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_core"
version = "1.0.221"
@@ -14429,7 +14599,7 @@ version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77"
dependencies = [
"darling",
"darling 0.20.11",
"proc-macro2",
"quote",
"syn 2.0.101",
@@ -14875,6 +15045,18 @@ dependencies = [
"smallvec",
]
[[package]]
name = "spectrum-analyzer"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5353405212be77e7f9e95aa77934f0aafb0f80c586571680b9c20ce5bae758"
dependencies = [
"float-cmp 0.10.0",
"libm",
"microfft",
"paste",
]
[[package]]
name = "spin"
version = "0.9.8"
@@ -15184,7 +15366,7 @@ dependencies = [
"cfg-if",
"libc",
"psm",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -15284,7 +15466,7 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
dependencies = [
"float-cmp",
"float-cmp 0.9.0",
]
[[package]]
@@ -15877,7 +16059,7 @@ dependencies = [
"fd-lock",
"io-lifetimes",
"rustix 0.38.44",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
"winx",
]
@@ -16059,7 +16241,7 @@ dependencies = [
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.7",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -18643,7 +18825,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -19355,7 +19537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [
"bitflags 2.9.0",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]

View File

@@ -378,7 +378,7 @@ remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rodio = { git = "https://github.com/RustAudio/rodio" }
rodio = { git = "https://github.com/RustAudio/rodio", rev = "be453f2" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
@@ -604,6 +604,7 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
rand = "0.9"
rayon = "1.8"
realfft = "3.4.0"
ref-cast = "1.0.24"
regex = "1.5"
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c770a32f1998d6e999cef3e59e0013e6c4415", default-features = false, features = [
@@ -622,6 +623,7 @@ runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-demangle = "0.1.23"
rustc-hash = "2.1.0"
rustfft = { version = "6.2.0", features = ["avx"] }
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }

View File

@@ -22,12 +22,21 @@ denoise = { path = "../denoise" }
log.workspace = true
parking_lot.workspace = true
rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }
rubato = "0.16.2"
serde.workspace = true
settings.workspace = true
smol.workspace = true
thiserror.workspace = true
util.workspace = true
workspace-hack.workspace = true
rand.workspace = true
[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" }
[dev-dependencies]
rodio = { workspace = true, features = [ "wav", "playback", "wav_output", "noise" ] }
gpui = { workspace = true, "features" = ["test-support"] }
spectrum-analyzer = "1.7"
plotly = "0.13"
itertools.workspace = true

View File

@@ -17,7 +17,10 @@ mod non_windows_and_freebsd_deps {
use non_windows_and_freebsd_deps::*;
use rodio::{
Decoder, OutputStream, OutputStreamBuilder, Source, mixer::Mixer, nz, source::Buffered,
Decoder, OutputStream, OutputStreamBuilder, Source,
mixer::Mixer,
nz,
source::{AutomaticGainControlSettings, Buffered},
};
use settings::Settings;
use std::{io::Cursor, num::NonZero, path::PathBuf, sync::atomic::Ordering, time::Duration};
@@ -26,18 +29,18 @@ use util::ResultExt;
mod audio_settings;
mod replays;
mod rodio_ext;
#[cfg(test)]
mod test;
pub use audio_settings::AudioSettings;
pub use rodio_ext::RodioExt;
use crate::audio_settings::LIVE_SETTINGS;
// We are migrating to 16kHz sample rate from 48kHz. In the future
// once we are reasonably sure most users have upgraded we will
// remove the LEGACY parameters.
//
// We migrate to 16kHz because it is sufficient for speech and required
// by the denoiser and future Speech to Text layers.
pub const SAMPLE_RATE: NonZero<u32> = nz!(16000);
// We are migrating to 44100Hz mono from 48kHz stereo. In the future once we are
// reasonably sure most users have upgraded we will remove the LEGACY
// parameters.
// We migrate to 44100 because if its good for cd's its good enough for us
pub const SAMPLE_RATE: NonZero<u32> = nz!(44100);
pub const CHANNEL_COUNT: NonZero<u16> = nz!(1);
pub const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
(SAMPLE_RATE.get() as usize / 100) * CHANNEL_COUNT.get() as usize;
@@ -104,49 +107,60 @@ impl Default for Audio {
}
}
fn automatic_gain_control_settings() -> AutomaticGainControlSettings {
AutomaticGainControlSettings {
target_level: 0.9,
attack_time: Duration::from_secs_f32(1f32),
release_time: Duration::from_secs_f32(0f32),
absolute_max_gain: 5.0,
}
}
impl Global for Audio {}
impl Audio {
fn setup_mixer(&mut self) -> impl Source + use<> {
let (mixer, source) = rodio::mixer::mixer(CHANNEL_COUNT, SAMPLE_RATE);
// or the mixer will end immediately as its empty.
mixer.add(rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE));
// The webrtc apm is not yet compiling for windows & freebsd
#[cfg(not(any(
any(all(target_os = "windows", target_env = "gnu")),
target_os = "freebsd"
)))]
let echo_canceller = Arc::clone(&self.echo_canceller);
#[cfg(not(any(
any(all(target_os = "windows", target_env = "gnu")),
target_os = "freebsd"
)))]
let source = source.inspect_buffer::<BUFFER_SIZE, _>(move |buffer| {
let mut buf: [i16; _] = buffer.map(|s| s.to_sample());
echo_canceller
.lock()
.process_reverse_stream(
&mut buf,
SAMPLE_RATE.get() as i32,
CHANNEL_COUNT.get().into(),
)
.expect("Audio input and output threads should not panic");
});
self.output_mixer = Some(mixer);
source
}
fn ensure_output_exists(&mut self) -> Result<&Mixer> {
#[cfg(debug_assertions)]
log::warn!(
"Audio does not sound correct without optimizations. Use a release build to debug audio issues"
);
if self.output_handle.is_none() {
if self.output_mixer.is_none() {
let mixer_source = self.setup_mixer();
let output_handle = OutputStreamBuilder::open_default_stream()
.context("Could not open default output stream")?;
info!("Output stream: {:?}", output_handle);
output_handle.mixer().add(mixer_source);
self.output_handle = Some(output_handle);
if let Some(output_handle) = &self.output_handle {
let (mixer, source) = rodio::mixer::mixer(CHANNEL_COUNT, SAMPLE_RATE);
// or the mixer will end immediately as its empty.
mixer.add(rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE));
self.output_mixer = Some(mixer);
// The webrtc apm is not yet compiling for windows & freebsd
#[cfg(not(any(
any(all(target_os = "windows", target_env = "gnu")),
target_os = "freebsd"
)))]
let echo_canceller = Arc::clone(&self.echo_canceller);
#[cfg(not(any(
any(all(target_os = "windows", target_env = "gnu")),
target_os = "freebsd"
)))]
let source = source.inspect_buffer::<BUFFER_SIZE, _>(move |buffer| {
let mut buf: [i16; _] = buffer.map(|s| s.to_sample());
echo_canceller
.lock()
.process_reverse_stream(
&mut buf,
SAMPLE_RATE.get() as i32,
CHANNEL_COUNT.get().into(),
)
.expect("Audio input and output threads should not panic");
});
output_handle.mixer().add(source);
}
}
Ok(self
@@ -163,53 +177,44 @@ impl Audio {
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
pub fn open_microphone(voip_parts: VoipParts) -> anyhow::Result<impl Source> {
let stream = rodio::microphone::MicrophoneBuilder::new()
.default_device()?
.default_config()?
.prefer_sample_rates([
SAMPLE_RATE, // sample rates trivially resamplable to `SAMPLE_RATE`
SAMPLE_RATE.saturating_mul(nz!(2)),
SAMPLE_RATE.saturating_mul(nz!(3)),
SAMPLE_RATE.saturating_mul(nz!(4)),
])
.prefer_channel_counts([nz!(1), nz!(2), nz!(3), nz!(4)])
.prefer_buffer_sizes(512..)
.open_stream()?;
info!("Opened microphone: {:?}", stream.config());
let stream = stream
pub fn input_pipeline(
voip_parts: VoipParts,
raw_mic_input: impl Source,
) -> anyhow::Result<impl Source> {
let stream = raw_mic_input
.possibly_disconnected_channels_to_mono()
.constant_samplerate(SAMPLE_RATE)
.limit(LimitSettings::live_performance())
.process_buffer::<BUFFER_SIZE, _>(move |buffer| {
let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
if voip_parts
.echo_canceller
.lock()
.process_stream(
&mut int_buffer,
SAMPLE_RATE.get() as i32,
CHANNEL_COUNT.get() as i32,
)
.context("livekit audio processor error")
.log_err()
.is_some()
{
for (sample, processed) in buffer.iter_mut().zip(&int_buffer) {
*sample = (*processed).to_sample();
}
}
})
.denoise()
.context("Could not set up denoiser")?
.automatic_gain_control(0.90, 1.0, 0.0, 5.0)
.periodic_access(Duration::from_millis(100), move |agc_source| {
agc_source
.set_enabled(LIVE_SETTINGS.auto_microphone_volume.load(Ordering::Relaxed));
let denoise = agc_source.inner_mut();
denoise.set_enabled(LIVE_SETTINGS.denoise.load(Ordering::Relaxed));
});
.limit(LimitSettings::live_performance());
// .process_buffer::<BUFFER_SIZE, _>(move |buffer| {
// let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
// if voip_parts
// .echo_canceller
// .lock()
// .process_stream(
// &mut int_buffer,
// SAMPLE_RATE.get() as i32,
// CHANNEL_COUNT.get() as i32,
// )
// .context("livekit audio processor error")
// .log_err()
// .is_some()
// {
// for (sample, processed) in buffer.iter_mut().zip(&int_buffer) {
// *sample = (*processed).to_sample();
// }
// }
// });
// .denoise()
// .context("Could not set up denoiser")?
// .automatic_gain_control(automatic_gain_control_settings())
// .periodic_access(Duration::from_millis(100), move |agc_source| {
// agc_source
// .set_enabled(LIVE_SETTINGS.auto_microphone_volume.load(Ordering::Relaxed));
// let denoise = agc_source.inner_mut();
// denoise
// .set_enabled(LIVE_SETTINGS.denoise.load(Ordering::Relaxed))
// .unwrap(); // todo make this log?
// });
let stream = if voip_parts.legacy_audio_compatible {
stream.constant_params(LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE)
@@ -217,6 +222,11 @@ impl Audio {
stream.constant_params(CHANNEL_COUNT, SAMPLE_RATE)
};
// the audio processing module gives us half its buffer duration
// of empty samples. We can skip those.
const APM_CHUNK_DURATION: Duration = Duration::from_millis(10);
let stream = stream.skip_duration(APM_CHUNK_DURATION / 2);
let (replay, stream) = stream.replayable(REPLAY_DURATION)?;
voip_parts
.replays
@@ -225,6 +235,22 @@ impl Audio {
Ok(stream)
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
pub fn open_microphone(voip_parts: VoipParts) -> anyhow::Result<impl Source> {
let stream = rodio::microphone::MicrophoneBuilder::new()
.default_device()?
.default_config()?
.prefer_sample_rates([
SAMPLE_RATE, // sample rates trivially resamplable to `SAMPLE_RATE`
SAMPLE_RATE.saturating_mul(nz!(2)),
])
.prefer_channel_counts([nz!(1), nz!(2), nz!(3), nz!(4)])
.prefer_buffer_sizes(512..)
.open_stream()?;
info!("Opened microphone: {:?}", stream.config());
Self::input_pipeline(voip_parts, stream)
}
pub fn play_voip_stream(
source: impl rodio::Source + Send + 'static,
speaker_name: String,
@@ -233,7 +259,7 @@ impl Audio {
) -> anyhow::Result<()> {
let (replay_source, source) = source
.constant_params(CHANNEL_COUNT, SAMPLE_RATE)
.automatic_gain_control(0.90, 1.0, 0.0, 5.0)
.automatic_gain_control(automatic_gain_control_settings())
.periodic_access(Duration::from_millis(100), move |agc_source| {
agc_source.set_enabled(LIVE_SETTINGS.auto_speaker_volume.load(Ordering::Relaxed));
})

View File

@@ -1,25 +1,20 @@
use std::{
num::NonZero,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use std::{num::NonZero, time::Duration};
use crossbeam::queue::ArrayQueue;
use denoise::{Denoiser, DenoiserError};
use denoise::DenoiserError;
use log::warn;
use rodio::{
ChannelCount, Sample, SampleRate, Source, conversions::SampleRateConverter, nz,
source::UniformSourceIterator,
ChannelCount, Sample, SampleRate, Source, buffer::SamplesBuffer,
conversions::ChannelCountConverter, nz,
};
const MAX_CHANNELS: usize = 8;
use crate::rodio_ext::resample::FixedResampler;
pub use replayable::{Replay, ReplayDurationTooShort, Replayable};
#[derive(Debug, thiserror::Error)]
#[error("Replay duration is too short must be >= 100ms")]
pub struct ReplayDurationTooShort;
mod replayable;
mod resample;
mod resampling_denoise;
const MAX_CHANNELS: usize = 8;
// These all require constant sources (so the span is infinitely long)
// this is not guaranteed by rodio however we know it to be true in all our
@@ -36,14 +31,15 @@ pub trait RodioExt: Source + Sized {
duration: Duration,
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort>;
fn take_samples(self, n: usize) -> TakeSamples<Self>;
fn denoise(self) -> Result<Denoiser<Self>, DenoiserError>;
fn denoise(self) -> Result<resampling_denoise::ResamplingDenoiser<Self>, DenoiserError>;
fn constant_params(
self,
channel_count: ChannelCount,
sample_rate: SampleRate,
) -> UniformSourceIterator<Self>;
fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate<Self>;
) -> ConstantChannelCount<FixedResampler<Self>>;
fn constant_samplerate(self, sample_rate: SampleRate) -> FixedResampler<Self>;
fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self>;
fn into_samples_buffer(self) -> SamplesBuffer;
}
impl<S: Source> RodioExt for S {
@@ -81,38 +77,7 @@ impl<S: Source> RodioExt for S {
self,
duration: Duration,
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort> {
if duration < Duration::from_millis(100) {
return Err(ReplayDurationTooShort);
}
let samples_per_second = self.sample_rate().get() as usize * self.channels().get() as usize;
let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
let samples_to_queue =
(samples_to_queue as usize).next_multiple_of(self.channels().get().into());
let chunk_size =
(samples_per_second.div_ceil(10)).next_multiple_of(self.channels().get() as usize);
let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
let is_active = Arc::new(AtomicBool::new(true));
let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
Ok((
Replay {
rx: Arc::clone(&queue),
buffer: Vec::new().into_iter(),
sleep_duration: duration / 2,
sample_rate: self.sample_rate(),
channel_count: self.channels(),
source_is_active: is_active.clone(),
},
Replayable {
tx: queue,
inner: self,
buffer: Vec::with_capacity(chunk_size),
chunk_size,
is_active,
},
))
replayable::replayable(self, duration)
}
fn take_samples(self, n: usize) -> TakeSamples<S> {
TakeSamples {
@@ -120,45 +85,48 @@ impl<S: Source> RodioExt for S {
left_to_take: n,
}
}
fn denoise(self) -> Result<Denoiser<Self>, DenoiserError> {
let res = Denoiser::try_new(self);
res
fn denoise(self) -> Result<resampling_denoise::ResamplingDenoiser<Self>, DenoiserError> {
resampling_denoise::ResamplingDenoiser::new(self)
}
fn constant_params(
self,
channel_count: ChannelCount,
sample_rate: SampleRate,
) -> UniformSourceIterator<Self> {
UniformSourceIterator::new(self, channel_count, sample_rate)
) -> ConstantChannelCount<FixedResampler<Self>> {
ConstantChannelCount::new(self.constant_samplerate(sample_rate), channel_count)
}
fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate<Self> {
ConstantSampleRate::new(self, sample_rate)
fn constant_samplerate(self, sample_rate: SampleRate) -> FixedResampler<Self> {
FixedResampler::new(self, sample_rate)
}
fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self> {
ToMono::new(self)
}
fn into_samples_buffer(mut self) -> SamplesBuffer {
let samples: Vec<_> = self.by_ref().collect();
SamplesBuffer::new(self.channels(), self.sample_rate(), samples)
}
}
pub struct ConstantSampleRate<S: Source> {
inner: SampleRateConverter<S>,
pub struct ConstantChannelCount<S: Source> {
inner: ChannelCountConverter<S>,
channels: ChannelCount,
sample_rate: SampleRate,
}
impl<S: Source> ConstantSampleRate<S> {
fn new(source: S, target_rate: SampleRate) -> Self {
let input_sample_rate = source.sample_rate();
let channels = source.channels();
let inner = SampleRateConverter::new(source, input_sample_rate, target_rate, channels);
impl<S: Source> ConstantChannelCount<S> {
pub fn new(source: S, target_channels: ChannelCount) -> Self {
let input_channels = source.channels();
let sample_rate = source.sample_rate();
let inner = ChannelCountConverter::new(source, input_channels, target_channels);
Self {
sample_rate,
inner,
channels,
sample_rate: target_rate,
channels: target_channels,
}
}
}
impl<S: Source> Iterator for ConstantSampleRate<S> {
impl<S: Source> Iterator for ConstantChannelCount<S> {
type Item = rodio::Sample;
fn next(&mut self) -> Option<Self::Item> {
@@ -170,7 +138,7 @@ impl<S: Source> Iterator for ConstantSampleRate<S> {
}
}
impl<S: Source> Source for ConstantSampleRate<S> {
impl<S: Source> Source for ConstantChannelCount<S> {
fn current_span_len(&self) -> Option<usize> {
None
}
@@ -268,6 +236,15 @@ pub struct TakeSamples<S> {
left_to_take: usize,
}
impl<S: Clone> Clone for TakeSamples<S> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
left_to_take: self.left_to_take,
}
}
}
impl<S: Source> Iterator for TakeSamples<S> {
type Item = Sample;
@@ -307,53 +284,6 @@ impl<S: Source> Source for TakeSamples<S> {
}
}
/// constant source, only works on a single span
#[derive(Debug)]
struct ReplayQueue {
inner: ArrayQueue<Vec<Sample>>,
normal_chunk_len: usize,
/// The last chunk in the queue may be smaller than
/// the normal chunk size. This is always equal to the
/// size of the last element in the queue.
/// (so normally chunk_size)
last_chunk: Mutex<Vec<Sample>>,
}
impl ReplayQueue {
fn new(queue_len: usize, chunk_size: usize) -> Self {
Self {
inner: ArrayQueue::new(queue_len),
normal_chunk_len: chunk_size,
last_chunk: Mutex::new(Vec::new()),
}
}
/// Returns the length in samples
fn len(&self) -> usize {
self.inner.len().saturating_sub(1) * self.normal_chunk_len
+ self
.last_chunk
.lock()
.expect("Self::push_last can not poison this lock")
.len()
}
fn pop(&self) -> Option<Vec<Sample>> {
self.inner.pop() // removes element that was inserted first
}
fn push_last(&self, mut samples: Vec<Sample>) {
let mut last_chunk = self
.last_chunk
.lock()
.expect("Self::len can not poison this lock");
std::mem::swap(&mut *last_chunk, &mut samples);
}
fn push_normal(&self, samples: Vec<Sample>) {
let _pushed_out_of_ringbuf = self.inner.force_push(samples);
}
}
/// constant source, only works on a single span
pub struct ProcessBuffer<const N: usize, S, F>
where
@@ -487,147 +417,15 @@ where
}
}
/// constant source, only works on a single span
#[derive(Debug)]
pub struct Replayable<S: Source> {
inner: S,
buffer: Vec<Sample>,
chunk_size: usize,
tx: Arc<ReplayQueue>,
is_active: Arc<AtomicBool>,
}
impl<S: Source> Iterator for Replayable<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.inner.next() {
self.buffer.push(sample);
// If the buffer is full send it
if self.buffer.len() == self.chunk_size {
self.tx.push_normal(std::mem::take(&mut self.buffer));
}
Some(sample)
} else {
let last_chunk = std::mem::take(&mut self.buffer);
self.tx.push_last(last_chunk);
self.is_active.store(false, Ordering::Relaxed);
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<S: Source> Source for Replayable<S> {
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
fn channels(&self) -> ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}
/// constant source, only works on a single span
#[derive(Debug)]
pub struct Replay {
rx: Arc<ReplayQueue>,
buffer: std::vec::IntoIter<Sample>,
sleep_duration: Duration,
sample_rate: SampleRate,
channel_count: ChannelCount,
source_is_active: Arc<AtomicBool>,
}
impl Replay {
pub fn source_is_active(&self) -> bool {
// - source could return None and not drop
// - source could be dropped before returning None
self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
}
/// Duration of what is in the buffer and can be returned without blocking.
pub fn duration_ready(&self) -> Duration {
let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
Duration::from_secs_f64(seconds_queued)
}
/// Number of samples in the buffer and can be returned without blocking.
pub fn samples_ready(&self) -> usize {
self.rx.len() + self.buffer.len()
}
}
impl Iterator for Replay {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.buffer.next() {
return Some(sample);
}
loop {
if let Some(new_buffer) = self.rx.pop() {
self.buffer = new_buffer.into_iter();
return self.buffer.next();
}
if !self.source_is_active() {
return None;
}
// The queue does not support blocking on a next item. We want this queue as it
// is quite fast and provides a fixed size. We know how many samples are in a
// buffer so if we do not get one now we must be getting one after `sleep_duration`.
std::thread::sleep(self.sleep_duration);
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
((self.rx.len() + self.buffer.len()), None)
}
}
impl Source for Replay {
fn current_span_len(&self) -> Option<usize> {
None // source is not compatible with spans
}
fn channels(&self) -> ChannelCount {
self.channel_count
}
fn sample_rate(&self) -> SampleRate {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
#[cfg(test)]
mod tests {
use rodio::{nz, static_buffer::StaticSamplesBuffer};
use super::*;
const SAMPLES: [Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
pub const SAMPLES: [Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
fn test_source() -> StaticSamplesBuffer {
pub fn test_source() -> StaticSamplesBuffer {
StaticSamplesBuffer::new(nz!(1), nz!(1), &SAMPLES)
}
@@ -690,74 +488,4 @@ mod tests {
assert_eq!(yielded, SAMPLES.len())
}
}
mod instant_replay {
use super::*;
#[test]
fn continues_after_history() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(3))
.expect("longer than 100ms");
source.by_ref().take(3).count();
let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
assert_eq!(&yielded, &SAMPLES[0..3],);
source.count();
let yielded: Vec<Sample> = replay.collect();
assert_eq!(&yielded, &SAMPLES[3..5],);
}
#[test]
fn keeps_only_latest() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
source.by_ref().take(5).count(); // get all items but do not end the source
let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
assert_eq!(&yielded, &SAMPLES[3..5]);
source.count(); // exhaust source
assert_eq!(replay.next(), None);
}
#[test]
fn keeps_correct_amount_of_seconds() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
// exhaust but do not yet end source
source.by_ref().take(40_000).count();
// take all samples we can without blocking
let ready = replay.samples_ready();
let n_yielded = replay.take_samples(ready).count();
let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
let margin = 16_000 / 10; // 100ms
assert!(n_yielded as u32 >= max - margin);
}
#[test]
fn samples_ready() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (mut replay, source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
assert_eq!(replay.by_ref().samples_ready(), 0);
source.take(8000).count(); // half a second
let margin = 16_000 / 10; // 100ms
let ready = replay.samples_ready();
assert!(ready >= 8000 - margin);
}
}
}

View File

@@ -0,0 +1,308 @@
use std::{
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use crossbeam::queue::ArrayQueue;
use rodio::{ChannelCount, Sample, SampleRate, Source};
#[derive(Debug, thiserror::Error)]
#[error("Replay duration is too short must be >= 100ms")]
pub struct ReplayDurationTooShort;
pub fn replayable<S: Source>(
source: S,
duration: Duration,
) -> Result<(Replay, Replayable<S>), ReplayDurationTooShort> {
if duration < Duration::from_millis(100) {
return Err(ReplayDurationTooShort);
}
let samples_per_second = source.sample_rate().get() as usize * source.channels().get() as usize;
let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
let samples_to_queue =
(samples_to_queue as usize).next_multiple_of(source.channels().get().into());
let chunk_size =
(samples_per_second.div_ceil(10)).next_multiple_of(source.channels().get() as usize);
let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
let is_active = Arc::new(AtomicBool::new(true));
let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
Ok((
Replay {
rx: Arc::clone(&queue),
buffer: Vec::new().into_iter(),
sleep_duration: duration / 2,
sample_rate: source.sample_rate(),
channel_count: source.channels(),
source_is_active: is_active.clone(),
},
Replayable {
tx: queue,
inner: source,
buffer: Vec::with_capacity(chunk_size),
chunk_size,
is_active,
},
))
}
/// constant source, only works on a single span
#[derive(Debug)]
struct ReplayQueue {
inner: ArrayQueue<Vec<Sample>>,
normal_chunk_len: usize,
/// The last chunk in the queue may be smaller than
/// the normal chunk size. This is always equal to the
/// size of the last element in the queue.
/// (so normally chunk_size)
last_chunk: Mutex<Vec<Sample>>,
}
impl ReplayQueue {
fn new(queue_len: usize, chunk_size: usize) -> Self {
Self {
inner: ArrayQueue::new(queue_len),
normal_chunk_len: chunk_size,
last_chunk: Mutex::new(Vec::new()),
}
}
/// Returns the length in samples
fn len(&self) -> usize {
self.inner.len().saturating_sub(1) * self.normal_chunk_len
+ self
.last_chunk
.lock()
.expect("Self::push_last can not poison this lock")
.len()
}
fn pop(&self) -> Option<Vec<Sample>> {
self.inner.pop() // removes element that was inserted first
}
fn push_last(&self, mut samples: Vec<Sample>) {
let mut last_chunk = self
.last_chunk
.lock()
.expect("Self::len can not poison this lock");
std::mem::swap(&mut *last_chunk, &mut samples);
}
fn push_normal(&self, samples: Vec<Sample>) {
let _pushed_out_of_ringbuf = self.inner.force_push(samples);
}
}
/// constant source, only works on a single span
#[derive(Debug)]
pub struct Replayable<S: Source> {
inner: S,
buffer: Vec<Sample>,
chunk_size: usize,
tx: Arc<ReplayQueue>,
is_active: Arc<AtomicBool>,
}
impl<S: Source> Iterator for Replayable<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.inner.next() {
self.buffer.push(sample);
// If the buffer is full send it
if self.buffer.len() == self.chunk_size {
self.tx.push_normal(std::mem::take(&mut self.buffer));
}
Some(sample)
} else {
let last_chunk = std::mem::take(&mut self.buffer);
self.tx.push_last(last_chunk);
self.is_active.store(false, Ordering::Relaxed);
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<S: Source> Source for Replayable<S> {
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
fn channels(&self) -> ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}
/// constant source, only works on a single span
#[derive(Debug)]
pub struct Replay {
rx: Arc<ReplayQueue>,
buffer: std::vec::IntoIter<Sample>,
sleep_duration: Duration,
sample_rate: SampleRate,
channel_count: ChannelCount,
source_is_active: Arc<AtomicBool>,
}
impl Replay {
pub fn source_is_active(&self) -> bool {
// - source could return None and not drop
// - source could be dropped before returning None
self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
}
/// Duration of what is in the buffer and can be returned without blocking.
pub fn duration_ready(&self) -> Duration {
let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
Duration::from_secs_f64(seconds_queued)
}
/// Number of samples in the buffer and can be returned without blocking.
pub fn samples_ready(&self) -> usize {
self.rx.len() + self.buffer.len()
}
}
impl Iterator for Replay {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.buffer.next() {
return Some(sample);
}
loop {
if let Some(new_buffer) = self.rx.pop() {
self.buffer = new_buffer.into_iter();
return self.buffer.next();
}
if !self.source_is_active() {
return None;
}
// The queue does not support blocking on a next item. We want this queue as it
// is quite fast and provides a fixed size. We know how many samples are in a
// buffer so if we do not get one now we must be getting one after `sleep_duration`.
std::thread::sleep(self.sleep_duration);
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
((self.rx.len() + self.buffer.len()), None)
}
}
impl Source for Replay {
fn current_span_len(&self) -> Option<usize> {
None // source is not compatible with spans
}
fn channels(&self) -> ChannelCount {
self.channel_count
}
fn sample_rate(&self) -> SampleRate {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
#[cfg(test)]
mod tests {
use rodio::{nz, static_buffer::StaticSamplesBuffer};
use super::*;
use crate::{
RodioExt,
rodio_ext::tests::{SAMPLES, test_source},
};
#[test]
fn continues_after_history() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(3))
.expect("longer than 100ms");
source.by_ref().take(3).count();
let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
assert_eq!(&yielded, &SAMPLES[0..3],);
source.count();
let yielded: Vec<Sample> = replay.collect();
assert_eq!(&yielded, &SAMPLES[3..5],);
}
#[test]
fn keeps_only_latest() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
source.by_ref().take(5).count(); // get all items but do not end the source
let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
assert_eq!(&yielded, &SAMPLES[3..5]);
source.count(); // exhaust source
assert_eq!(replay.next(), None);
}
#[test]
fn keeps_correct_amount_of_seconds() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
// exhaust but do not yet end source
source.by_ref().take(40_000).count();
// take all samples we can without blocking
let ready = replay.samples_ready();
let n_yielded = replay.take_samples(ready).count();
let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
let margin = 16_000 / 10; // 100ms
assert!(n_yielded as u32 >= max - margin);
}
#[test]
fn samples_ready() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (mut replay, source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
assert_eq!(replay.by_ref().samples_ready(), 0);
source.take(8000).count(); // half a second
let margin = 16_000 / 10; // 100ms
let ready = replay.samples_ready();
assert!(ready >= 8000 - margin);
}
}

View File

@@ -0,0 +1,322 @@
use std::time::Duration;
use rodio::{Sample, SampleRate, Source};
use rubato::{FftFixedInOut, Resampler};
pub struct FixedResampler<S> {
input: S,
next_channel: usize,
next_frame: usize,
output_buffer: Vec<Vec<Sample>>,
input_buffer: Vec<Vec<Sample>>,
target_sample_rate: SampleRate,
resampler: FftFixedInOut<Sample>,
}
impl<S: Source> FixedResampler<S> {
pub fn new(input: S, target_sample_rate: SampleRate) -> Self {
let chunk_size_in =
Duration::from_millis(50).as_secs_f32() * input.sample_rate().get() as f32;
let chunk_size_in = chunk_size_in.ceil() as usize;
let resampler = FftFixedInOut::new(
input.sample_rate().get() as usize,
target_sample_rate.get() as usize,
chunk_size_in,
input.channels().get() as usize,
)
.expect(
"sample rates are non zero, and we are not changing it so there is no resample ratio",
);
let mut this = Self {
next_channel: 0,
next_frame: 0,
output_buffer: resampler.output_buffer_allocate(true),
input_buffer: resampler.input_buffer_allocate(false),
target_sample_rate,
resampler,
input,
};
this.bootstrap();
this
}
pub fn into_inner(self) -> S {
self.input
}
fn bootstrap(&mut self) -> Option<()> {
for _ in 0..self.resampler.input_frames_next() {
for input_channel in &mut self.input_buffer {
input_channel.push(self.input.next()?);
}
}
let (input_frames, output_frames) = self.resampler
.process_into_buffer(&mut self.input_buffer, &mut self.output_buffer, None).expect("Input and output buffer channels are correct as they have been set by the resampler. The buffer for each channel is the same length. The buffer length is what is requested the resampler.");
debug_assert_eq!(input_frames, self.input_buffer[0].len());
debug_assert_eq!(output_frames, self.output_buffer[0].len());
self.next_frame = self.resampler.output_delay();
self.next_channel = 0;
Some(())
}
#[cold]
fn resample_buffer(&mut self) -> Option<()> {
for input_channel in &mut self.input_buffer {
input_channel.clear();
}
for _ in 0..self.resampler.input_frames_next() {
for input_channel in &mut self.input_buffer {
input_channel.push(self.input.next()?);
}
}
let (input_frames, output_frames) = self.resampler
.process_into_buffer(&mut self.input_buffer, &mut self.output_buffer, None).expect("Input and output buffer channels are correct as they have been set by the resampler. The buffer for each channel is the same length. The buffer length is what is requested the resampler.");
debug_assert_eq!(input_frames, self.input_buffer[0].len());
debug_assert_eq!(output_frames, self.output_buffer[0].len());
self.next_frame = 0;
self.next_channel = 0;
Some(())
}
}
impl<S: Source> Source for FixedResampler<S> {
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> rodio::ChannelCount {
self.input.channels()
}
fn sample_rate(&self) -> rodio::SampleRate {
self.target_sample_rate
}
fn total_duration(&self) -> Option<std::time::Duration> {
self.input.total_duration()
}
}
impl<S: Source> FixedResampler<S> {
fn next_sample(&mut self) -> Option<Sample> {
let sample = self.output_buffer[self.next_channel]
.get(self.next_frame)
.copied();
if self.next_channel < (self.input.channels().get() - 1) as usize {
self.next_channel += 1;
} else {
self.next_channel = 0;
self.next_frame += 1;
}
sample
}
}
impl<S: Source> Iterator for FixedResampler<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.next_sample() {
return Some(sample);
}
self.resample_buffer()?;
self.next_sample()
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use crate::{
test::{recording_of_voice, sine},
RodioExt,
};
use itertools::Itertools;
use rodio::{nz, Source};
use spectrum_analyzer::{scaling::divide_by_N_sqrt, FrequencyLimit};
#[derive(Debug)]
struct PeakPitch {
pub median: f32,
pub error: f32,
}
fn assert_non_zero_volume_fuzzy(source: impl Source) {
let sample_rate = source.sample_rate();
let chunk_size = sample_rate.get() / 1000;
let ms_volume = source.into_iter().chunks(chunk_size as usize);
let ms_volume = ms_volume
.into_iter()
.map(|chunk| chunk.into_iter().map(|s| s.abs()).sum::<f32>() / chunk_size as f32);
for (millis, volume) in ms_volume.enumerate() {
assert!(
volume > 0.01,
"Volume about zero around {:?}",
Duration::from_millis(millis as u64)
)
}
}
fn median_peak_pitch(source: impl Source) -> PeakPitch {
use spectrum_analyzer::{samples_fft_to_spectrum, windows::hann_window};
let channels = source.channels().get();
let sample_rate = source.sample_rate().get();
let nyquist_freq = (sample_rate / 2) as f32;
let hundred_millis: usize = usize::try_from(sample_rate / 10)
.unwrap()
.next_power_of_two();
// de-interleave (take channel 0)
let samples: Vec<_> = source.step_by(channels as usize).collect();
let mut resolution = 0f32;
let mut peaks = samples
.chunks_exact(hundred_millis)
.map(|chunk| {
let spectrum = samples_fft_to_spectrum(
&hann_window(chunk),
sample_rate,
// only care about the human audible range (sorry bats)
// (resamplers can include artifacts outside this range
// we do not care about since we wont hear them anyway)
FrequencyLimit::Range(20f32, 20_000f32.min(nyquist_freq)),
Some(&divide_by_N_sqrt),
)
.unwrap();
resolution = resolution.max(spectrum.frequency_resolution());
spectrum.max().0
})
.collect_vec();
peaks.sort();
let median = peaks[peaks.len() / 2].val();
PeakPitch {
median,
error: resolution,
}
}
#[test]
fn constant_samplerate_preserves_length() {
let test_signal = recording_of_voice(nz!(3), nz!(48_000));
let resampled = test_signal.clone().constant_samplerate(nz!(16_000));
let diff_in_length = test_signal
.total_duration()
.unwrap()
.abs_diff(resampled.total_duration().unwrap());
assert!(diff_in_length.as_secs_f32() < 0.1)
}
#[test]
fn stereo_gets_preserved() {
use rodio::{
buffer::SamplesBuffer,
source::{Function, SignalGenerator},
};
let sample_rate = nz!(48_000);
let sample_rate_resampled = nz!(16_000);
let frequency_0 = 550f32;
let frequency_1 = 330f32;
let channel0 = SignalGenerator::new(sample_rate, frequency_0, Function::Sine)
.take_duration(Duration::from_secs(1));
let channel1 = SignalGenerator::new(sample_rate, frequency_1, Function::Sine)
.take_duration(Duration::from_secs(1));
let source = channel0.interleave(channel1).collect_vec();
let source = SamplesBuffer::new(nz!(2), sample_rate, source);
let resampled = source
.clone()
.constant_samplerate(sample_rate_resampled)
.collect_vec();
let (channel0_resampled, channel1_resampled): (Vec<_>, Vec<_>) = resampled
.chunks_exact(2)
.map(|s| TryInto::<[_; 2]>::try_into(s).unwrap())
.map(|[channel0, channel1]| (channel0, channel1))
.unzip();
for (resampled, frequency) in [
(channel0_resampled, frequency_0),
(channel1_resampled, frequency_1),
] {
let resampled = SamplesBuffer::new(nz!(1), sample_rate_resampled, resampled);
let peak_pitch = median_peak_pitch(resampled);
assert!(
(peak_pitch.median - frequency).abs() < peak_pitch.error,
"pitch should be {frequency} but was {peak_pitch:?}"
)
}
}
#[test]
fn resampler_does_not_add_any_latency() {
let resampled = sine(nz!(1), nz!(48_000))
.clone()
.constant_samplerate(nz!(16_000))
.into_samples_buffer();
assert_non_zero_volume_fuzzy(resampled);
}
#[cfg(test)]
mod constant_samplerate_preserves_pitch {
use crate::test::sine;
use super::*;
#[test]
fn one_channel() {
let test_signal = sine(nz!(1), nz!(48_000));
rodio::wav_to_file(test_signal.clone(), "test_signal2.wav").unwrap();
let resampled = test_signal
.clone()
.constant_samplerate(nz!(16_000))
.into_samples_buffer();
rodio::wav_to_file(resampled.clone(), "resampled2.wav").unwrap();
let peak_pitch_before = median_peak_pitch(test_signal);
let peak_pitch_after = median_peak_pitch(resampled);
assert!(
(peak_pitch_before.median - peak_pitch_after.median).abs()
< peak_pitch_before.error.max(peak_pitch_after.error),
"peak pitch_before: {peak_pitch_before:?}, peak pitch_after: {peak_pitch_after:?}"
);
}
#[test]
fn two_channel() {
let test_signal = sine(nz!(2), nz!(48_000));
let resampled = test_signal
.clone()
.constant_samplerate(nz!(16_000))
.into_samples_buffer();
let peak_pitch_before = median_peak_pitch(test_signal);
let peak_pitch_after = median_peak_pitch(resampled);
assert!(
(peak_pitch_before.median - peak_pitch_after.median).abs()
< peak_pitch_before.error.max(peak_pitch_after.error),
"peak pitch_before: {peak_pitch_before:?}, peak pitch_after: {peak_pitch_after:?}"
);
}
}
}

View File

@@ -0,0 +1,93 @@
use denoise::{Denoiser, DenoiserError};
use rodio::Sample;
use std::time::Duration;
use super::RodioExt;
use super::resample::FixedResampler;
use rodio::ChannelCount;
use rodio::SampleRate;
use rodio::Source;
#[derive(Default)]
enum InnerRSD<S: Source> {
Transparent(S),
Denoised(FixedResampler<Denoiser<FixedResampler<S>>>),
#[default]
ShouldNotExist,
}
pub struct ResamplingDenoiser<S: Source> {
inner: InnerRSD<S>,
}
impl<S: Source> ResamplingDenoiser<S> {
pub fn new(source: S) -> Result<Self, DenoiserError> {
Ok(ResamplingDenoiser {
inner: InnerRSD::Transparent(source),
})
}
pub fn set_enabled(&mut self, enabled: bool) -> Result<(), DenoiserError> {
self.inner = match std::mem::take(&mut self.inner) {
InnerRSD::Transparent(s) => {
if enabled {
let sr = s.sample_rate();
InnerRSD::Denoised(
Denoiser::try_new(s.constant_samplerate(denoise::SUPPORTED_SAMPLE_RATE))?
.constant_samplerate(sr),
)
} else {
InnerRSD::Transparent(s)
}
}
InnerRSD::Denoised(s) => {
if !enabled {
InnerRSD::Transparent(s.into_inner().into_inner().into_inner())
} else {
InnerRSD::Denoised(s)
}
}
InnerRSD::ShouldNotExist => unreachable!(),
};
Ok(())
}
}
impl<S: Source> Source for ResamplingDenoiser<S> {
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> ChannelCount {
match &self.inner {
// Different types, can't unify :c
InnerRSD::Transparent(s) => s.channels(),
InnerRSD::Denoised(s) => s.channels(),
InnerRSD::ShouldNotExist => unreachable!(),
}
}
fn sample_rate(&self) -> SampleRate {
match &self.inner {
// Different types, can't unify :c
InnerRSD::Transparent(s) => s.sample_rate(),
InnerRSD::Denoised(s) => s.sample_rate(),
InnerRSD::ShouldNotExist => unreachable!(),
}
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
impl<S: Source> Iterator for ResamplingDenoiser<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
match &mut self.inner {
InnerRSD::Denoised(denoiser) => denoiser.next(),
InnerRSD::Transparent(source) => source.next(),
InnerRSD::ShouldNotExist => unreachable!(),
}
}
}

495
crates/audio/src/test.rs Normal file
View File

@@ -0,0 +1,495 @@
//! A complex end to end audio test comparing audio features like fft spectrum
//! of a signal before and after going through the audio pipeline
use std::env::current_dir;
use std::io::Cursor;
use std::iter;
use std::ops::Range;
use std::sync::atomic::Ordering;
use std::time::Duration;
use gpui::BorrowAppContext;
use plotly::layout::Axis;
use rand::SeedableRng;
use rand::rngs::SmallRng;
use rodio::{ChannelCount, Decoder, SampleRate, mixer, nz, wav_to_file};
use rodio::{Source, buffer::SamplesBuffer};
use spectrum_analyzer::scaling::divide_by_N_sqrt;
use spectrum_analyzer::windows::{self, hann_window};
use spectrum_analyzer::{FrequencyLimit, FrequencySpectrum, samples_fft_to_spectrum};
use crate::audio_settings::LIVE_SETTINGS;
use crate::test::detector::BasicVoiceDetector;
use crate::{Audio, LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE, RodioExt, VoipParts};
mod detector;
// in hz
const HUMAN_SPEECH_RANGE: Range<f32> = 90.0..260.0;
#[gpui::test]
fn test_input_pipeline(cx: &mut gpui::TestAppContext) {
// strange params to invite bugs to show themselves
let test_signal = recording_of_voice(nz!(3), nz!(48_000));
let test_signal_duration = test_signal
.total_duration()
.expect("recordings have a length");
let voip_parts = VoipParts::new(&cx.to_async()).unwrap();
LIVE_SETTINGS.denoise.store(false, Ordering::Relaxed);
let input = Audio::input_pipeline(voip_parts, test_signal.clone()).unwrap();
let input_pipeline = input
.take_duration(test_signal_duration)
.into_samples_buffer();
let expected_output =
recording_of_voice(input_pipeline.channels(), input_pipeline.sample_rate());
rodio::wav_to_file(
BasicVoiceDetector::add_voice_activity_as_channel(input_pipeline.clone()),
"input_pipeline_output.wav",
)
.unwrap();
rodio::wav_to_file(
BasicVoiceDetector::add_voice_activity_as_channel(expected_output.clone()),
"input_pipeline_expect.wav",
)
.unwrap();
assert_similar_voice_spectra(expected_output, input_pipeline);
}
#[gpui::test]
fn test_output_pipeline(cx: &mut gpui::TestAppContext) {
let test_signal = recording_of_voice(LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE);
let test_signal_duration = test_signal
.total_duration()
.expect("recordings have a length");
let audio_output =
cx.update(|cx| cx.update_default_global::<Audio, _>(|audio, _| audio.setup_mixer()));
cx.update(|cx| {
Audio::play_voip_stream(test_signal.clone(), "test".to_string(), true, cx).unwrap()
});
let output_pipeline = audio_output
.take_duration(test_signal_duration)
.into_samples_buffer();
// dont care about the channel count and sample rate, as long as the voice
// signal matches
let expected_output =
recording_of_voice(output_pipeline.channels(), output_pipeline.sample_rate());
rodio::wav_to_file(output_pipeline.clone(), "output_pipeline_output.wav").unwrap();
rodio::wav_to_file(expected_output.clone(), "output_pipeline_expect.wav").unwrap();
assert_similar_voice_spectra(expected_output, output_pipeline);
}
// TODO make a perf variant
#[gpui::test]
fn test_full_audio_pipeline(cx: &mut gpui::TestAppContext) {
let test_signal = recording_of_voice(nz!(3), nz!(44_100));
let test_signal_duration = test_signal
.total_duration()
.expect("recordings have a length");
let audio_output =
cx.update(|cx| cx.update_default_global::<Audio, _>(|audio, _| audio.setup_mixer()));
let voip_parts = VoipParts::new(&cx.to_async()).unwrap();
let input = Audio::input_pipeline(voip_parts, test_signal).unwrap();
cx.update(|cx| Audio::play_voip_stream(input, "test".to_string(), true, cx).unwrap());
let full_pipeline = audio_output
.take_duration(test_signal_duration)
.into_samples_buffer();
// dont care about the channel count and sample rate, as long as the voice
// signal matches
let expected_output = recording_of_voice(full_pipeline.channels(), full_pipeline.sample_rate());
rodio::wav_to_file(full_pipeline.clone(), "full_pipeline_output.wav").unwrap();
rodio::wav_to_file(expected_output.clone(), "full_pipeline_expect.wav").unwrap();
assert_similar_voice_spectra(expected_output, full_pipeline);
}
fn human_perceivable_energy(spectrum: &FrequencySpectrum) -> f32 {
spectrum
.data()
.iter()
.filter(|(freq, _)| HUMAN_SPEECH_RANGE.contains(&freq.val()))
.max_by_key(|(_, energy)| energy)
.unwrap()
.1
.val()
}
fn energy_of_chunk(chunk: &[rodio::Sample], sample_rate: SampleRate) -> f32 {
let spectrum = samples_fft_to_spectrum(
&hann_window(chunk),
sample_rate.get(),
FrequencyLimit::All,
Some(&divide_by_N_sqrt),
)
.unwrap();
human_perceivable_energy(&spectrum)
}
fn maximum_energy(mut a: impl rodio::Source) -> f32 {
let a_samples: Vec<_> = a.by_ref().collect();
assert!(!a_samples.is_empty());
let ten_millis: usize = usize::try_from(a.sample_rate().get() / 100)
.unwrap()
.next_power_of_two();
a_samples
.chunks_exact(ten_millis)
.map(|chunk| energy_of_chunk(chunk, a.sample_rate()))
.fold(0f32, |max, energy| max.max(energy))
}
// Test signals should be at least 50% voice
fn assert_similar_voice_spectra(
expected: impl rodio::Source + Clone,
pipeline: impl rodio::Source + Clone,
) {
assert!(expected.total_duration().is_some());
assert!(pipeline.total_duration().is_some());
assert_eq!(expected.sample_rate(), pipeline.sample_rate());
assert_eq!(expected.channels(), pipeline.channels());
let total_duration = expected.total_duration().expect("just asserted");
let voice_detector = BasicVoiceDetector::new(expected.clone());
assert!(
voice_detector
.voice_less_duration()
.div_duration_f32(total_duration)
< 0.75,
"Test samples should be at least 25% voice and those parts should be recognized as such"
);
let sample_rate = expected.sample_rate();
let expected_samples: Vec<_> = expected.clone().collect();
let pipeline_samples: Vec<_> = pipeline.clone().collect();
const WINDOW_SIZE: usize = 2048;
const WINDOW_OVERLAP: usize = 0;
// beautiful functional code :3
let (passing, len) = voice_detector
.segments_with_voice
.into_iter()
.filter(|to_judge| to_judge.start_samples(sample_rate) > WINDOW_OVERLAP)
.filter(|to_judge| {
to_judge.end_samples(sample_rate) < expected_samples.len() - WINDOW_OVERLAP
})
.flat_map(|to_judge| {
let chunks = (to_judge.len_samples(sample_rate) + WINDOW_OVERLAP) / WINDOW_SIZE;
(0..chunks)
.map(|i| {
let window_start =
&to_judge.start_samples(sample_rate) + i * WINDOW_SIZE + WINDOW_OVERLAP;
let window_center = window_start + WINDOW_SIZE / 2;
let window_center = window_center as f64 / sample_rate.get() as f64;
let window_center = Duration::from_secs_f64(window_center);
(window_start, window_center)
})
.collect::<Vec<_>>()
})
.map(|(window_start, window_center)| {
let window = window_start..window_start + WINDOW_SIZE;
(
&expected_samples[window.clone()],
&pipeline_samples[dbg!(window)],
dbg!(window_center),
)
})
.map(|(input, output, window_center)| {
(
samples_fft_to_spectrum(
&windows::hamming_window(input),
sample_rate.get(),
FrequencyLimit::Min(4.0),
Some(&divide_by_N_sqrt),
)
.unwrap(),
samples_fft_to_spectrum(
&windows::hamming_window(output),
sample_rate.get(),
FrequencyLimit::Min(4.0),
Some(&divide_by_N_sqrt),
)
.unwrap(),
window_center,
)
})
.filter_map(assert_same_voice_signal)
.fold((0, 0), |(passing, len), passed| {
(passing + u32::from(passed), len + 1)
});
assert!(
passing > len * 9 / 10,
">10% of chunks mismatched: {passing} passing out of {len}"
);
}
fn spectra_chunk_size(source: &impl Source, minimum_duration: Duration) -> usize {
((minimum_duration.as_secs_f64() * source.sample_rate().get() as f64) as usize)
.next_power_of_two()
}
fn spectrum_duration(source: &impl Source, minimum_duration: Duration) -> Duration {
Duration::from_secs_f64(
spectra_chunk_size(source, minimum_duration) as f64 / source.sample_rate().get() as f64,
)
}
fn assert_same_voice_signal(
(expected, pipeline, window_center): (FrequencySpectrum, FrequencySpectrum, Duration),
) -> Option<bool> {
// The timbre of a voice (the difference in how voices sound) is determined
// by all kinds of resonances in the throat/mouth. These happen roughly at
// multiples of the lowest usually loudest voice frequency.
let (voice_freq_expected, voice_freq_pipeline) = match (
fundamental_voice_freq(&expected),
fundamental_voice_freq(&pipeline),
) {
(None, _) => return dbg!(None),
(Some(voice_freq_expected), None) => {
panic!(
"Could not find fundamental voice freq in output while there is one in the input at {voice_freq_expected}Hz.\nLoudest 5 frequencies in output:\n{}\n\n{}",
display_loudest_5_frequencies(&pipeline),
plot_spectra(&[(&expected, "expected"), (&pipeline, "pipeline")]),
);
}
(Some(voice_freq_expected), Some(voice_freq_pipeline)) => {
(voice_freq_expected, voice_freq_pipeline)
}
};
assert!(
less_than_10percent_diff((voice_freq_expected, voice_freq_pipeline)),
"expected: {voice_freq_expected}, pipeline: {voice_freq_pipeline}, at: {window_center:?}\n\n{}",
plot_spectra(&[(&expected, "expected"), (&pipeline, "pipeline")])
);
// Guards against voice distortion
// unfortunately affected by (de)noise as that (de)distorts voice.
Some(same_ratio_between_harmonics(
&expected,
&pipeline,
voice_freq_expected,
))
}
fn fundamental_voice_freq(spectrum: &FrequencySpectrum) -> Option<f32> {
spectrum
.data()
.iter()
.filter(|(freq, _)| HUMAN_SPEECH_RANGE.contains(&freq.val()))
.max_by_key(|(_, energy)| energy)
.map(|(freq, _)| freq.val())
}
fn same_ratio_between_harmonics(
expected: &FrequencySpectrum,
pipeline: &FrequencySpectrum,
fundamental_voice_freq: f32,
) -> bool {
fn ratios(
spectrum: &FrequencySpectrum,
fundamental_voice_freq: f32,
) -> impl Iterator<Item = f32> {
let (_freq, fundamental) = spectrum.freq_val_closest(fundamental_voice_freq);
let voice_harmonics = (2..=3)
.into_iter()
.map(move |i| dbg!(fundamental_voice_freq) * i as f32);
voice_harmonics.clone().map(move |freq| {
let (_freq, harmonic) = spectrum.freq_val_closest(freq);
harmonic.val() / fundamental.val()
})
}
ratios(expected, fundamental_voice_freq)
.zip(ratios(pipeline, fundamental_voice_freq))
.all(less_than_20percent_diff)
}
fn less_than_10percent_diff((a, b): (f32, f32)) -> bool {
dbg!(a, b);
(a - b).abs() < a.max(b) * 0.1
}
fn less_than_20percent_diff((a, b): (f32, f32)) -> bool {
dbg!(a, b);
(a - b).abs() < a.max(b) * 0.3
}
fn display_loudest_5_frequencies(spectrum: &FrequencySpectrum) -> String {
let mut spectrum: Vec<_> = spectrum.data().iter().collect();
spectrum.sort_by_key(|(_, amplitude)| amplitude);
spectrum.reverse();
spectrum.truncate(5);
spectrum
.into_iter()
.map(|(freq, amplitude)| format!("freq: {freq},\tamplitude: {amplitude}\n"))
.collect()
}
// Returns ascii encoding a link to open the plot
pub fn plot_spectra(spectra: &[(&FrequencySpectrum, &str)]) -> String {
use plotly::{Bar, Plot};
let mut plot = Plot::new();
let layout = plotly::Layout::new().x_axis(Axis::new().type_(plotly::layout::AxisType::Log));
// .y_axis(Axis::new().type_(plotly::layout::AxisType::Log));
plot.set_layout(layout);
for (spectrum, label) in spectra {
let (x, y): (Vec<_>, Vec<_>) = spectrum
.data()
.iter()
.map(|(freq, amplitude)| (freq.val(), amplitude.val()))
.filter(|(freq, _)| *freq > 85.0)
.unzip();
let trace = Bar::new(x, y).name(label).show_legend(true).opacity(0.5);
plot.add_trace(trace);
}
let path = current_dir().unwrap().join("plot.html");
plot.write_html(&path);
link(path.display(), "Open spectra plot")
}
fn link(target: impl std::fmt::Display, name: &str) -> String {
const START_OF_LINK: &str = "\x1b]8;;file://";
const START_OF_NAME: &str = "\x1b\\";
const END_OF_LINK: &str = "\x1b]8;;\x1b\\";
format!("{START_OF_LINK}{target}{START_OF_NAME}{name}{END_OF_LINK}")
}
pub(crate) fn sine(channels: ChannelCount, sample_rate: SampleRate) -> impl Source + Clone {
// We can not resample this file ourselves as then we would not be testing
// the resampler. These are test files resampled by audacity.
let recording = match (channels.get(), sample_rate.get()) {
(1, 48_000) => include_bytes!("../test/sine_1_48000.wav").as_slice(),
(2, 48_000) => include_bytes!("../test/sine_2_48000.wav").as_slice(),
_ => panic!("No test recording with {channels} channels and sampler rate: {sample_rate}"),
};
let recording = Cursor::new(recording);
let recording = Decoder::new(recording).unwrap();
SamplesBuffer::new(
recording.channels(),
recording.sample_rate(),
recording.collect::<Vec<_>>(),
)
}
pub(crate) fn recording_of_voice(
channels: ChannelCount,
sample_rate: SampleRate,
) -> impl Source + Clone {
// We can not resample this file ourselves as then we would not be testing
// the resampler. These are test files resampled by audacity.
let recording = match (channels.get(), sample_rate.get()) {
(1, 44_100) => include_bytes!("../test/input_test_1_44100.wav").as_slice(),
(1, 48_000) => include_bytes!("../test/input_test_1_48000.wav").as_slice(),
(2, 44_100) => include_bytes!("../test/input_test_2_44100.wav").as_slice(),
(2, 48_000) => include_bytes!("../test/input_test_2_48000.wav").as_slice(),
(3, 44_100) => include_bytes!("../test/input_test_3_44100.wav").as_slice(),
(3, 48_000) => include_bytes!("../test/input_test_3_48000.wav").as_slice(),
_ => panic!("No test recording with {channels} channels and sampler rate: {sample_rate}"),
};
let recording = Cursor::new(recording);
let recording = Decoder::new(recording).unwrap();
SamplesBuffer::new(
recording.channels(),
recording.sample_rate(),
recording.collect::<Vec<_>>(),
)
}
#[test]
#[should_panic]
fn test_rejects_pitch_shift() {
// also known as 'robot/chipmunk voice'
let original = recording_of_voice(nz!(1), nz!(44100));
let pitch_shifted = original
.clone()
.speed(1.2) // effectively increases the pitch by 20%
.constant_samplerate(original.sample_rate())
.into_samples_buffer();
wav_to_file(original.clone(), "original.wav").unwrap();
wav_to_file(pitch_shifted.clone(), "pitch_shifted.wav").unwrap();
assert_similar_voice_spectra(original, pitch_shifted);
}
#[test]
#[should_panic]
fn test_rejects_large_amounts_of_noise() {
let original = recording_of_voice(nz!(1), nz!(44100));
let with_noise = add_noise(&original, 0.5);
assert_similar_voice_spectra(original, with_noise);
}
#[test]
fn test_ignores_volume() {
let original = recording_of_voice(nz!(1), nz!(44100));
let amplified = original.clone().amplify(1.42);
assert_similar_voice_spectra(original, amplified);
}
#[test]
fn test_ignore_low_volume_noise() {
let original = recording_of_voice(nz!(1), nz!(44100));
// 5% noise is quite hearable as the noise is across all frequencies so is
// perceived far more intense then a voice
let with_noise = add_noise(&original, 0.05);
assert_similar_voice_spectra(original, with_noise);
}
fn add_noise(original: &(impl Source + Clone + Send + 'static), amount: f32) -> SamplesBuffer {
let original_volume = original.clone().max_by(|a, b| a.total_cmp(b)).unwrap();
let noise = rodio::source::noise::WhiteUniform::new_with_rng(
original.sample_rate(),
SmallRng::seed_from_u64(1), // lets keep failure repeatable
);
let (mixer, with_noise) = mixer::mixer(original.channels(), original.sample_rate());
mixer.add(original.clone());
mixer.add(noise.amplify(amount * original_volume));
let with_noise = with_noise
.take_duration(
original
.total_duration()
.expect("should be a fixed length recording"),
)
.into_samples_buffer();
with_noise
}
#[test]
fn test_ignores_small_shifts() {
let original = recording_of_voice(nz!(1), nz!(44100));
let shifted = iter::repeat(0f32).take(10).chain(original.clone());
let shifted = SamplesBuffer::new(
original.channels(),
original.sample_rate(),
shifted.collect::<Vec<_>>(),
);
assert_similar_voice_spectra(original, shifted);
}

View File

@@ -0,0 +1,210 @@
use crate::RodioExt;
use crate::rodio_ext::ConstantChannelCount;
use crate::test::sine;
use crate::test::spectrum_duration;
use super::human_perceivable_energy;
use rodio::SampleRate;
use rodio::buffer::SamplesBuffer;
use rodio::nz;
use spectrum_analyzer::FrequencyLimit;
use spectrum_analyzer::FrequencySpectrum;
use spectrum_analyzer::scaling::divide_by_N_sqrt;
use spectrum_analyzer::windows::hann_window;
use super::maximum_energy;
use rodio::Source;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct VoiceSegment {
pub start: Duration,
pub end: Duration,
}
impl VoiceSegment {
const ZERO: Self = Self {
start: Duration::ZERO,
end: Duration::ZERO,
};
fn length(&self) -> Duration {
self.end - self.start
}
fn until(&self, other: &Self) -> Duration {
debug_assert!(self.end < other.start);
other.start - self.end
}
pub(crate) fn len_samples(&self, sample_rate: SampleRate) -> usize {
(self.length().as_secs_f64() * sample_rate.get() as f64) as usize
}
pub(crate) fn start_samples(&self, sample_rate: SampleRate) -> usize {
(self.start.as_secs_f64() * sample_rate.get() as f64) as usize
}
pub(crate) fn end_samples(&self, sample_rate: SampleRate) -> usize {
(self.end.as_secs_f64() * sample_rate.get() as f64) as usize
}
}
pub(crate) struct BasicVoiceDetector {
pub(crate) segments_with_voice: Vec<VoiceSegment>,
}
impl BasicVoiceDetector {
pub(crate) fn new(source: impl Source + Clone) -> Self {
// only works on mono
let source = ConstantChannelCount::new(source, nz!(1)).into_samples_buffer();
// this gives a good resolution
let minimum_chunk_duration = Duration::from_millis(20);
let actual_chunk_duration = spectrum_duration(&source, minimum_chunk_duration);
let mut spectrum_start_pos = Duration::ZERO;
let mut partial_segment = None;
// empirically determined (by looking in audacity)
// see the 'soup' test for how
//
// while this might seem low remember humans precieve sound
// logarithmically. So 40% of energy sounds like 80% volume.
let threshold = 0.4 * maximum_energy(source.clone());
let segments_with_voice: Vec<_> = iter_spectra(source.clone(), actual_chunk_duration)
.filter_map(|spectrum| {
let voice_detected = human_perceivable_energy(&spectrum) > threshold;
spectrum_start_pos += actual_chunk_duration;
match (&mut partial_segment, voice_detected) {
(Some(VoiceSegment { end, .. }), true) => *end = spectrum_start_pos,
(Some(VoiceSegment { start, .. }), false) => {
let res = Some(VoiceSegment {
start: *start,
end: spectrum_start_pos,
});
partial_segment = None;
return res;
}
(None, true) => {
partial_segment = Some(VoiceSegment {
start: spectrum_start_pos,
end: spectrum_start_pos,
})
}
(None, false) => partial_segment = None,
};
None
})
.collect();
Self {
segments_with_voice,
}
}
pub fn voice_less_duration(&self) -> Duration {
self.segments_with_voice
.iter()
.map(|range| range.end - range.start)
.sum()
}
fn beep_where_voice_detected(&self, source: &impl Source) -> SamplesBuffer {
let sine = sine(nz!(1), source.sample_rate());
let mut with_voice = [VoiceSegment::ZERO]
.iter()
.chain(dbg!(&self.segments_with_voice).iter())
.peekable();
let mut samples = Vec::new();
loop {
let Some(current_voice_segment) = with_voice.next() else {
break;
};
let voice_range_duration = current_voice_segment.length();
samples.extend(
sine.clone()
.amplify(1.0)
.take_duration(voice_range_duration),
);
let Some(next_voice_segment) = with_voice.peek() else {
break;
};
let until_next = current_voice_segment.until(next_voice_segment);
dbg!(until_next);
samples.extend(sine.clone().amplify(0.0).take_duration(until_next));
}
SamplesBuffer::new(nz!(1), source.sample_rate(), samples)
}
pub fn add_voice_activity_as_channel(mut source: impl Source + Clone) -> impl Source {
let detector = Self::new(source.clone());
let mut voice_activity = detector.beep_where_voice_detected(&source).into_iter();
let mut samples = Vec::new();
loop {
let Some(s1) = source.next() else {
break;
};
let Some(s2) = source.next() else {
break;
};
let Some(s3) = voice_activity.next() else {
break;
};
samples.extend_from_slice(&[s1, s2, s3]);
}
SamplesBuffer::new(
source.channels().checked_add(1).unwrap(),
source.sample_rate(),
samples,
)
}
}
fn iter_spectra(
expected: impl Source + Clone,
chunk_duration: Duration,
) -> impl Iterator<Item = FrequencySpectrum> {
assert!(expected.total_duration().is_some());
let chunk_size = super::spectra_chunk_size(&expected, chunk_duration);
let expected_samples: Vec<_> = expected.clone().collect();
expected_samples
.chunks_exact(chunk_size)
.map(|input| {
super::samples_fft_to_spectrum(
&hann_window(input),
expected.sample_rate().get(),
FrequencyLimit::Min(4.0),
Some(&divide_by_N_sqrt),
)
.unwrap()
})
.collect::<Vec<_>>()
.into_iter()
}
#[cfg(test)]
mod test {
use crate::test::{detector::BasicVoiceDetector, recording_of_voice};
use rodio::{nz, wav_to_file};
#[test]
fn soup() {
let original = recording_of_voice(nz!(1), nz!(48000));
let detector = BasicVoiceDetector::new(original.clone());
let siny = detector.beep_where_voice_detected(&original);
wav_to_file(siny, "voice_activity.wav").unwrap();
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -15,7 +15,7 @@ log.workspace = true
rodio = { workspace = true, features = ["wav_output"] }
rustfft = { version = "6.2.0", features = ["avx"] }
realfft = "3.4.0"
rustfft.workspace = true
realfft.workspace = true
thiserror.workspace = true
workspace-hack.workspace = true

View File

@@ -8,14 +8,15 @@ use rodio::{ChannelCount, Sample, SampleRate, Source, nz};
use crate::engine::BLOCK_SHIFT;
const SUPPORTED_SAMPLE_RATE: SampleRate = nz!(16_000);
const SUPPORTED_CHANNEL_COUNT: ChannelCount = nz!(1);
pub const SUPPORTED_SAMPLE_RATE: SampleRate = nz!(16_000);
pub const SUPPORTED_CHANNEL_COUNT: ChannelCount = nz!(1);
pub struct Denoiser<S: Source> {
inner: S,
input_tx: mpsc::Sender<[Sample; BLOCK_SHIFT]>,
denoised_rx: mpsc::Receiver<[Sample; BLOCK_SHIFT]>,
ready: [Sample; BLOCK_SHIFT],
exhausted: bool,
next: usize,
state: IterState,
// When disabled instead of reading denoised sub-blocks from the engine through
@@ -94,9 +95,14 @@ impl<S: Source> Denoiser<S> {
state: IterState::Startup { enabled: true },
next: BLOCK_SHIFT,
queued: Queue::new(),
exhausted: false,
})
}
pub fn into_inner(self) -> S {
self.inner
}
pub fn set_enabled(&mut self, enabled: bool) {
self.state = match (enabled, self.state) {
(false, IterState::StartingMidAudio { .. }) | (false, IterState::Enabled) => {
@@ -178,11 +184,18 @@ struct DenoiseEngineCrashed;
impl<S: Source> Denoiser<S> {
#[cold]
fn prepare_next_ready(&mut self) -> Result<Option<f32>, DenoiseEngineCrashed> {
// Iterator next allows calling next multiple times after receiving none
// We would hang waiting for denoised audio if we did not return here.
if self.exhausted {
return Ok(None);
}
self.state = match self.state {
IterState::Startup { enabled } => {
// guaranteed to be coming from silence
for _ in 0..3 {
let Some(sub_block) = read_sub_block(&mut self.inner) else {
self.exhausted = true;
return Ok(None);
};
self.queued.push(sub_block);
@@ -191,6 +204,7 @@ impl<S: Source> Denoiser<S> {
.map_err(|_| DenoiseEngineCrashed)?;
}
let Some(sub_block) = read_sub_block(&mut self.inner) else {
self.exhausted = true;
return Ok(None);
};
self.queued.push(sub_block);
@@ -202,6 +216,7 @@ impl<S: Source> Denoiser<S> {
self.ready = self.denoised_rx.recv().map_err(|_| DenoiseEngineCrashed)?;
let Some(sub_block) = read_sub_block(&mut self.inner) else {
self.exhausted = true;
return Ok(None);
};
self.queued.push(sub_block);
@@ -216,6 +231,7 @@ impl<S: Source> Denoiser<S> {
IterState::Enabled => {
self.ready = self.denoised_rx.recv().map_err(|_| DenoiseEngineCrashed)?;
let Some(sub_block) = read_sub_block(&mut self.inner) else {
self.exhausted = true;
return Ok(None);
};
self.queued.push(sub_block);
@@ -229,6 +245,7 @@ impl<S: Source> Denoiser<S> {
// we can re-enable at any point.
self.ready = self.queued.pop();
let Some(sub_block) = read_sub_block(&mut self.inner) else {
self.exhausted = true;
return Ok(None);
};
self.queued.push(sub_block);
@@ -239,6 +256,7 @@ impl<S: Source> Denoiser<S> {
} => {
self.ready = self.queued.pop();
let Some(sub_block) = read_sub_block(&mut self.inner) else {
self.exhausted = true;
return Ok(None);
};
self.queued.push(sub_block);

View File

@@ -9,19 +9,19 @@ use rodio::DeviceTrait as _;
mod record;
pub use record::CaptureInput;
#[cfg(not(any(
test,
feature = "test-support",
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
)))]
// #[cfg(not(any(
// test,
// feature = "test-support",
// all(target_os = "windows", target_env = "gnu"),
// target_os = "freebsd"
// )))]
mod livekit_client;
#[cfg(not(any(
test,
feature = "test-support",
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
)))]
// #[cfg(not(any(
// test,
// feature = "test-support",
// all(target_os = "windows", target_env = "gnu"),
// target_os = "freebsd"
// )))]
pub use livekit_client::*;
// If you need proper LSP in livekit_client you've got to comment
@@ -29,27 +29,32 @@ pub use livekit_client::*;
// - the mods: mock_client & test and their conditional blocks
// - the pub use mock_client::* and their conditional blocks
#[cfg(any(
test,
feature = "test-support",
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
))]
mod mock_client;
#[cfg(any(
test,
feature = "test-support",
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
))]
pub mod test;
#[cfg(any(
test,
feature = "test-support",
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
))]
pub use mock_client::*;
// #[cfg(all(
// test,
// any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")
// ))]
// mod end_to_end_test;
// #[cfg(any(
// test,
// feature = "test-support",
// all(target_os = "windows", target_env = "gnu"),
// target_os = "freebsd"
// ))]
// mod mock_client;
// #[cfg(any(
// test,
// feature = "test-support",
// all(target_os = "windows", target_env = "gnu"),
// target_os = "freebsd"
// ))]
// pub mod test;
// #[cfg(any(
// test,
// feature = "test-support",
// all(target_os = "windows", target_env = "gnu"),
// target_os = "freebsd"
// ))]
// pub use mock_client::*;
#[derive(Debug, Clone)]
pub enum Participant {

View File

@@ -10,7 +10,7 @@ use log::info;
use playback::capture_local_video_track;
use settings::Settings;
mod playback;
pub(crate) mod playback;
use crate::{
LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication,

View File

@@ -600,8 +600,8 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti
winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] }
windows-core = { version = "0.61" }
windows-numerics = { version = "0.2" }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
windows-sys-d4189bed749088b6 = { package = "windows-sys", version = "0.61", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
@@ -627,8 +627,8 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti
winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] }
windows-core = { version = "0.61" }
windows-numerics = { version = "0.2" }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
windows-sys-d4189bed749088b6 = { package = "windows-sys", version = "0.61", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }