From 5038eb40ebb00611c774f87445de4578b1652785 Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Sun, 9 Jun 2019 23:04:49 +0200 Subject: [PATCH] Proof-of-concept VUMeter --- Cargo.lock | 7 --- Cargo.toml | 1 - README.md | 13 +++++ src/audio.rs | 75 +++++++++++++++++++++++++ src/gui.rs | 52 +++++++++++++++++ src/main.rs | 153 +++++++++++++++------------------------------------ 6 files changed, 185 insertions(+), 116 deletions(-) create mode 100644 README.md create mode 100644 src/audio.rs create mode 100644 src/gui.rs diff --git a/Cargo.lock b/Cargo.lock index 5790025..39d014a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,7 +52,6 @@ version = "0.1.0" dependencies = [ "cpal 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "gtk 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "sample 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -517,11 +516,6 @@ dependencies = [ "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "sample" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "stdweb" version = "0.1.3" @@ -674,7 +668,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" "checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" -"checksum sample 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc0966ad788ec7289562643e05c611b7853e0618b7f9306aafd2fc727abfe168" "checksum stdweb 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dde0593aeb8d47accea5392b39350015b5eccb12c0d98044d856983d89548dea" diff --git a/Cargo.toml b/Cargo.toml index fe48286..6b4385c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ edition = "2018" [dependencies] cpal = "0.8.2" -sample = "0.10.0" [dependencies.gtk] version = "0.5.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..12e6371 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# rust-lock-free-vumeter + +Simple proof-of-concept of a lock-free audio/GUI pattern, inspired by [Tim Doumler at CPPCon 2015](https://www.youtube.com/watch?v=boPEO2auJj4) - but in Rust, not C++. + +## TODO: + +It should be possible to further optimize the thread interaction by avoiding `SeqCst` ordering. + +## References + +* https://www.youtube.com/watch?v=boPEO2auJj4 +* https://bartoszmilewski.com/2008/08/04/multicores-and-publication-safety/ +* http://moodycamel.com/blog/2013/a-fast-lock-free-queue-for-c++ diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..c17fc12 --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,75 @@ +extern crate cpal; + +use cpal::{EventLoop, Sample, StreamData, UnknownTypeInputBuffer}; +use std::{ + i16, + sync::atomic::{AtomicBool, AtomicI16, Ordering}, +}; + +pub struct AudioState { + pub max_value: AtomicI16, + pub gui_up_to_date: AtomicBool, +} + +impl AudioState { + pub fn new() -> Self { + AudioState { + max_value: AtomicI16::new(0), + gui_up_to_date: AtomicBool::new(false), + } + } +} + +// Open the default audio device, and start the audio routine: +pub fn start(state: &AudioState) -> Result<(), String> { + let event_loop = EventLoop::new(); + let device = cpal::default_input_device().ok_or("Could not find a default input device")?; + + let mut supported_formats_range = device + .supported_input_formats() + .map_err(|_| "Could not get supported input formats")?; + let format = supported_formats_range + .next() + .ok_or("Could not get supported formats range")? + .with_max_sample_rate(); + + let stream_id = event_loop + .build_input_stream(&device, &format) + .map_err(|_| "Could not build input stream")?; + + event_loop.play_stream(stream_id); + + // Define the actual audio callback. + // All we do is extract the enum variant, deference the buffer + // and then lend it to process_audio: + event_loop.run(|_stream_id, stream_data| match stream_data { + StreamData::Input { + buffer: UnknownTypeInputBuffer::I16(buffer), + } => process_audio(&*buffer, state), + StreamData::Input { + buffer: UnknownTypeInputBuffer::U16(buffer), + } => process_audio(&*buffer, state), + StreamData::Input { + buffer: UnknownTypeInputBuffer::F32(buffer), + } => process_audio(&*buffer, state), + _ => unreachable!(), + }); +} + +// process_audio takes a reference to a slice of sample-friendly +// primitives, calculates the max value and atomically updates the AudioState. +fn process_audio(buffer: &[T], state: &AudioState) +where + T: cpal::Sample, +{ + let max = buffer + .iter() + .map(Sample::to_i16) + .max() + .map(|s| s.max(i16::min_value() + 1)) + .unwrap_or(0) + .abs(); + + state.max_value.store(max, Ordering::SeqCst); + state.gui_up_to_date.store(false, Ordering::SeqCst); +} diff --git a/src/gui.rs b/src/gui.rs new file mode 100644 index 0000000..e319f68 --- /dev/null +++ b/src/gui.rs @@ -0,0 +1,52 @@ +extern crate gtk; + +use gtk::prelude::*; +use gtk::{LevelBar, LevelBarExt, Window, WindowType}; + +pub struct Gui { + level_bar: gtk::LevelBar, +} + +impl Gui { + pub fn new(title: &str) -> Self { + let window = Window::new(WindowType::Toplevel); + window.set_title(title); + window.set_default_size(350, 70); + + window.connect_delete_event(|_, _| { + gtk::main_quit(); + Inhibit(false) + }); + + let level_bar = LevelBar::new(); + window.add(&level_bar); + window.show_all(); + + Self { + level_bar: level_bar, + } + } + + pub fn set_level(&self, level: f64) { + self.level_bar.set_value(level); + } +} + +pub fn add_timeout(interval: u32, mut func: F) +where + F: FnMut() + 'static, +{ + gtk::timeout_add(interval, move || { + func(); + Continue(true) + }); +} + +pub fn init() -> Result<(), String> { + gtk::init().map_err(|_| "Failed to initialize GTK")?; + Ok(()) +} + +pub fn run() { + gtk::main(); +} diff --git a/src/main.rs b/src/main.rs index 2cc0c03..6a4899e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,117 +1,54 @@ -extern crate cpal; -extern crate gtk; +mod audio; +mod gui; -use std::thread; -use std::iter::Iterator; -use cpal::{EventLoop, StreamData, UnknownTypeInputBuffer, Sample}; -use gtk::prelude::*; -use gtk::{Button, Window, WindowType}; +use std::{ + sync::{atomic::Ordering, Arc}, + thread, +}; -pub struct Gui { - window: gtk::Window -} - -impl Gui { - pub fn new() -> Self { - let window = Window::new(WindowType::Toplevel); - window.set_title("Hello GTK"); - window.set_default_size(350, 70); - - let button = Button::new_with_label("Click me now"); - window.add(&button); - window.show_all(); - - window.connect_delete_event(|_, _| { - println!("Bye"); - gtk::main_quit(); - Inhibit(false) - }); - - button.connect_clicked(|_| { - println!("Clicked!"); - }); - - Self { - window: window - } - } -} - -struct AudioEngine { - event_loop: EventLoop, - max_value: i16, -} - -impl AudioEngine { - pub fn new() -> Self { - AudioEngine { - event_loop: EventLoop::new(), - max_value: 0 - } - } - - pub fn init(&mut self) -> Result<(), String> { - let device = cpal::default_input_device() - .ok_or("Could not find a default input device")?; - let mut supported_formats_range = device.supported_input_formats() - .map_err(|_| "Could not get supported input formats")?; - let format = supported_formats_range - .next() - .ok_or("Could not get supported formats range")? - .with_max_sample_rate(); - - let stream_id = self.event_loop.build_input_stream(&device, &format) - .map_err(|_| "Could not build input stream")?; - self.event_loop.play_stream(stream_id); - - Ok(()) - } - - pub fn run(&mut self) { - self.event_loop.run(|_stream_id, stream_data| { - match stream_data { - StreamData::Input { buffer: UnknownTypeInputBuffer::I16(buffer) } => - self.process_audio(buffer), - StreamData::Input { buffer: UnknownTypeInputBuffer::U16(buffer) } => - self.process_audio(buffer), - StreamData::Input { buffer: UnknownTypeInputBuffer::F32(buffer) } => - self.process_audio(buffer), - _ => unreachable!() - } - }); - } - - fn process_audio(&mut self, buffer: cpal::InputBuffer) where T: cpal::Sample { - let max = buffer - .iter() - .map(Sample::to_i16) - .max() - .unwrap_or(0) - .abs(); - - if max > self.max_value { - println!("new max: {}", max); - self.max_value = max; - } - } -} +const FLOAT_MAX: f64 = i16::max_value() as f64; fn main() { - // Audio: - thread::spawn(move || { - let mut audio_engine = AudioEngine::new(); - audio_engine.init().unwrap_or_else(|err| { - panic!("Could not start audio engine: {}", err); - }); + // Initialize state shared between audio and GUI threads. + // The struct itself is Sync and therefore safe to share between + // threads, but an Arc pointer is used to workaround Rust's + // ownership rules. Calling `clone` on the pointer increments its + // internal reference count allowing for safe memory management + // between threads. This should not cause a runtime penalty inside + // the audio callback. + let global_state = Arc::new(audio::AudioState::new()); + let audio_state = global_state.clone(); + let gui_state = global_state.clone(); - audio_engine.run(); + // Spawn a thread for the audio engine: + thread::spawn(move || { + audio::start(&audio_state).unwrap(); }); - // GUI - if gtk::init().is_err() { - panic!("Failed to initialize GTK."); - } - let _gui = Gui::new(); + // Setup GUI: + gui::init().unwrap(); + let the_gui = gui::Gui::new("VUMeter-Rust"); - gtk::main(); + // Setup a timer on the main thread to poll the audio state and + // update the GUI. It takes a second `clone`d copy of the Arc pointer. + gui::add_timeout(50, move || { + // Atomically swap the `gui_up_to_date` value: + let result = gui_state.gui_up_to_date.compare_exchange( + false, + true, + Ordering::SeqCst, + Ordering::SeqCst, + ); + + if let Ok(false) = result { + // if the maximum value has changed, load it into a local + // variable and convert it to range 0..1.0. + // Then as we are on the main thread we can update the + // GUI safely. + let val = gui_state.max_value.load(Ordering::SeqCst); + the_gui.set_level((val as f64) / FLOAT_MAX); + } + }); + + gui::run(); }