Compare commits
19 Commits
perf/proje
...
hifi-resam
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6acc181a6 | ||
|
|
c78baaf71f | ||
|
|
f602047b3c | ||
|
|
d372473e1e | ||
|
|
4cef8743b1 | ||
|
|
5e6dd2b5d4 | ||
|
|
b01378cbb4 | ||
|
|
f4c397a18c | ||
|
|
c4db4c5e59 | ||
|
|
b9832bca1e | ||
|
|
0d17281cad | ||
|
|
2627309c27 | ||
|
|
11251105eb | ||
|
|
04f4c780b1 | ||
|
|
ef9c9a718f | ||
|
|
b3c20c2d5c | ||
|
|
e84660c0ad | ||
|
|
dfc70b3020 | ||
|
|
dc319c1895 |
230
Cargo.lock
generated
230
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
308
crates/audio/src/rodio_ext/replayable.rs
Normal file
308
crates/audio/src/rodio_ext/replayable.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
322
crates/audio/src/rodio_ext/resample.rs
Normal file
322
crates/audio/src/rodio_ext/resample.rs
Normal 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(÷_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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
93
crates/audio/src/rodio_ext/resampling_denoise.rs
Normal file
93
crates/audio/src/rodio_ext/resampling_denoise.rs
Normal 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
495
crates/audio/src/test.rs
Normal 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(÷_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(÷_by_N_sqrt),
|
||||
)
|
||||
.unwrap(),
|
||||
samples_fft_to_spectrum(
|
||||
&windows::hamming_window(output),
|
||||
sample_rate.get(),
|
||||
FrequencyLimit::Min(4.0),
|
||||
Some(÷_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);
|
||||
}
|
||||
210
crates/audio/src/test/detector.rs
Normal file
210
crates/audio/src/test/detector.rs
Normal 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(÷_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();
|
||||
}
|
||||
}
|
||||
BIN
crates/audio/test/input_test_1_44100.wav
Normal file
BIN
crates/audio/test/input_test_1_44100.wav
Normal file
Binary file not shown.
BIN
crates/audio/test/input_test_1_48000.wav
Normal file
BIN
crates/audio/test/input_test_1_48000.wav
Normal file
Binary file not shown.
BIN
crates/audio/test/input_test_2_44100.wav
Normal file
BIN
crates/audio/test/input_test_2_44100.wav
Normal file
Binary file not shown.
BIN
crates/audio/test/input_test_2_48000.wav
Normal file
BIN
crates/audio/test/input_test_2_48000.wav
Normal file
Binary file not shown.
BIN
crates/audio/test/input_test_3_44100.wav
Normal file
BIN
crates/audio/test/input_test_3_44100.wav
Normal file
Binary file not shown.
BIN
crates/audio/test/input_test_3_48000.wav
Normal file
BIN
crates/audio/test/input_test_3_48000.wav
Normal file
Binary file not shown.
BIN
crates/audio/test/sine_1_48000.wav
Normal file
BIN
crates/audio/test/sine_1_48000.wav
Normal file
Binary file not shown.
BIN
crates/audio/test/sine_2_48000.wav
Normal file
BIN
crates/audio/test/sine_2_48000.wav
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user