Proof-of-concept VUMeter
This commit is contained in:
parent
c1673f7b40
commit
5038eb40eb
|
@ -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"
|
||||
|
|
|
@ -6,7 +6,6 @@ edition = "2018"
|
|||
|
||||
[dependencies]
|
||||
cpal = "0.8.2"
|
||||
sample = "0.10.0"
|
||||
|
||||
[dependencies.gtk]
|
||||
version = "0.5.0"
|
||||
|
|
|
@ -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++
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
153
src/main.rs
153
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<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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue