Refactor the imp/gui bondary to use real traits (#18)
Build Mumble Web 2 / linux_build (push) Successful in 1m24s
Build Mumble Web 2 / windows_build (push) Successful in 2m36s
Build Mumble Web 2 / android_build (push) Successful in 5m57s
Build android container / android-release-builder-container-build (push) Successful in -4s
Build Mumble Web 2 release builder containers / windows-release-builder-container-build (push) Successful in 16s

# Summary

Introduces a trait-based platform abstraction layer that makes the boundary
between platform-specific and shared code explicit and compile-time verified.

The TLDR version of this new trait stuff works:

1. Define a `PlatformInterface` trait.
2. Each platform defines a zero-sized struct implementing the trait (ex `WebPlatform`).
3. Create an ifdef'd type alias on those structs:

```rust
#[cfg(feature = "web")]
pub type Platform = web::WebPlatform;

#[cfg(all(feature = "desktop"))]
pub type Platform = desktop::DesktopPlatform;

#[cfg(all(feature = "mobile", not(feature = "web")))]
pub type Platform = mobile::MobilePlatform;
```

5. Add a compile time assertion that `Platform` implements `PlatformInterface`.

#  Motivation

Previously, platform code used a mix of pub use re-exports and #[cfg] blocks
that made it difficult to understand what each platform must implement. The
new trait-based approach provides:

- Clear documentation of the platform contract
- Compile-time verification that all platforms implement required
  functionality
- Ability to cargo check without feature flags (via stub platform)

# Changes

New traits in imp/mod.rs:
  - PlatformInterface - logging, permissions, network, config, storage. Overall this the trait that platforms must satify to compile.
  - AudioSystemInterface - audio system initialization and recording
  - AudioPlayerInterface - opus audio playback

 Type aliases:
  - Platform, AudioSystem, AudioPlayer resolve to the correct types based on
  feature flags

Call site updates:
  - Changed from imp::function() to Platform::function() syntax
  - Removed ImpRead/ImpWrite helper traits in favor of direct bounds

# Testing

Manual testing reveals that Web and Desktop still work, I (Liam) have not tested the mobile version beyond compilation.

Co-authored-by: Liam Warfield <liam.warfield@gmail.com>
Reviewed-on: #18
Co-authored-by: Sam Sartor <me@samsartor.com>
Co-committed-by: Sam Sartor <me@samsartor.com>
This commit was merged in pull request #18.
This commit is contained in:
2026-02-18 04:53:41 +00:00
committed by Liam Warfield
parent 083a11274e
commit 9006a082b0
12 changed files with 621 additions and 292 deletions
+34 -37
View File
@@ -1,19 +1,12 @@
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
use color_eyre::eyre::{eyre, Error};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _};
use futures::io::{AsyncRead, AsyncWrite};
use std::mem::replace;
use std::sync::Arc;
use std::sync::Mutex;
use tracing::{error, info, warn};
pub trait ImpRead: AsyncRead + Unpin + Send + 'static {}
impl<T: AsyncRead + Unpin + Send + 'static> ImpRead for T {}
pub trait ImpWrite: AsyncWrite + Unpin + Send + 'static {}
impl<T: AsyncWrite + Unpin + Send + 'static> ImpWrite for T {}
pub struct AudioSystem {
pub struct NativeAudioSystem {
output: cpal::Device,
input: cpal::Device,
processors: AudioProcessorSender,
@@ -52,28 +45,7 @@ fn encode_and_send(
type Buffer = Arc<Mutex<dasp_ring_buffer::Bounded<Vec<i16>>>>;
impl AudioSystem {
pub async fn new() -> Result<Self, Error> {
// TODO
let host = cpal::default_host();
let name = host.id();
let processors = AudioProcessorSender::default();
Ok(AudioSystem {
output: host
.default_output_device()
.ok_or(eyre!("no output devices from {name:?}"))?,
input: host
.default_input_device()
.ok_or(eyre!("no input devices from {name:?}"))?,
processors,
recording_stream: None,
})
}
pub fn set_processor(&self, processor: AudioProcessor) {
self.processors.store(Some(processor))
}
impl NativeAudioSystem {
fn choose_config(
&self,
configs: impl Iterator<Item = cpal::SupportedStreamConfigRange>,
@@ -103,8 +75,32 @@ impl AudioSystem {
.cloned()
.ok_or(eyre!("no supported stream configs"))
}
}
pub fn start_recording(
impl super::AudioSystemInterface for NativeAudioSystem {
type AudioPlayer = NativeAudioPlayer;
async fn new() -> Result<Self, Error> {
let host = cpal::default_host();
let name = host.id();
let processors = AudioProcessorSender::default();
Ok(NativeAudioSystem {
output: host
.default_output_device()
.ok_or(eyre!("no output devices from {name:?}"))?,
input: host
.default_input_device()
.ok_or(eyre!("no input devices from {name:?}"))?,
processors,
recording_stream: None,
})
}
fn set_processor(&self, processor: AudioProcessor) {
self.processors.store(Some(processor))
}
fn start_recording(
&mut self,
mut each: impl FnMut(Vec<u8>, bool) + Send + 'static,
) -> Result<(), Error> {
@@ -124,7 +120,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);
};
@@ -144,7 +141,7 @@ impl AudioSystem {
}
}
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> {
fn create_player(&mut self) -> Result<NativeAudioPlayer, Error> {
let config = self.choose_config(self.output.supported_output_configs()?)?;
info!(
"creating player on {:?} with {:#?}",
@@ -182,7 +179,7 @@ impl AudioSystem {
)?
};
stream.play()?;
Ok(AudioPlayer {
Ok(NativeAudioPlayer {
decoder,
stream,
buffer,
@@ -191,15 +188,15 @@ impl AudioSystem {
}
}
pub struct AudioPlayer {
pub struct NativeAudioPlayer {
decoder: opus::Decoder,
stream: cpal::Stream,
buffer: Buffer,
tmp: Vec<i16>,
}
impl AudioPlayer {
pub fn play_opus(&mut self, payload: &[u8]) {
impl super::AudioPlayerInterface for NativeAudioPlayer {
fn play_opus(&mut self, payload: &[u8]) {
let len = match self.decoder.decode(payload, &mut self.tmp, false) {
Ok(l) => l,
Err(e) => {