Proof-of-concept VUMeter

This commit is contained in:
Rob Watson 2019-06-09 23:04:49 +02:00
parent c1673f7b40
commit 5038eb40eb
6 changed files with 185 additions and 116 deletions

7
Cargo.lock generated
View File

@ -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"

View File

@ -6,7 +6,6 @@ edition = "2018"
[dependencies]
cpal = "0.8.2"
sample = "0.10.0"
[dependencies.gtk]
version = "0.5.0"

13
README.md Normal file
View File

@ -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++

75
src/audio.rs Normal file
View File

@ -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<T> takes a reference to a slice of sample-friendly
// primitives, calculates the max value and atomically updates the AudioState.
fn process_audio<T>(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);
}

52
src/gui.rs Normal file
View File

@ -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<F>(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();
}

View File

@ -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<T>(&mut self, buffer: cpal::InputBuffer<T>) 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();
}