Created
December 22, 2019 01:05
-
-
Save AdamBrouwersHarries/67bc0194e02cdca145993e46b735fc3a to your computer and use it in GitHub Desktop.
Simple terminal based BPM detection/reporting in Rust uing tui
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// Based on code from tui-rs/examples, adapted to support bpm calculation by beats. | |
/// A simple example demonstrating how to handle user input. This is | |
/// a bit out of the scope of the library as it does not provide any | |
/// input handling out of the box. However, it may helps some to get | |
/// started. | |
/// | |
/// This is a very simple example: | |
/// * A input box always focused. Every character you type is registered | |
/// here | |
/// * Pressing Backspace erases a character | |
/// * Pressing Enter pushes the current input in the history of previous | |
/// messages | |
#[allow(dead_code)] | |
mod util; | |
use std::io::{self, Write}; | |
use termion::cursor::Goto; | |
use termion::event::Key; | |
use termion::input::MouseTerminal; | |
use termion::raw::IntoRawMode; | |
use termion::screen::AlternateScreen; | |
use tui::backend::TermionBackend; | |
use tui::layout::{Constraint, Direction, Layout, Alignment}; | |
use tui::style::{Color, Style}; | |
use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget}; | |
use tui::Terminal; | |
use unicode_width::UnicodeWidthStr; | |
use std::collections::VecDeque; | |
use std::time::Duration; | |
use std::time::Instant; | |
use crate::util::event::{Event, Events}; | |
const SAMPLES: usize = 60 * 600; | |
const TIMEOUT: f32 = 5.0; // seconds | |
struct BpmTiming { | |
/// The most recent instant at which a button was pressed | |
last_press: Instant, | |
/// A queue of durations, over which we calculate the mean interval, and therefore BPM | |
durations: VecDeque<std::time::Duration>, | |
} | |
impl BpmTiming { | |
fn new() -> BpmTiming { | |
BpmTiming { | |
last_press: std::time::Instant::now(), | |
durations: std::collections::VecDeque::new(), | |
} | |
} | |
fn push_instant(&mut self, press: Instant) -> () { | |
// calculate the duration of the last press | |
let new_duration = self.last_press.elapsed(); | |
// replace the last press with our most recent | |
self.last_press = press; | |
if new_duration > Duration::from_secs_f32(TIMEOUT) { | |
self.durations.clear(); | |
} else { | |
// Append our new duration to the list of durations | |
self.durations.push_front(new_duration); | |
self.durations.truncate(SAMPLES); | |
} | |
} | |
fn clear_samples(&mut self) -> () { | |
self.durations.clear(); | |
} | |
fn report_mean_time(&self) -> String { | |
// Sum the durations | |
let total_duration : std::time::Duration = self.durations.iter().sum(); | |
let mean_duration: std::time::Duration = total_duration / (self.durations.len() as u32); | |
let events_per_min = 60.0 / mean_duration.as_secs_f32(); | |
format!("Mean duration: {:?} / Events per minute: {:?}", mean_duration, events_per_min) | |
} | |
} | |
/// App holds the state of the application | |
struct App { | |
timings: BpmTiming, | |
/// The text that we're reporting | |
reported_text: String, | |
} | |
impl Default for App { | |
fn default() -> App { | |
App { | |
timings: BpmTiming::new(), | |
reported_text: String::new(), | |
} | |
} | |
} | |
fn main() -> Result<(), failure::Error> { | |
// Terminal initialization | |
let stdout = io::stdout().into_raw_mode()?; | |
let stdout = MouseTerminal::from(stdout); | |
let stdout = AlternateScreen::from(stdout); | |
let backend = TermionBackend::new(stdout); | |
let mut terminal = Terminal::new(backend)?; | |
// Setup event handlers | |
let events = Events::new(); | |
let usage = "Super simple bpm, the command line app! | |
Press <q> to leave the app. | |
Press <c> to clear the sample queue. | |
Press any other button to register a beat! | |
"; | |
// Create default app state | |
let mut app = App::default(); | |
loop { | |
// Draw UI | |
terminal.draw(|mut f| { | |
let chunks = Layout::default() | |
.direction(Direction::Vertical) | |
.margin(2) | |
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref()) | |
.split(f.size()); | |
Paragraph::new([Text::raw(usage)].iter()) | |
.alignment(Alignment::Right) | |
.style(Style::default()) | |
.block(Block::default()) | |
.render(&mut f, chunks[0]); | |
Paragraph::new([Text::raw(&app.reported_text)].iter()) | |
.alignment(Alignment::Center) | |
.style(Style::default().fg(Color::Yellow)) | |
.block(Block::default().borders(Borders::ALL).title("Input")) | |
.render(&mut f, chunks[1]); | |
})?; | |
// Handle input | |
match events.next()? { | |
Event::Input(input) => match input { | |
Key::Char('q') => { | |
break; | |
} | |
Key::Char('c') => { | |
app.timings.clear_samples() | |
} | |
Key::Char(c) => { | |
app.timings.push_instant(std::time::Instant::now()); | |
app.reported_text = app.timings.report_mean_time(); | |
} | |
_ => {} | |
}, | |
_ => {} | |
} | |
} | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment