From 411d923c2aa78a8410590224a491578f4d4ecf79 Mon Sep 17 00:00:00 2001 From: Sam Sartor Date: Sat, 24 Jan 2026 20:16:02 -0700 Subject: [PATCH] wip improved trait shit --- gui/src/app.rs | 18 ++--- gui/src/effects.rs | 17 ++--- gui/src/imp/desktop.rs | 7 +- gui/src/imp/mobile.rs | 4 +- gui/src/imp/mod.rs | 141 ++++++++++++------------------------ gui/src/imp/native_audio.rs | 3 +- gui/src/imp/web.rs | 84 ++++++++------------- gui/src/lib.rs | 21 ++++-- gui/src/main.rs | 4 +- 9 files changed, 118 insertions(+), 181 deletions(-) diff --git a/gui/src/app.rs b/gui/src/app.rs index d60dad1..a56e647 100644 --- a/gui/src/app.rs +++ b/gui/src/app.rs @@ -6,7 +6,7 @@ use mumble_web2_common::{ClientConfig, ServerStatus}; use ordermap::OrderSet; use std::collections::{HashMap, HashSet}; -use crate::imp; +use crate::imp::{Platform, PlatformInterface as _}; pub type ChannelId = u32; pub type UserId = u32; @@ -690,12 +690,12 @@ pub fn LoginView(config: Resource) -> Element { use_resource(move || async move { let client = reqwest::Client::new(); loop { - *last_status.write_unchecked() = Some(imp::get_status(&client).await); - imp::sleep(std::time::Duration::from_secs_f32(1.0)).await; + *last_status.write_unchecked() = Some(Platform::get_status(&client).await); + Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await; } }); - let mut address_input = use_signal(|| imp::load_server_url()); + let mut address_input = use_signal(|| Platform::load_server_url()); let address = use_memo(move || { if let Some(addr) = address_input() { addr.clone() @@ -706,14 +706,14 @@ pub fn LoginView(config: Resource) -> Element { } }); - let previous_username = imp::load_username(); + let previous_username = Platform::load_username(); let mut username = use_signal(|| previous_username.unwrap_or(String::new())); let do_connect = move |_| { //let _ = set_default_username(&username.read()); - let _ = imp::set_default_username(&username.read()); + let _ = Platform::set_default_username(&username.read()); if config.read().as_ref().is_some_and(|cfg| cfg.any_server) { - imp::set_default_server(&address.read()); + Platform::set_default_server(&address.read()); } net.send(Connect { address: address.read().clone(), @@ -860,13 +860,13 @@ pub fn app() -> Element { use_coroutine(|rx: UnboundedReceiver| super::network_entrypoint(rx)); let config = use_resource(|| async move { - match imp::load_config().await { + match Platform::load_config().await { Ok(config) => config, Err(_) => ClientConfig::default(), } }); - imp::request_permissions(); + Platform::request_permissions(); rsx!( document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" } diff --git a/gui/src/effects.rs b/gui/src/effects.rs index 64c9fe3..aef2713 100644 --- a/gui/src/effects.rs +++ b/gui/src/effects.rs @@ -7,10 +7,7 @@ use std::cell::RefCell; use std::sync::Arc; use tracing::{error, info}; -use crate::imp::{CurrentPlatform, PlatformRuntime, SpawnHandleTrait}; - -/// The spawn handle type for the current platform. -type SpawnHandle = ::SpawnHandle; +use crate::imp::{SpawnHandle, SpawnHandleInterface as _}; static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz"); // TODO: make this user configurable. @@ -35,10 +32,7 @@ enum DenoisingModelState { Availible(Box), } -fn with_denoising_model( - spawn: &SpawnHandle, - func: impl FnOnce(&mut DfTract) -> O, -) -> Option { +fn with_denoising_model(spawn: &SpawnHandle, func: impl FnOnce(&mut DfTract) -> O) -> Option { // 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! { @@ -126,7 +120,12 @@ impl AudioProcessor { } impl AudioProcessor { - pub fn process(&mut self, audio: &[f32], channels: usize, output: &mut Vec) -> TransmitState { + pub fn process( + &mut self, + audio: &[f32], + channels: usize, + output: &mut Vec, + ) -> TransmitState { let mut include_raw = true; if self.denoise { with_denoising_model(&self.spawn, |df| { diff --git a/gui/src/imp/desktop.rs b/gui/src/imp/desktop.rs index 5c49df6..460769d 100644 --- a/gui/src/imp/desktop.rs +++ b/gui/src/imp/desktop.rs @@ -7,7 +7,10 @@ use std::collections::HashMap; use std::future::Future; use std::time::Duration; -use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; +use super::{ + PlatformConfig, PlatformInit, PlatformInterface, PlatformNetwork, PlatformRuntime, + SpawnHandleTrait, +}; pub use super::connect::*; pub use super::native_audio::*; @@ -107,7 +110,7 @@ impl PlatformInit for DesktopPlatform { } } -impl Platform for DesktopPlatform {} +impl PlatformInterface for DesktopPlatform {} fn get_config_path() -> std::path::PathBuf { let strategy = choose_app_strategy(AppStrategyArgs { diff --git a/gui/src/imp/mobile.rs b/gui/src/imp/mobile.rs index b32a409..31a4da5 100644 --- a/gui/src/imp/mobile.rs +++ b/gui/src/imp/mobile.rs @@ -7,7 +7,7 @@ use mumble_web2_common::{ClientConfig, ServerStatus}; use std::future::Future; use std::time::Duration; -use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; +use super::{PlatformInterface, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; pub use super::connect::*; pub use super::native_audio::*; @@ -107,7 +107,7 @@ impl PlatformInit for MobilePlatform { } } -impl Platform for MobilePlatform {} +impl PlatformInterface for MobilePlatform {} pub fn set_default_username(_username: &str) -> Option<()> { None diff --git a/gui/src/imp/mod.rs b/gui/src/imp/mod.rs index ae20e12..44b29ba 100644 --- a/gui/src/imp/mod.rs +++ b/gui/src/imp/mod.rs @@ -15,8 +15,7 @@ use std::time::Duration; // ============================================================================ /// Trait for spawn handles that can be stored and used to spawn tasks later. -#[cfg(feature = "web")] -pub trait SpawnHandleTrait: Clone + 'static { +pub trait SpawnHandleInterface: Clone + 'static { /// Spawn an async task using this handle. fn spawn(&self, future: F) where @@ -26,51 +25,39 @@ pub trait SpawnHandleTrait: Clone + 'static { fn current() -> Self; } -/// Trait for spawn handles that can be stored and used to spawn tasks later. -#[cfg(any(feature = "desktop", feature = "mobile"))] -pub trait SpawnHandleTrait: Clone + 'static { - /// Spawn an async task using this handle. - fn spawn(&self, future: F) - where - F: Future + Send + 'static; - - /// Get a spawn handle for the current context. - fn current() -> Self; +pub trait AudioSystemInterface { + type AudioPlayer: AudioPlayerInterface; } -/// Runtime primitives: task spawning and async sleep. -#[cfg(feature = "web")] -pub trait PlatformRuntime { - /// The spawn handle type for this platform. - type SpawnHandle: SpawnHandleTrait; +pub trait AudioPlayerInterface {} - /// Spawn an async task. - fn spawn(future: F) - where - F: Future + 'static; +/// This is the main trait that each platform must implement. It combines all +/// platform-specific functionality into a single interface, providing compile-time +/// verification that all platforms implement the required functionality. +pub trait PlatformInterface { + type AudioSystem: AudioSystemInterface; + type SpawnHandle: SpawnHandleInterface; - /// Async sleep for the given duration. - fn sleep(duration: Duration) -> impl Future; -} + /// Initialize logging for the platform. + fn init_logging(); -/// Runtime primitives: task spawning and async sleep. -#[cfg(any(feature = "desktop", feature = "mobile"))] -pub trait PlatformRuntime { - /// The spawn handle type for this platform. - type SpawnHandle: SpawnHandleTrait; + /// Request runtime permissions (Android audio recording, etc.). + fn request_permissions(); - /// Spawn an async task. - fn spawn(future: F) - where - F: Future + Send + 'static; + /// Establish a connection to the Mumble server and run the network loop. + fn network_connect( + address: String, + username: String, + event_rx: &mut UnboundedReceiver, + gui_config: &ClientConfig, + ) -> impl Future>; - /// Async sleep for the given duration. - fn sleep(duration: Duration) -> impl Future; -} + /// Get server status (user count, version, etc.). + fn get_status( + client: &reqwest::Client, + ) -> impl Future>; -/// Configuration persistence: loading and saving user preferences. -pub trait PlatformConfig { - /// Load the client configuration (proxy URL, cert hash, etc.). + /// Load the proxy overrides (proxy URL, cert hash, etc.). fn load_config() -> impl Future>; /// Load saved username. @@ -84,39 +71,16 @@ pub trait PlatformConfig { /// Save the default server URL. fn set_default_server(server: &str) -> Option<()>; + + /// Spawn an async task. + fn spawn(future: F) + where + F: Future + 'static; + + /// Async sleep for the given duration. + fn sleep(duration: Duration) -> impl Future; } -/// Network operations: connecting to servers. -pub trait PlatformNetwork { - /// Establish a connection to the Mumble server and run the network loop. - fn network_connect( - address: String, - username: String, - event_rx: &mut UnboundedReceiver, - gui_config: &ClientConfig, - ) -> impl Future>; - - /// Get server status (user count, version, etc.). - fn get_status(client: &reqwest::Client) - -> impl Future>; -} - -/// Platform initialization. -pub trait PlatformInit { - /// Initialize logging for the platform. - fn init_logging(); - - /// Request runtime permissions (Android audio recording, etc.). - fn request_permissions(); -} - -/// Combined platform trait. -/// -/// This is the main trait that each platform must implement. It combines all -/// platform-specific functionality into a single interface, providing compile-time -/// verification that all platforms implement the required functionality. -pub trait Platform: PlatformRuntime + PlatformConfig + PlatformNetwork + PlatformInit {} - // ============================================================================ // Platform Modules // ============================================================================ @@ -138,36 +102,21 @@ mod mobile; // Platform Type Alias // ============================================================================ -/// The current platform type, selected at compile time based on features. #[cfg(feature = "web")] -pub type CurrentPlatform = web::WebPlatform; +pub type Platform = web::WebPlatform; -#[cfg(feature = "desktop")] -pub type CurrentPlatform = desktop::DesktopPlatform; +#[cfg(all(feature = "desktop", not(feature = "web")))] +pub type Platform = desktop::DesktopPlatform; -#[cfg(feature = "mobile")] -pub type CurrentPlatform = mobile::MobilePlatform; +#[cfg(all(feature = "mobile", not(feature = "web"), not(feature = "desktop")))] +pub type Platform = mobile::MobilePlatform; + +pub type AudioSystem = ::AudioSystem; +pub type AudioPlayer = ::AudioPlayer; +pub type SpawnHandle = ::SpawnHandle; /// Compile-time assertion that CurrentPlatform implements Platform. const _: () = { - fn assert_platform() {} - let _ = assert_platform::; + fn assert_platform() {} + let _ = assert_platform::; }; - -// ============================================================================ -// Platform Re-exports -// ============================================================================ - -#[cfg(feature = "desktop")] -pub use desktop::*; -#[cfg(feature = "mobile")] -pub use mobile::*; - -#[cfg(feature = "mobile")] -pub use mobile::request_permissions; - -#[cfg(any(feature = "desktop", feature = "web"))] -pub fn request_permissions() {} - -#[cfg(all(feature = "web", not(any(feature = "desktop", feature = "mobile"))))] -pub use web::*; diff --git a/gui/src/imp/native_audio.rs b/gui/src/imp/native_audio.rs index 3778aac..0ac62f6 100644 --- a/gui/src/imp/native_audio.rs +++ b/gui/src/imp/native_audio.rs @@ -124,7 +124,8 @@ impl AudioSystem { if let Some(new_processor) = processors.take() { current_processor = new_processor; } - let state = current_processor.process(frame, config.channels as usize, &mut output_buffer); + let state = + current_processor.process(frame, config.channels as usize, &mut output_buffer); encode_and_send(state, &mut output_buffer, &mut encoder, &mut each); }; diff --git a/gui/src/imp/web.rs b/gui/src/imp/web.rs index 575d083..4bea43d 100644 --- a/gui/src/imp/web.rs +++ b/gui/src/imp/web.rs @@ -38,8 +38,6 @@ use web_sys::WorkletOptions; use web_sys::{console, window}; use web_sys::{AudioContext, AudioDataCopyToOptions}; -use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; - pub use wasm_bindgen_futures::spawn_local as spawn; pub trait ImpRead: AsyncRead + Unpin + 'static {} @@ -55,15 +53,14 @@ impl ImpWrite for T {} /// Web platform implementation using WebTransport and Web Audio API. pub struct WebPlatform; -pub async fn sleep(d: Duration) { - TimeoutFuture::new(d.as_millis() as u32).await -} - // ============================================================================ // Trait Implementations // ============================================================================ -impl SpawnHandleTrait for SpawnHandle { +#[derive(Clone)] +pub struct WebSpawnHandle; + +impl super::SpawnHandleInterface for WebSpawnHandle { fn spawn(&self, future: F) where F: Future + 'static, @@ -72,26 +69,22 @@ impl SpawnHandleTrait for SpawnHandle { } fn current() -> Self { - SpawnHandle + WebSpawnHandle } } -impl PlatformRuntime for WebPlatform { - type SpawnHandle = SpawnHandle; +impl super::PlatformInterface for WebPlatform { + type AudioSystem = WebAudioSystem; + type SpawnHandle = WebSpawnHandle; - fn spawn(future: F) - where - F: Future + 'static, - { - wasm_bindgen_futures::spawn_local(future); + fn init_logging() { + init_logging(); } - async fn sleep(duration: Duration) { - TimeoutFuture::new(duration.as_millis() as u32).await; + fn request_permissions() { + // No-op on web } -} -impl PlatformConfig for WebPlatform { async fn load_config() -> color_eyre::Result { load_config().await } @@ -111,9 +104,7 @@ impl PlatformConfig for WebPlatform { fn set_default_server(server: &str) -> Option<()> { set_default_server(server) } -} -impl PlatformNetwork for WebPlatform { async fn network_connect( address: String, username: String, @@ -126,20 +117,19 @@ impl PlatformNetwork for WebPlatform { async fn get_status(client: &reqwest::Client) -> color_eyre::Result { get_status(client).await } -} -impl PlatformInit for WebPlatform { - fn init_logging() { - init_logging(); + fn spawn(future: F) + where + F: Future + 'static, + { + wasm_bindgen_futures::spawn_local(future); } - fn request_permissions() { - // No-op on web + async fn sleep(duration: Duration) { + TimeoutFuture::new(duration.as_millis() as u32).await; } } -impl Platform for WebPlatform {} - trait ResultExt { fn ey(self) -> Result; } @@ -162,7 +152,7 @@ impl ResultExt for Result { } } -pub struct AudioSystem { +pub struct WebAudioSystem { webctx: AudioContext, processors: AudioProcessorSender, } @@ -193,7 +183,11 @@ async fn attach_worklet(audio_context: &AudioContext) -> Result<(), Error> { Ok(()) } -impl AudioSystem { +impl super::AudioSystemInterface for WebAudioSystem { + type AudioPlayer = WebAudioPlayer; +} + +impl WebAudioSystem { pub async fn new() -> Result { // Create MediaStreams to playback decoded audio // The audio context is used to reproduce audio. @@ -202,7 +196,7 @@ impl AudioSystem { let processors = AudioProcessorSender::default(); - Ok(AudioSystem { webctx, processors }) + Ok(WebAudioSystem { webctx, processors }) } pub fn set_processor(&self, processor: AudioProcessor) { @@ -224,7 +218,7 @@ impl AudioSystem { Ok(()) } - pub fn create_player(&mut self) -> Result { + pub fn create_player(&mut self) -> Result { let sink_node = AudioWorkletNode::new(&self.webctx, "rust_speaker_worklet").ey()?; // Connect worklet to destination @@ -277,13 +271,15 @@ impl AudioSystem { decoder_error.forget(); output.forget(); - Ok(AudioPlayer(audio_decoder)) + Ok(WebAudioPlayer(audio_decoder)) } } -pub struct AudioPlayer(AudioDecoder); +pub struct WebAudioPlayer(AudioDecoder); -impl AudioPlayer { +impl super::AudioPlayerInterface for WebAudioPlayer {} + +impl WebAudioPlayer { pub fn play_opus(&mut self, payload: &[u8]) { let js_audio_payload = Uint8Array::from(payload); let _ = self.0.decode( @@ -583,19 +579,3 @@ pub fn init_logging() { info!("logging initiated"); } - -#[derive(Clone)] -pub struct SpawnHandle; - -impl SpawnHandle { - pub fn current() -> Self { - SpawnHandle - } - - pub fn spawn(&self, future: F) - where - F: Future + 'static, - { - spawn(future); - } -} diff --git a/gui/src/lib.rs b/gui/src/lib.rs index e37c8a7..a4b5ec1 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -7,11 +7,12 @@ use asynchronous_codec::FramedWrite; use color_eyre::eyre::{bail, Error}; use dioxus::prelude::*; use futures::select; +use futures::AsyncRead; +use futures::AsyncWrite; use futures::FutureExt as _; use futures::SinkExt as _; use futures::StreamExt as _; use futures_channel::mpsc::UnboundedSender; -pub use imp::spawn; use msghtml::process_message_html; use mumble_protocol::control::msgs; use mumble_protocol::control::ControlCodec; @@ -27,7 +28,9 @@ use tracing::error; use tracing::info; use crate::effects::AudioProcessor; -use crate::imp::{AudioSystem, CurrentPlatform, Platform, PlatformNetwork, PlatformRuntime}; +use crate::imp::{ + AudioPlayer, AudioSystem, AudioSystemInterface as _, Platform, PlatformInterface as _, +}; pub mod app; mod effects; @@ -47,7 +50,9 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver) { *STATE.server.write() = Default::default(); *STATE.status.write() = ConnectionState::Connecting; - if let Err(error) = CurrentPlatform::network_connect(address, username, &mut event_rx, &config).await { + if let Err(error) = + Platform::network_connect(address, username, &mut event_rx, &config).await + { error!("could not connect {:?}", error); *STATE.status.write() = ConnectionState::Failed(error.to_string()); } else { @@ -56,7 +61,7 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver) { } } -pub async fn network_loop( +pub async fn network_loop( username: String, event_rx: &mut UnboundedReceiver, mut reader: FramedRead>, @@ -105,12 +110,12 @@ pub async fn network_loop( break; } - CurrentPlatform::sleep(Duration::from_millis(3000)).await; + Platform::sleep(Duration::from_millis(3000)).await; } }); } - let mut audio = imp::AudioSystem::new().await?; + let mut audio = AudioSystem::new().await?; { let send_chan = send_chan.clone(); let mut sequence_num = 0; @@ -296,8 +301,8 @@ fn accept_command( fn accept_packet( msg: ControlPacket, - audio_context: &mut imp::AudioSystem, - player_map: &mut HashMap, + audio_context: &mut AudioSystem, + player_map: &mut HashMap, ) -> Result<(), Error> { match msg { ControlPacket::UDPTunnel(u) => { diff --git a/gui/src/main.rs b/gui/src/main.rs index 0e2f19f..a4185c6 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -1,6 +1,6 @@ -use mumble_web2_gui::{app, imp}; +use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _}; pub fn main() { - imp::init_logging(); + Platform::init_logging(); dioxus::launch(app::app); }