--- ## About This Talk Rust in 2025 Options for cross-platform development Compare a couple of GUI frameworks A disappointing lack of audio code Extra stuff if there's time --- ## About Me - Software Engineer - Born in Bristol, living in Berlin - 2018: Talked about Rust at ADC - ["Introduction to Rust for Audio Developers"](https://www.youtube.com/watch?v=Yom9E-67bdI) - 2019: Ableton ➡️ freelancing - 2020: Started the [Koto](https://koto.dev) project - ... - 2025: Making music software again 🥳 --- ## Quick Rust Refresher - Similar goals to C++ - System level programming - High-level, 'zero-cost' abstractions - ...With a strong emphasis on _safety_ --- ## Safety Lots of safety checks baked into the compiler - Undefined behaviour is verboten - No accidental memory access - No shared mutable state - Thread safety guarantees --- ## Unsafety - Escape hatch when needed with `unsafe` - Safe abstractions built around `unsafe` logic - e.g. `Vec
` uses `unsafe` internally - `unsafe` code is a strong hint when debugging --- ## Great Defaults - Sum types at the core of the language - (`enum` in Rust) - Const-by-default, `mut` is opt-in - `Result`-based error handling --- ## Great Tooling - [`rustup`](https://rustup.rs) - Standard toolchain installer - `cargo` - Standard build tool and package manager - `crates.io` - Standard package repository - `docs.rs` - Documentation for all `crates.io` crates --- ## It's Not Perfect - You'll find your pet things to criticize in the language - (Opt-in 'editions' enable backwards-compatible breaking changes) - Lots of desirable WIP features are nightly-only --- ## Rust Timeline - 2006: Started at Mozilla - 2012: `0.1` - First public release - 2015: `1.0` - First stable release - ... - 2025: `1.91` --- ## Rust in 2025 - Used widely in production - (...in some areas) - Command line tools - Networking - Security - Embedded systems ---  ---  --- ## Are We Gui Yet? [areweguiyet.com](https://areweguiyet.com) > The roots aren't deep but the seeds are planted. --- ## Are We Gui Yet? - UI in Rust has taken a while... - Traditional OOP patterns are possible, although not very ergonomic - Lots of projects trying different directions - [A 2025 Survey of Rust GUI Libraries](https://www.boringcactus.com/2025/04/13/2025-survey-of-rust-gui-libraries.html) notes: - Rust has been an attractive space for research - The xilem project ---  notes: - Argues that what people want are App frameworks, GUI toolkits are only part of the picture ---
`egui` - [rerun.io](https://rerun.io) ---
Iced - Octasine ---
Slint - WesAudio _HYPERION Editor --- ## Building Cross-platform Music Applications With Rust Pragmatic choice: use Rust from other languages and frameworks JUCE, Flutter, React Native Swift, Kotlin --- ## Why Care About Full-Stack Rust? To minimize context switching Everyone has a Maximum Languages Per Stack threshold (My MLPS is 2-3) --- ## The Plan - Build a simple audio app in various Rust app frameworks - Parameters sent to an audio processor - Realtime audio data -> visualization - Run on as many platforms as the framework supports --- ## Freeverb - Well-known public domain algorithm - See my 2018 talk for some background - [github.com/irh/freeverb-rs](https://github.com/irh/freeverb-rs) ---
---
--- ```rust #[derive(Clone)] struct FreeverbParameters { pub dampening: FloatParameter, pub width: FloatParameter, pub room_size: FloatParameter, pub freeze: BoolParameter, pub dry: FloatParameter, pub wet: FloatParameter, pub scope: BoolParameter, } ``` --- ```rust trait Parameter { fn name(&self) -> Arc
; fn id(&self) -> usize; fn value_type(&self) -> ValueType; fn default_user_value(&self) -> f32; fn value_converter(&self) -> Arc
; fn string_converter(&self) -> Arc
; } ``` --- ```rust #[derive(Debug, Clone)] pub enum ToProcessor { BeginEdit(usize), SetParameter(usize, f32), EndEdit(usize), } pub trait PushMessage
{ fn push(&self, message: T) -> bool; } pub trait PopMessage
{ fn pop(&self) -> Option
; } ``` --- ```rust pub struct AudioStream
{ ... } impl
AudioStream
{ pub fn new() -> Result
{ ... } pub fn to_processor(&self) -> ToProcessorSender { ... } pub fn from_processor(&self) -> FromProcessorReceiver
{ ... } } ``` --- `egui`
[egui.rs](https://egui.rs) --- ## `egui` - An immediate-mode UI framework - Popular as an embedded UI - Companion app framework, `eframe` ---
Video not supported.
--- ```rust[|2-3|4-7|8] fn main() -> Result<()> { eframe::run_native( "Freeverb", NativeOptions { viewport: ViewportBuilder::default().with_inner_size([400.0, 265.0]), ..Default::default() }, Box::new(|_| Ok(Box::new(App::new()?))), ) } ``` --- ```rust pub struct App { ui_state: FreeverbUiState, audio_stream: Option
>, } impl App { pub fn new() -> Result
{ ... } fn toggle_audio_stream(&mut self) { ... } } ``` --- ```rust[|1-2|3-5|7|8-9|10|12-17|14|15] impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) { if let Some(stream) = &self.audio_stream. { self.ui_state.receive_processor_messages(stream.from_processor()); } CentralPanel::default().show(ctx, |ui| { // Header ui.horizontal(|ui| { ui.label(RichText::new("Freeverb").text_style(TextStyle::Heading)); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { let mut audio_enabled = self.audio_stream.is_some(); if ui.checkbox(&mut audio_enabled, "Enable Audio").changed() { self.toggle_audio_stream(); } }); }); ... ``` --- ```rust // Parameters ui.vertical(|ui| { ui.add(FloatSlider::new(&mut parameters.dampening, &self.to_processor)); ui.add(FloatSlider::new(&mut parameters.width, &self.to_processor)); ui.add(FloatSlider::new(&mut parameters.room_size, &self.to_processor)); ui.add(Checkbox::new(&mut parameters.freeze, &self.to_processor)); ui.add(FloatSlider::new(&mut parameters.dry, &self.to_processor)); ui.add(FloatSlider::new(&mut parameters.wet, &self.to_processor)); }); ``` --- ```rust[|1-14|2-5|6|7|9-11|16-28|21-23] let response = ui.add({ let mut slider = egui::Slider::new( &mut self.parameter.value, value_converter.min()..=value_converter.max(), ) .custom_formatter(|n, _| string_converter.to_string(n as f32)) .custom_parser(|s| string_converter.to_f32(s).map(|n| n as f64)); if let Some(unit) = string_converter.unit() { slider = slider.suffix(format!(" {unit}")); } slider }); if let Some(to_processor) = self.to_processor { if response.drag_started() { to_processor.push(ToProcessor::BeginEdit(id)); } if response.changed() { to_processor.push(ToProcessor::SetParameter(id, self.parameter.value)); } if response.drag_stopped() { to_processor.push(ToProcessor::EndEdit(id)); } } ``` --- ```rust[|2-4] // Scope let size = ui.available_size(); ui.add_sized( vec2(size.y, size.y), PhaseScope::new( self.to_processor.is_some(), self.state.scope_frames.iter().cloned(), ), ); ``` --- ```rust[|3,12|4-6|7-10] let stroke = ui.visuals().noninteractive().fg_stroke; let color = stroke.color; for (i, point) in points.enumerate() { let alpha = ((i as f32 / point_count) * 255.0 * 0.5) as u8; let color = Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), alpha); painter.line_segment( [previous_point, point], Stroke::new(stroke.width, color), ); previous_point = point; } ``` ---
---
---
---
--- Dioxus
[dioxuslabs.com](https://dioxuslabs.com) --- ## Dioxus - An application framework in Rust using the web stack - Uses the OS-provided webview by default - Renderer-agnostic, a native renderer is in the works - Tons of effort on tooling - `dx` CLI tool for building, running, bundling - Hot-loading of assets, UI code - ...and now support for hot-patching of Rust code ---
--- ```rust[|1-2|3-11|12] fn main() { dioxus::LaunchBuilder::new() .with_cfg(desktop! { use dioxus::desktop::{Config, LogicalSize, WindowBuilder}; Config::new().with_window( WindowBuilder::new() .with_title("Freeverb") .with_inner_size(LogicalSize::new(310, 500)) ) }) .launch(App); } ``` --- ```rust[|1-2|3-4|6-7|9-10|11-22|7,11|25-30] #[component] pub fn App() -> Element { // Initialize the parameters when the app is first run. let parameters = use_hook(FreeverbParameters::default); // Create a signal for enabling or disabling the audio stream. let mut audio_enabled = use_signal(|| false); // Create or destroy the audio stream when `audio_enabled` changes. let audio_stream = use_memo(move || { if audio_enabled() { match AudioStream::new() { Ok(stream) => Some(FreeverbStream(Arc::new(stream))), Err(error) => { error!("Failed to create audio stream: {error}"); *audio_enabled.write() = false; None } } } else { None } }); // Derive a signal from the audio stream that provides the to_processor sender let to_processor = use_memo(move || { audio_stream() .as_ref() .map(|stream| stream.0.to_processor()) }); ... ``` --- ```rust[|3|4|6-20|15,16|22-27] ... rsx! { document::Link { rel: "stylesheet", href: MAIN_CSS } div { class: "header", div { class: "header-text", "Freeverb" } Toggle { pressed: audio_enabled(), on_pressed_change: move |pressed| *audio_enabled.write() = pressed, "Enable Audio" } } ParameterSlider { parameter: parameters.dampening, to_processor: to_processor } ParameterSlider { parameter: parameters.width, to_processor: to_processor } ParameterSlider { parameter: parameters.room_size, to_processor: to_processor } ParameterToggle { parameter: parameters.freeze, to_processor: to_processor } ParameterSlider { parameter: parameters.dry, to_processor: to_processor } ParameterSlider { parameter: parameters.wet, to_processor: to_processor } } ``` --- ```rust[|1-5|8-9|11-16|18-19|26-36|29|30-34] #[component] pub fn ParameterSlider( parameter: FloatParameter, to_processor: ReadSignal
>, ) -> Element { let id = parameter.id(); // Create a signal based on the parameter value. let mut value = use_signal(|| parameter.value); // Send updated values to the processor. use_effect(move || { if let Some(to_processor) = to_processor() { to_processor.push(ToProcessor::SetParameter(id, value())); } }); rsx! { document::Link { rel: "stylesheet", href: asset!("./slider.css") } div { class: "slider-name", "{name}" } div { slider::Slider { class: "slider", value: Some(SliderValue::Single(value())), on_value_change: move |new_value| match new_value{ SliderValue::Single(new_value) => { *value.write() = new_value as f32; } }, } } } ``` ---
---
---
Video not supported.
--- ## `audio-app` [github.com/irh/audio-app](https://github.com/irh/audio-app) - Hopefully serves as a useful entry point for learning - Also includes examples using `Iced` and `Vizia` - Contributions are welcome - New frameworks, bug fixes, better docs --- ## Closing Thoughts - Rust + Audio Dev is coming along nicely - Full-stack cross-platform Rust music app dev is now _possible_ - ...with plenty of rough edges... - ...but things are improving quickly! - The more the merrier, get involved! - A good place to start: [Rust Audio](https://rust.audio) - If you have a budget, please sponsor open-source projects --- # THANKS FOR YOUR TIME [github.com/irh/audio-app](https://github.com/irh/audio-app) --- ## Extras #### More Gui Frameworks #### Audio Crates #### Platform Details #### No Dioxus Scope? --- ## More GUI Frameworks to Check Out - [Iced](https://iced.rs) - Popular for apps and plugins - Elm-architecture in Rust - [Vizia](https://book.vizia.dev) - Also has `nih_plug` support - Based on reactive data bindings via lenses --- ## More GUI Frameworks to Check Out - [Slint](https://slint.dev) - Commercially backed, founded by some ex-Qt devs - UIs built in a reactive language, with bindings for app logic in Rust, C++, Python, Node - They've had a focus on embedded, but are improving support for other platforms - See [Making Slint Desktop Ready (Oct 2025)](https://slint.dev/blog/making-slint-desktop-ready) --- ## More GUI Frameworks to Check Out - [Bevy](https://bevy.org) - A popular open source ECS-based game engine - Highly modular - UI is bare-bones now, but is under active development --- ## Audio Crates - [`cpal`](https://github.com/rustaudio/cpal) - Cross-Platform Audio Layer - [`midir`](https://github.com/Boddlnagg/midir) - Cross-Platform MIDI Layer --- ## Audio Crates... - [`dasp`](https://github.com/RustAudio/dasp) - Lots of DSP utilities - [`FunDSP`](https://github.com/SamiPerttu/fundsp) - Fancy audio processing - `sine_hz(f) * f * m + f >> sine()` --- ## Audio Crates... - [`Symphonia`](https://github.com/pdeljanov/Symphonia) - Decoder for tons of audio formats - [`firewheel`](https://github.com/BillyDM/Firewheel) - WIP New audio engine for [Bevy](https://bevy.org) --- ## Audio Crates... - [`nih-plug`](https://github.com/robbert-vdh/nih-plug) - Plugin abstraction framework - BYO UI Framework - Targets: CLAP, VST3, Standalone - (AU possible via CLAP wrapper) --- ## Platform Details #### Plugins - Plugins are using `nih_plug` - A plugin host abstraction framework - It supports `VST3`, `CLAP`, and standalone - (`AU` possible through [clap-wrapper](https://github.com/free-audio/clap-wrapper)) - My approach in this project was 'app -> plugin' rather than the other way around - ...which made things harder. - ...so no Iced or Vizia yet. --- ## Platform Details #### iPhone - Not much to it, using a pre-build script to compile the rust binary - The app calls the binary's `main` function directly - An `AVAudioSession` needs to be created - The [`objc2`](https://docs.rs/objc2/latest/objc2/) crates are used to make platform API calls --- ## Platform Details #### Android - Build the application as a `cdylib` - Use a `NativeActivity` that calls into an exported `android_main` function - Runtime permission for microphone access needs to be requested - Platform API calls via the [`ndk`](https://crates.io/crates/ndk) and [`jni`](https://crates.io/crates/jni) crates. - The [`android-permissions`](https://crates.io/crates/android-permissions) helper crate is handy --- ## Platform Details #### Web - The processor runs in an `AudioWorklet` - ...in a separate `WebAssembly` context - `Wasm` atomics in Rust are [currently unstable](https://github.com/rust-lang/rust/issues/77839) - So the `audio-app` project jumps through hoops that could be avoided on nightly - Getting the `.wasm` loaded is hard - Visualization data gets serialized --- ## No Scope for Dioxus? - On the desktop, the application runs as native code - So browser APIs like `canvas` aren't available - A trick involving layering the webview over a `wgpu` window is possible... - ...but wouldn't be cross-platform so I didn't want to go that way - The WIP native renderer will make a cross-platform canvas possible - When? I hope soon!