Proof-of-concept VUMeter
This commit is contained in:
parent
c1673f7b40
commit
5038eb40eb
|
@ -52,7 +52,6 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cpal 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"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]]
|
[[package]]
|
||||||
|
@ -517,11 +516,6 @@ dependencies = [
|
||||||
"ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "stdweb"
|
name = "stdweb"
|
||||||
version = "0.1.3"
|
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 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 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 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 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 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"
|
"checksum termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dde0593aeb8d47accea5392b39350015b5eccb12c0d98044d856983d89548dea"
|
||||||
|
|
|
@ -6,7 +6,6 @@ edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cpal = "0.8.2"
|
cpal = "0.8.2"
|
||||||
sample = "0.10.0"
|
|
||||||
|
|
||||||
[dependencies.gtk]
|
[dependencies.gtk]
|
||||||
version = "0.5.0"
|
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;
|
mod audio;
|
||||||
extern crate gtk;
|
mod gui;
|
||||||
|
|
||||||
use std::thread;
|
use std::{
|
||||||
use std::iter::Iterator;
|
sync::{atomic::Ordering, Arc},
|
||||||
use cpal::{EventLoop, StreamData, UnknownTypeInputBuffer, Sample};
|
thread,
|
||||||
use gtk::prelude::*;
|
};
|
||||||
use gtk::{Button, Window, WindowType};
|
|
||||||
|
|
||||||
pub struct Gui {
|
const FLOAT_MAX: f64 = i16::max_value() as f64;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Audio:
|
// Initialize state shared between audio and GUI threads.
|
||||||
thread::spawn(move || {
|
// The struct itself is Sync and therefore safe to share between
|
||||||
let mut audio_engine = AudioEngine::new();
|
// threads, but an Arc pointer is used to workaround Rust's
|
||||||
audio_engine.init().unwrap_or_else(|err| {
|
// ownership rules. Calling `clone` on the pointer increments its
|
||||||
panic!("Could not start audio engine: {}", err);
|
// 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
|
// Setup GUI:
|
||||||
if gtk::init().is_err() {
|
gui::init().unwrap();
|
||||||
panic!("Failed to initialize GTK.");
|
let the_gui = gui::Gui::new("VUMeter-Rust");
|
||||||
}
|
|
||||||
let _gui = Gui::new();
|
|
||||||
|
|
||||||
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