Skip to content

Instantly share code, notes, and snippets.

@AdamBrouwersHarries
Created December 22, 2019 01:05
Show Gist options
  • Save AdamBrouwersHarries/67bc0194e02cdca145993e46b735fc3a to your computer and use it in GitHub Desktop.
Save AdamBrouwersHarries/67bc0194e02cdca145993e46b735fc3a to your computer and use it in GitHub Desktop.
Simple terminal based BPM detection/reporting in Rust uing tui
/// 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