4 Commits

Author SHA1 Message Date
sam 1efd32892e denoising actually works on desktop! 2025-10-28 01:20:59 -06:00
sam 1ff302816e load an actual denoising model 2025-10-28 01:10:35 -06:00
sam ebcf5ce4ce fix some dependencies 2025-10-27 22:49:49 -06:00
sam 4e30be3ebd switch to scss asset & remove sir 2025-10-27 19:04:33 -06:00
12 changed files with 3184 additions and 1620 deletions
+1
View File
@@ -6,3 +6,4 @@ server_hash.txt
proxy/bundle
config.toml
proxy/config.toml
gui/assets/*_onnx.tar.gz
Generated
+2605 -1136
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -40,7 +40,7 @@ services:
ports:
- "4433:4433/tcp"
- "4433:4433/udp"
command: ["cargo", "run", "-p", "mumble-web2-proxy"]
command: ["cargo", "run", "-p", "mumble-web2-proxy", "--locked"]
network_mode: host
mumble-server:
+38 -34
View File
@@ -7,12 +7,12 @@ edition = "2021"
# Web Dependencies
# ================
dioxus-web = { version = "0.6.3", optional = true }
wasm-bindgen = { version = "0.2.92", optional = true }
wasm-bindgen-futures = { version = "0.4.42", optional = true }
wasm-streams = { version = "0.4.0", optional = true }
serde-wasm-bindgen = { version = "0.6.5", optional = true }
js-sys = { version = "0.3.70", optional = true }
web-sys = { version = "0.3.72", features = [
wasm-bindgen = { version = "^0.2.92", optional = true }
wasm-bindgen-futures = { version = "^0.4.42", optional = true }
wasm-streams = { version = "^0.4.0", optional = true }
serde-wasm-bindgen = { version = "^0.6.5", optional = true }
js-sys = { version = "^0.3.70", optional = true }
web-sys = { version = "^0.3.72", features = [
"WebTransport",
"console",
"WebTransportOptions",
@@ -54,14 +54,14 @@ web-sys = { version = "0.3.72", features = [
"AudioSampleFormat",
"Storage",
], optional = true }
gloo-timers = { version = "0.3.0", features = ["futures"], optional = true }
tracing-web = { version = "0.1.3", optional = true }
gloo-timers = { version = "^0.3.0", features = ["futures"], optional = true }
tracing-web = { version = "^0.1.3", optional = true }
# Desktop Dependecies
# ===================
dioxus-desktop = { version = "0.6.3", optional = true }
tokio = { version = "1.41.1", features = ["net", "rt"], optional = true }
tokio-rustls = { version = "0.26.0", optional = true }
tokio = { version = "^1.41.1", features = ["net", "rt"], optional = true }
tokio-rustls = { version = "^0.26.0", optional = true }
opus = { version = "0.3.0", optional = true }
cpal = { version = "0.15.3", optional = true }
dasp_ring_buffer = { version = "0.11.0", optional = true }
@@ -71,38 +71,41 @@ dasp_ring_buffer = { version = "0.11.0", optional = true }
dioxus = { version = "0.6.3" }
once_cell = "1.19.0"
asynchronous-codec = { workspace = true }
futures = "0.3.30"
merge-io = "0.3.0"
futures = "^0.3.30"
merge-io = "^0.3.0"
mumble-protocol = { workspace = true }
serde_json = "1.0.117"
tokio-util = { version = "0.7.11", features = ["codec", "compat"] }
byteorder = "1.5.0"
ogg = "0.9.1"
ordermap = "0.5.3"
html-purifier = "0.3.0"
markdown = "0.3.0"
futures-channel = "0.3.30"
sir = { git = "https://gitlab.com/samsartor/sir", features = [
"dioxus",
] } # dioxus 0.6
serde_json = "1"
tokio-util = { version = "^0.7.11", features = ["codec", "compat"] }
byteorder = "1.5"
ogg = "^0.9.1"
ordermap = "^0.5.3"
html-purifier = "^0.3.0"
markdown = "^0.3.0"
futures-channel = "^0.3.30"
mumble-web2-common = { workspace = true }
serde = { workspace = true }
tracing-subscriber = { version = "0.3.18", features = ["ansi"] }
tracing = "0.1.40"
color-eyre = "0.6.3"
crossbeam-queue = "0.3.11"
lol_html = "2.2.0"
rfd = "0.15.2"
base64 = "0.22"
mime_guess = "2.0.5"
async_cell = "0.2.3"
reqwest = { version = "0.12.22", features = ["json"] }
tracing-subscriber = { version = "^0.3.18", features = ["ansi"] }
tracing = "^0.1.40"
color-eyre = "^0.6.3"
crossbeam-queue = "^0.3.11"
lol_html = "^2.2.0"
rfd = "^0.15.2"
base64 = "^0.22"
mime_guess = "^2.0.5"
async_cell = "^0.2.3"
reqwest = { version = "^0.12.22", features = ["json"] }
# Denoising
# =========
deep_filter = { git = "https://github.com/Rikorose/DeepFilterNet.git", rev = "d375b2d8309e0935d165700c91da9de862a99c31" }
deep_filter = { git = "https://github.com/Rikorose/DeepFilterNet.git", rev = "d375b2d8309e0935d165700c91da9de862a99c31", features = ["tract"] }
crossbeam = "0.8.4"
[patch.crates-io]
tract-hir = "=0.12.4"
tract-core = "=0.12.4"
tract-onnx = "=0.12.4"
tract-pulse = "=0.12.4"
[features]
web = [
"dioxus/web",
@@ -115,6 +118,7 @@ web = [
"web-sys",
"gloo-timers",
"tracing-web",
"deep_filter/wasm",
]
desktop = [
"dioxus/desktop",
+293
View File
@@ -0,0 +1,293 @@
:root {
--txt-color: oklch(0.9 0 99);
--bg-color: oklch(0.15 0.01 338.64);
--light-bg-color: oklch(0.25 0.01 338.64);
--login-bg-color: #5d7680;
--primary-btn-color: #7bad9f;
--accent-normal: #7bad9f;
--accent-muted: #ff746c;
--accent-deafened: #464459;
--line-width: 2px;
--line-color: white;
}
body {
margin: 0;
}
#main {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-around;
background-color: var(--bg-color);
overflow: auto;
color: var(--txt-color);
font-family: Nunito;
font-size: 15pt;
font-weight: 600;
}
button {
font-weight: bold;
font-size: medium;
border: none;
border-radius: 4px;
color: var(--txt-color);
background-color: var(--primary-btn-color);
cursor: pointer;
}
input {
border: none;
border-radius: 4px;
background-color: white;
color: black;
}
input:focus,
input:focus-visible {
border: none;
outline: solid var(--line-width) var(--accent-normal);
outline-offset: -3px;
}
a:link {
color: var(--accent-normal);
}
a:visited {
color: var(--accent-muted);
}
.userpil {
border-radius: 100px;
padding: 4px 8px;
width: fit-content;
img {
height: 1em;
vertical-align: text-bottom;
}
&.is_self {
font-weight: bolder;
}
}
.channel {
&_details {
flex: 0 0 100%;
summary {
cursor: pointer;
}
summary:focus-visible {
outline: none;
}
}
&_children {
border-left: solid var(--line-color) var(--line-width);
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
margin-left: 5px;
padding-left: 11px;
padding-top: 4px;
}
}
.chat {
&_panel {
display: flex;
flex-direction: column;
}
&_history {
overflow-y: auto;
flex: 1 0 0;
}
&_message {
display: flex;
flex-direction: row;
margin: 16px;
gap: 8px;
align-items: center;
}
&_box_wrapper {
padding: 16px;
border-top: solid var(--line-color) var(--line-width);
}
&_box {
display: flex;
flex-direction: row;
gap: 16px;
background-color: var(--light-bg-color);
padding-top: 16px;
padding-bottom: 16px;
padding-left: 8px;
padding-right: 16px;
border-radius: 8px;
input {
color: white;
background-color: var(--light-bg-color);
font-size: larger;
flex-grow: 1;
border: none;
}
input:focus {
outline: none;
}
}
}
.user_edit_button {
background-color: oklch(0.53 0.1431 264.18);
border-radius: 50%;
aspect-ratio: 1 / 1;
}
.button_row {
display: flex;
gap: 10px;
.spacer {
flex-grow: 1;
}
}
.toggle_button {
padding: 8px;
height: 100%;
aspect-ratio: 1 / 1;
background-color: unset;
border: solid rgb(255 255 255 / 0.1) 3px;
border-radius: 10px;
color: rgb(255 255 255 / 50%);
transition: all 0.5s ease-in-out;
&.is_on {
background-color: oklch(0.5 0.1381 21.71 / 20.12%);
color: oklch(0.53 0.1505 21.71 / 89.38%);
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
vertical-align: middle;
font-size: 35px;
}
}
.server {
&_grid {
display: grid;
height: 100%;
background-color: var(--bg-color);
grid-template-rows: 1fr auto;
grid-template-columns: 1fr 2fr;
grid-template-areas:
"tree chat"
"control chat";
@media screen and (max-width: 720px) {
grid-template-rows: auto 1fr 1fr;
grid-template-columns: 1fr;
grid-template-areas:
"tree"
"control"
"chat";
}
}
&_channel_box {
padding: 16px;
overflow: auto;
grid-area: tree;
}
&_chat_box {
display: flex;
flex-direction: row;
grid-area: chat;
border-left: solid var(--line-color) var(--line-width);
@media screen and (max-width: 720px) {
border-left: unset;
border-top: solid var(--line-color) var(--line-width);
}
}
&_control_box {
padding: 16px;
margin: 16px;
background-color: var(--light-bg-color);
border-radius: 10px;
overflow: hidden;
grid-area: control;
display: flex;
gap: 10px;
flex-direction: column;
}
}
.login {
max-width: 50vw;
align-self: center;
padding: 32px;
border-radius: 16px;
background-color: var(--login-bg-color);
display: flex;
flex-direction: column;
gap: 16px;
input,
button {
padding: 8px;
}
h1 {
margin: 0;
color: #b3c6b4;
}
&_bttn {
font-weight: bold;
font-size: large;
}
&_error {
background-color: white;
border-radius: 4px;
overflow: auto;
padding: 4px;
color: red;
pre {
color: black;
}
}
}
+38
View File
@@ -0,0 +1,38 @@
use std::path::Path;
use std::process::Command;
fn main() {
// Define the target directory and file
let assets_dir = "assets";
let target_file = format!("{}/DeepFilterNet3_ll_onnx.tar.gz", assets_dir);
let target_path = Path::new(&target_file);
// Check if the file already exists
if target_path.exists() {
println!("cargo:warning=DeepFilterNet model already exists at {}", target_file);
return;
}
println!("cargo:warning=Downloading DeepFilterNet model to {}...", target_file);
// Download the file using curl
let url = "https://github.com/Rikorose/DeepFilterNet/raw/refs/heads/main/models/DeepFilterNet3_ll_onnx.tar.gz";
let status = Command::new("curl")
.args([
"-L", // Follow redirects
"-o", &target_file, // Output file
url,
])
.status()
.expect("Failed to execute curl command. Make sure curl is installed.");
if !status.success() {
panic!("Failed to download DeepFilterNet model from {}", url);
}
println!("cargo:warning=Successfully downloaded DeepFilterNet model to {}", target_file);
// Rerun this build script if the target file is deleted
println!("cargo:rerun-if-changed={}", target_file);
}
+46 -429
View File
@@ -4,7 +4,6 @@ use dioxus::prelude::*;
use mime_guess::Mime;
use mumble_web2_common::{ClientConfig, ServerStatus};
use ordermap::OrderSet;
use sir::{css, global_css};
use std::collections::HashMap;
use crate::imp;
@@ -138,25 +137,6 @@ impl UserIcon {
#[component]
pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element {
let pill = css!(
"
border-radius: 100px;
padding: 4px 8px;
width: fit-content;
img {
height: 1em;
vertical-align: text-bottom;
}
"
);
let pill_self = css!(
"
font-weight: bolder;
"
);
let color = match icon {
UserIcon::Normal => "var(--accent-normal)",
UserIcon::Muted => "var(--accent-muted)",
@@ -166,7 +146,7 @@ pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element {
rsx!(
div {
class: match isself { true => format!("{pill} {pill_self}"), false => format!("{pill}") },
class: match isself { true => "userpil is_self", false => "userpil" },
style: "background-color: {color}",
{ icon.url().map(|url| rsx!(img { src: url })) }
"\u{00A0}{name}\u{00A0}"
@@ -200,34 +180,9 @@ pub fn Channel(id: ChannelId) -> Element {
return rsx!("missing channel {id}");
};
let channel_details = css!(
"
flex: 0 0 100%;
summary {
cursor: pointer;
}
summary:focus-visible {
outline: none;
}
"
);
let channel_children = css!(
"
border-left: solid var(--line-color) var(--line-width);
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
margin-left: 5px;
padding-left: 11px;
padding-top: 4px; "
);
rsx!(
details {
class: "{channel_details}",
class: "channel_details",
open: true,
summary {
span {
@@ -242,7 +197,7 @@ pub fn Channel(id: ChannelId) -> Element {
}
if state.users.len() + state.children.len() > 0 {
div {
class: "{channel_children}",
class: "channel_children",
for id in state.users.iter() {
User { id: *id }
}
@@ -283,71 +238,6 @@ pub fn ChatView() -> Element {
let server = STATE.server.read();
let mut draft = use_signal(|| "".to_string());
let chat_panel = css!(
"
display: flex;
flex-direction: column;
"
);
let chat_history = css!(
"
overflow-y: auto;
flex: 1 0 0;
"
);
let chat_message = css!(
"
display: flex;
flex-direction: row;
margin: 16px;
gap: 8px;
align-items: center;
"
);
let chat_box_wrapper = css!(
"
padding: 16px;
border-top: solid var(--line-color) var(--line-width);
"
);
let chat_box = css!(
"
display: flex;
flex-direction: row;
gap: 16px;
background-color: var(--light-bg-color);
padding-top: 16px;
padding-bottom: 16px;
padding-left: 8px;
padding-right: 16px;
border-radius: 8px;
input {
color: white;
background-color: var(--light-bg-color);
font-size: larger;
flex-grow: 1;
border: none;
}
input:focus {
outline: none;
}
"
);
let mut do_send = move || {
if let Some(user) = STATE.server.read().this_user() {
net.send(SendChat {
@@ -359,12 +249,12 @@ pub fn ChatView() -> Element {
rsx!(
div {
class: "{chat_panel}",
class: "chat_panel",
div {
class: "{chat_history}",
class: "chat_history",
for chat in server.chat.iter() {
div {
class: "{chat_message}",
class: "chat_message",
if let Some(sender) = chat.sender.and_then(|u| server.users.get(&u)) {
UserPill {
name: sender.name.clone(),
@@ -379,9 +269,9 @@ pub fn ChatView() -> Element {
}
}
div {
class: "{chat_box_wrapper}",
class: "chat_box_wrapper",
div {
class: "{chat_box}",
class: "chat_box",
input {
placeholder: "say something",
value: "{draft.read()}",
@@ -418,25 +308,6 @@ pub fn ChatView() -> Element {
)
}
// true => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "mic_off"}),
//Connecting => rsx! {
// div {
// class: "{connecting_status}",
// span {
// class: "material-symbols-outlined",
// style: "vertical-align: middle; font-size: 30px;",
// "signal_cellular_alt_2_bar"
// }
// span {
// style: "width: 5px; display: inline-block;"
// }
// span {
// style: "vertical-align: middle; font-size: 30px;",
// "Connecting"
// }
// }
//},
#[component]
pub fn ControlView(config: Resource<ClientConfig>) -> Element {
let net: Coroutine<Command> = use_coroutine_handle();
@@ -462,99 +333,15 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
.as_ref()
.and_then(|gui_config| gui_config.proxy_url.clone());
let button_row = css!(
r#"
display: flex;
gap: 10px;
"#
);
let spacer = css!(
r#"
flex-grow: 1;
"#
);
let toggle_button = css!(
r#"
padding: 8px;
height: 100%;
aspect-ratio: 1 / 1;
background-color: unset;
border: solid rgb(255 255 255 / 0.1) 3px;
border-radius: 10px;
color: rgb(255 255 255 / 50%);
transition: all 0.5s ease-in-out;
"#
);
let toggle_button_on = css!(
r#"
padding: 8px;
height: 100%;
aspect-ratio: 1 / 1;
background-color: oklch(0.5 0.1381 21.71 / 20.12%);
border: solid rgb(255 255 255 / 0) 3px;
border-radius: 10px;
color: oklch(0.53 0.1505 21.71 / 89.38%);
transition: all 0.25s ease-in-out;
"#
);
let button_style = r#"
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
vertical-align: middle;
font-size: 35px;
"#;
let connecting_status = css!(
r#"
color: yellow;
"#
);
let connected_status = css!(
r#"
color: oklch(0.55 0.1184 141.35);
"#
);
let disconnected_status = css!(
r#"
color: gray;
"#
);
let failed_status = css!(
r#"
color: red;
"#
);
let connection_info = css!(
r#"
color: gray;
"#
);
let user_edit_button = css!(
r#"
background-color: oklch(0.53 0.1431 264.18);
border-radius: 50%;
aspect-ratio: 1 / 1;
"#
);
let connecting_color = "yellow";
let connected_color = "oklch(0.55 0.1184 141.35)";
let disconnected_color = "gray";
let failed_color = "red";
let connection_status = match &*status.read() {
Connecting => rsx! {
div {
class: "{connecting_status}",
style: "color: \"{connecting_color}\";",
span {
class: "material-symbols-outlined",
style: "vertical-align: middle; font-size: 30px;",
@@ -572,7 +359,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
Connected => rsx! {
div {
div {
class: "{connected_status}",
style: "color: \"{connected_color}\";",
span {
class: "material-symbols-outlined",
style: "vertical-align: middle; font-size: 30px;",
@@ -587,7 +374,6 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
}
}
div {
class: "{connection_info}",
span { style: "width: 3px; display: inline-block;"}
span { "{current_channel_name}" }
if let Some(proxy_url) = proxy_url {
@@ -599,7 +385,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
},
Disconnected => rsx! {
div {
class: "{disconnected_status}",
style: "color: \"{disconnected_color}\";",
span {
class: "material-symbols-outlined",
style: "vertical-align: middle;",
@@ -616,7 +402,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
},
Failed(_) => rsx! {
div {
class: "{failed_status}",
style: "color: \"{failed_color}\";",
span {
class: "material-symbols-outlined",
style: "vertical-align: middle;",
@@ -637,17 +423,16 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
rsx!(
// Server control
div {
class: "{button_row}",
class: "button_row",
div {
{connection_status}
}
span { class: "{spacer}" }
span { class: "spacer" }
button {
class: "{toggle_button}",
class: "toggle_button",
onclick: move |_| net.send(Disconnect),
span {
class: "material-symbols-outlined",
style: "{button_style}",
"signal_disconnected"
}
}
@@ -655,9 +440,9 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
hr { style: "width: 100%;" }
// User control
div {
class: "{button_row}",
class: "button_row",
button {
class: "{user_edit_button}",
class: "user_edit_button",
span {
class: "material-symbols-outlined",
style: "color: oklch(0.65 0.2245 28.06); font-size: 45px; font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;",
@@ -672,11 +457,11 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
span { style: "font-size: 20px; color: gray;", "some data" }
}
}
span { class: "{spacer}" }
span { class: "spacer" }
button {
class: match denoise() {
true => toggle_button_on,
false => toggle_button,
true => "toggle_button is_on",
false => "toggle_button",
},
role: "switch",
aria_checked: denoise(),
@@ -686,36 +471,36 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
net.send(UpdateMicEffects { denoise: new_denoise })
},
match denoise() {
true => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "cadence"}),
false => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "graphic_eq"}),
true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
}
}
button {
class: match mute || self_mute {
true => toggle_button_on,
false => toggle_button,
true => "toggle_button is_on",
false => "toggle_button",
},
role: "switch",
aria_checked: mute || self_mute,
disabled: mute,
onclick: move |_| net.send(SetMute { mute: !self_mute }),
match mute || self_mute {
true => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "mic_off"}),
false => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "mic"}),
true => rsx!(span { class: "material-symbols-outlined", "mic_off"}),
false => rsx!(span { class: "material-symbols-outlined", "mic"}),
}
}
button {
class: match deaf || self_deaf {
true => toggle_button_on,
false => toggle_button,
true => "toggle_button in_on",
false => "toggle_button",
},
role: "switch",
aria_checked: deaf || self_deaf,
disabled: deaf,
onclick: move |_| net.send(SetDeaf { deaf: !self_deaf }),
match deaf || self_deaf {
true => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "volume_off"}),
false => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "volume_up"}),
true => rsx!(span { class: "material-symbols-outlined", "volume_off"}),
false => rsx!(span { class: "material-symbols-outlined", "volume_up"}),
}
}
}
@@ -737,71 +522,11 @@ pub fn ServerView(config: Resource<ClientConfig>) -> Element {
return rsx!();
};
let grid = css!(
r#"
display: grid;
height: 100%;
background-color: var(--bg-color);
grid-template-rows: 1fr auto;
grid-template-columns: 1fr 2fr;
grid-template-areas:
"tree chat"
"control chat";
@media screen and (max-width: 720px) {
grid-template-rows: auto 1fr 1fr;
grid-template-columns: 1fr;
grid-template-areas:
"tree"
"control"
"chat";
}
"#
);
let channel_box = css!(
"
padding: 16px;
overflow: auto;
grid-area: tree;
"
);
let chat_box = css!(
"
display: flex;
flex-direction: row;
grid-area: chat;
border-left: solid var(--line-color) var(--line-width);
@media screen and (max-width: 720px) {
border-left:unset;
border-top: solid var(--line-color) var(--line-width);
}
"
);
let control_box = css!(
"
padding: 16px;
margin: 16px;
background-color: var(--light-bg-color);
border-radius: 10px;
overflow: hidden;
grid-area: control;
display: flex;
gap: 10px;
flex-direction: column;
"
);
rsx!(
div {
class: "{grid}",
class: "server_grid",
div {
class: "{channel_box}",
class: "server_channel_box",
for (id, state) in server.channels.iter() {
if state.parent.is_none() {
Channel { id: *id }
@@ -809,11 +534,11 @@ pub fn ServerView(config: Resource<ClientConfig>) -> Element {
}
}
div {
class: "{chat_box}",
class: "server_chat_box",
ChatView {}
}
div {
class: "{control_box}",
class: "server_control_box",
ControlView { config }
}
}
@@ -865,49 +590,6 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
let previous_username = imp::load_username();
let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
let error = css!(
"
background-color: white;
border-radius: 4px;
overflow: auto;
padding: 4px;
color: red;
pre {
color: black;
}
"
);
let login_box = css!(
"
max-width: 50vw;
align-self: center;
padding: 32px;
border-radius: 16px;
background-color: var(--login-bg-color);
display: flex;
flex-direction: column;
gap: 16px;
input,button {
padding: 8px;
}
h1 {
margin: 0;
color: #b3c6b4;
}
"
);
let bttn = css!(
"
font-weight: bold;
font-size: large;
"
);
let do_connect = move |_| {
//let _ = set_default_username(&username.read());
let _ = imp::set_default_username(&username.read());
@@ -921,25 +603,25 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
let bottom = match &*status.read() {
Disconnected => rsx! {
button {
class: "{bttn}",
class: "login_bttn",
onclick: do_connect.clone(),
"Connect"
}
},
Connecting => rsx! {
div {
class: "{bttn}",
class: "login_bttn",
"Connecting..."
}
},
Failed(msg) => rsx!(
button {
class: "{bttn}",
class: "login_bttn",
onclick: do_connect.clone(),
"Reconnect"
}
div {
class: "{error}",
class: "login_error",
"Failed to connect:"
pre {
"{msg}"
@@ -950,7 +632,7 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
};
rsx!(
div {
class: "{login_box}",
class: "login",
h1 {
"Mumble Web"
}
@@ -1025,6 +707,8 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
}
pub fn app() -> Element {
static STYLE: Asset = asset!("/assets/main.scss");
use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx));
let config = use_resource(|| async move {
match imp::load_config().await {
@@ -1033,78 +717,11 @@ pub fn app() -> Element {
}
});
global_css!(
"
:root {
--txt-color: oklch(0.9 0 99);
--bg-color: oklch(0.15 0.01 338.64);
--light-bg-color: oklch(0.25 0.01 338.64);
--login-bg-color: #5d7680;
--primary-btn-color: #7bad9f;
--accent-normal: #7bad9f;
--accent-muted: #ff746c;
--accent-deafened: #464459;
--line-width: 2px;
--line-color: white;
}
body {
margin: 0;
}
#main {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-around;
background-color: var(--bg-color);
overflow: auto;
color: var(--txt-color);
font-family: Nunito;
font-size: 15pt;
font-weight: 600;
}
button {
font-weight: bold;
font-size: medium;
border: none;
border-radius: 4px;
color: var(--txt-color);
background-color: var(--primary-btn-color);
cursor: pointer;
}
input {
border: none;
border-radius: 4px;
background-color: white;
color: black;
}
input:focus,input:focus-visible {
border: none;
outline: solid var(--line-width) var(--accent-normal);
outline-offset: -3px;
}
a:link {
color: var(--accent-normal);
}
a:visited {
color: var(--accent-muted);
}
"
);
rsx!(
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" }
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" }
document::Link{ rel: "stylesheet", href: STYLE }
sir::AppStyle { }
match *STATE.status.read() {
Connected => rsx!(ServerView { config }),
_ => rsx!(LoginView { config }),
+118 -9
View File
@@ -1,25 +1,134 @@
use crossbeam::atomic::AtomicCell;
use df::tract::{mut_slice_as_arrayviewmut, slice_as_arrayview};
use df::tract::{DfParams, DfTract, RuntimeParams};
use dioxus::prelude::{asset, manganis, Asset};
use std::cell::RefCell;
use std::sync::Arc;
use tracing::{error, info};
use crate::imp;
static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz");
enum DenoisingModelState {
Nothing,
Downloading(Arc<AtomicCell<Option<DfParams>>>),
Availible(Box<DfTract>),
}
fn with_denoising_model<O>(
spawn: &imp::SpawnHandle,
func: impl FnOnce(&mut DfTract) -> O,
) -> Option<O> {
// Using a thread local is super gross, but DfTract is not Send (so it can never leave the current
// thread) while AudioProcessing itself might change threads whenever.
thread_local! {
static STATE: RefCell<DenoisingModelState> = const { RefCell::new(DenoisingModelState::Nothing) };
}
STATE.with_borrow_mut(|state| match state {
DenoisingModelState::Nothing => {
let cell = Arc::new(AtomicCell::new(None));
let cell_task = cell.clone();
*state = DenoisingModelState::Downloading(cell);
spawn.spawn(async move {
let model_bytes = match imp::read_asset_bytes(&DF_MODEL).await {
Ok(b) => b,
Err(e) => {
error!("could not read denoising model from \"{DF_MODEL}\": {e:?}");
return;
}
};
let params = match DfParams::from_bytes(&model_bytes) {
Ok(p) => p,
Err(e) => {
error!("could not load denoising model parameters: {e:?}");
return;
}
};
cell_task.store(Some(params));
});
None
}
DenoisingModelState::Downloading(cell) => {
if let Some(params) = cell.take() {
let mut tract = match DfTract::new(params, &RuntimeParams::default_with_ch(1)) {
Ok(t) => Box::new(t),
Err(e) => {
error!("could not create denoising engine: {e:?}");
return None;
}
};
info!("instantiated denoising engine");
let out = func(&mut tract);
*state = DenoisingModelState::Availible(tract);
Some(out)
} else {
None
}
}
DenoisingModelState::Availible(tract) => Some(func(tract)),
})
}
#[derive(Default)]
pub struct AudioProcessor {
df: Option<::df::DFState>,
denoise: bool,
spawn: imp::SpawnHandle,
buffer: Vec<f32>,
}
impl AudioProcessor {
pub fn new_plain() -> Self {
AudioProcessor {
denoise: false,
spawn: imp::SpawnHandle::current(),
buffer: Vec::new(),
}
}
pub fn new_denoising() -> Self {
let df = ::df::DFState::default();
AudioProcessor { df: Some(df) }
AudioProcessor {
denoise: true,
spawn: imp::SpawnHandle::current(),
buffer: Vec::new(),
}
}
}
impl AudioProcessor {
pub fn process(&mut self, audio: &[f32], output: &mut Vec<f32>) {
if let Some(df) = &mut self.df {
let start = output.len();
output.extend(std::iter::repeat_n(0f32, audio.len()));
df.process_frame(audio, &mut output[start..]);
} else {
let mut include_raw = true;
if self.denoise {
with_denoising_model(&self.spawn, |df| {
include_raw = false;
self.buffer.extend_from_slice(audio);
output.reserve(audio.len());
let hop = df.hop_size;
let mut i = 0;
while self.buffer[i..].len() >= hop {
let audio = &self.buffer[i..][..hop];
i += audio.len();
let j = output.len();
output.extend(std::iter::repeat_n(0f32, audio.len()));
let output = &mut output[j..];
df.process(
slice_as_arrayview(audio, &[audio.len()])
.into_shape((1, audio.len()))
.unwrap(),
mut_slice_as_arrayviewmut(output, &[output.len()])
.into_shape((1, output.len()))
.unwrap(),
);
}
self.buffer.splice(..i, []);
});
}
if include_raw {
output.extend_from_slice(audio);
}
}
+17 -7
View File
@@ -1,16 +1,15 @@
use crate::app::Command;
use crate::effects::{AudioProcessor, AudioProcessorSender};
use color_eyre::eyre::{eyre, Error};
use color_eyre::eyre::{eyre, Context, Error};
use cpal::traits::{DeviceTrait, HostTrait};
use dioxus::hooks::{UnboundedReceiver, UnboundedSender};
use dioxus::hooks::UnboundedReceiver;
use futures::io::{AsyncRead, AsyncWrite};
use mumble_protocol::control::{ClientControlCodec, ControlPacket};
use mumble_protocol::Serverbound;
use mumble_protocol::control::ClientControlCodec;
use mumble_web2_common::ClientConfig;
use std::mem::replace;
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::sync::Mutex;
use std::{fmt, io, sync::Arc};
use tokio::net::TcpStream;
use tokio_rustls::rustls;
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
@@ -19,8 +18,9 @@ use tokio_rustls::rustls::ClientConfig as RlsClientConfig;
use tokio_rustls::rustls::DigitallySignedStruct;
use tokio_rustls::TlsConnector;
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
use tracing::{debug, error, info, instrument, warn};
use tracing::{error, info, instrument, warn};
pub use tokio::runtime::Handle as SpawnHandle;
pub use tokio::task::spawn;
pub use tokio::time::sleep;
@@ -70,7 +70,7 @@ impl AudioSystem {
) -> Result<(), Error> {
let mut encoder =
opus::Encoder::new(SAMPLE_RATE, opus::Channels::Mono, opus::Application::Voip)?;
let mut current_processor = AudioProcessor::default();
let mut current_processor = AudioProcessor::new_plain();
let mut output_buffer = Vec::new();
let processors = self.processors.clone();
let error_callback = move |e: cpal::StreamError| error!("error recording: {e:?}");
@@ -311,3 +311,13 @@ pub fn init_logging() {
.with_env_filter(env_filter)
.init();
}
// TODO: once we update to dioxus 0.7, swap this out with the dioxus-asset-resolver crate
pub async fn read_asset_bytes(asset: &dioxus::prelude::Asset) -> color_eyre::Result<Vec<u8>> {
let cur_exe = std::env::current_exe().unwrap();
let path = cur_exe
.parent()
.unwrap()
.join(asset.to_string().trim_matches('/'));
Ok(std::fs::read(&path).with_context(|| format!("native path \"{}\"", path.display()))?)
}
+24 -1
View File
@@ -8,6 +8,7 @@ use js_sys::Float32Array;
use mumble_protocol::control::ClientControlCodec;
use mumble_web2_common::ClientConfig;
use reqwest::Url;
use std::future::Future;
use std::time::Duration;
use tracing::level_filters::LevelFilter;
use tracing::{debug, error, info, instrument};
@@ -283,7 +284,7 @@ async fn run_encoder_worklet(
audio_encoder.configure(&encoder_config);
info!("created audio encoder");
let mut current_processor = AudioProcessor::default();
let mut current_processor = AudioProcessor::new_plain();
let onmessage: Closure<dyn FnMut(MessageEvent)> = Closure::new(move |event: MessageEvent| {
if let Some(new_processor) = processors.take() {
current_processor = new_processor;
@@ -444,3 +445,25 @@ pub fn init_logging() {
info!("logging initiated");
}
// TODO: once we update to dioxus 0.7, swap this out with the dioxus-asset-resolver crate
pub async fn read_asset_bytes(asset: &dioxus::prelude::Asset) -> color_eyre::Result<Vec<u8>> {
let path = asset.to_string();
let path = path.trim_matches('/');
Ok(reqwest::get(path).await?.bytes().await?.to_vec())
}
pub struct SpawnHandle;
impl SpawnHandle {
pub fn current() -> Self {
SpawnHandle
}
pub fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 'static,
{
spawn(future);
}
}
+1 -1
View File
@@ -289,7 +289,7 @@ fn accept_command(
if denoise {
audio.set_processor(AudioProcessor::new_denoising());
} else {
audio.set_processor(AudioProcessor::default());
audio.set_processor(AudioProcessor::new_plain());
}
}
}
+2 -2
View File
@@ -7,13 +7,13 @@ edition = "2021"
color-eyre = "^0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "^1.37", features = ["full"] }
tokio = { version = "1", features = ["full"] }
tokio-rustls = "0.26"
toml = "0.8"
tracing = { version = "^0.1.40", features = ["async-await"] }
tracing-subscriber = { version = "^0.3.18", features = ["env-filter"] }
mumble-web2-common = { workspace = true }
salvo = { version = "^0.74.2", features = [
salvo = { version = "^0.84.2", features = [
"quinn",
"eyre",
"rustls",