add built-in airhorn alarm sound

This commit is contained in:
Joe Ardent 2022-10-22 11:18:58 -07:00
parent f9590fe811
commit b347386e99
6 changed files with 70 additions and 36 deletions

BIN
airhorn_alarm.mp3 Normal file

Binary file not shown.

View file

@ -1,3 +1,5 @@
use std::ffi::OsString;
use clap::Parser; use clap::Parser;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -17,7 +19,10 @@ pub struct Cli {
/// Audio file to play at the end of the countdown. /// Audio file to play at the end of the countdown.
#[clap(long, short)] #[clap(long, short)]
pub alarm: Option<String>, pub alarm: Option<OsString>,
#[clap(short = 'A', conflicts_with = "alarm")]
pub airhorn: bool,
/// Begin countdown immediately. /// Begin countdown immediately.
#[clap(long = "immediate", long = "running", short = 'i', short = 'r')] #[clap(long = "immediate", long = "running", short = 'i', short = 'r')]

View file

@ -1,3 +1,14 @@
use std::ffi::OsString;
pub const AIRHORN: &[u8] = include_bytes!("../airhorn_alarm.mp3");
pub const PREDATOR_FONT: &[u8] = include_bytes!("../Predator.ttf");
pub mod cli; pub mod cli;
pub mod timer; pub mod timer;
mod util; mod util;
#[derive(Debug, Clone)]
pub(crate) enum Alarm {
Airhon,
User(OsString),
}

View file

@ -4,7 +4,8 @@ use katabastird::timer::Timer;
fn main() { fn main() {
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
renderer: eframe::Renderer::Wgpu, renderer: eframe::Renderer::Wgpu,
initial_window_size: Some(Vec2::new(1400.0, 800.0)), max_window_size: Some(Vec2::new(1400.0, 800.0)),
min_window_size: Some(Vec2::new(966.0, 600.0)),
..Default::default() ..Default::default()
}; };

View file

@ -5,10 +5,10 @@ use eframe::{App, CreationContext};
use egui::{Color32, Direction, FontId, Layout, RichText, Ui}; use egui::{Color32, Direction, FontId, Layout, RichText, Ui};
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use crate::{cli::Cli, util::*}; use crate::{cli::Cli, util::*, Alarm, PREDATOR_FONT};
const MIN_REPAINT: Duration = Duration::from_millis(200); // one frame before 200ms const MIN_REPAINT: Duration = Duration::from_millis(100);
const MAX_REPAINT: Duration = Duration::from_millis(500); const MAX_REPAINT: Duration = Duration::from_millis(250);
const DIGIT_FACTOR: f32 = 0.4; const DIGIT_FACTOR: f32 = 0.4;
const TEXT_FACTOR: f32 = 0.2; const TEXT_FACTOR: f32 = 0.2;
@ -25,7 +25,7 @@ pub struct Timer {
duration: Duration, duration: Duration,
state: TimerState, state: TimerState,
tstart: Instant, // so we can blink tstart: Instant, // so we can blink
alarm: Option<String>, alarm: Option<Alarm>,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -67,9 +67,9 @@ impl App for Timer {
let dur = Instant::now() - cs.updated; let dur = Instant::now() - cs.updated;
let dur = MIN_REPAINT.saturating_sub(dur); let dur = MIN_REPAINT.saturating_sub(dur);
ctx.request_repaint_after(dur); ctx.request_repaint_after(dur);
self.running(ui, height); self.running(ui, height, cs);
} }
TimerState::Paused(_) => self.paused(ui, height), TimerState::Paused(cs) => self.paused(ui, height, cs),
TimerState::Finished => self.finished(ui, height), TimerState::Finished => self.finished(ui, height),
} }
// check for quit key // check for quit key
@ -95,11 +95,19 @@ impl Timer {
CountDirection::Down CountDirection::Down
}; };
let alarm = if let Some(path) = cli.alarm {
Some(Alarm::User(path))
} else if cli.airhorn {
Some(Alarm::Airhon)
} else {
None
};
if predator { if predator {
let mut fonts = egui::FontDefinitions::default(); let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert( fonts.font_data.insert(
"predator".to_owned(), "predator".to_owned(),
egui::FontData::from_static(include_bytes!("../Predator.ttf")), egui::FontData::from_static(PREDATOR_FONT),
); );
fonts fonts
.families .families
@ -114,7 +122,7 @@ impl Timer {
direction, direction,
state: TimerState::Unstarted, state: TimerState::Unstarted,
tstart: Instant::now(), tstart: Instant::now(),
alarm: cli.alarm, alarm,
}; };
if cli.running { if cli.running {
let cs = ChronoState { let cs = ChronoState {
@ -150,23 +158,15 @@ impl Timer {
}); });
} }
fn running(&mut self, ui: &mut Ui, size: f32) { fn running(&mut self, ui: &mut Ui, size: f32, cs: ChronoState) {
let tsize = size * TEXT_FACTOR; let tsize = size * TEXT_FACTOR;
let text = RichText::new("PAUSE") let text = RichText::new("PAUSE")
.font(FontId::monospace(tsize)) .font(FontId::monospace(tsize))
.color(Color32::GOLD); .color(Color32::GOLD);
let elapsed;
let remaining;
let mut is_paused = false; let mut is_paused = false;
if let TimerState::Running(rs) = self.state { let elapsed = Instant::now() - cs.updated;
elapsed = Instant::now() - rs.updated; let remaining = cs.remaining.saturating_sub(elapsed);
remaining = rs.remaining;
} else {
unreachable!()
}
let remaining = remaining.saturating_sub(elapsed);
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::relative(0.3333)) .size(Size::relative(0.3333))
@ -216,17 +216,11 @@ impl Timer {
} }
} }
fn paused(&mut self, ui: &mut Ui, vsize: f32) { fn paused(&mut self, ui: &mut Ui, vsize: f32, cs: ChronoState) {
let elapsed;
let mut is_running = false; let mut is_running = false;
let tsize = vsize * TEXT_FACTOR; let tsize = vsize * TEXT_FACTOR;
if let TimerState::Paused(cs) = self.state {
elapsed = cs.remaining;
} else {
unreachable!()
}
let remaining = elapsed; let remaining = cs.remaining;
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::relative(0.33333)) .size(Size::relative(0.33333))
.size(Size::remainder()) .size(Size::remainder())

View file

@ -1,7 +1,15 @@
use std::time::Duration; use std::{
ffi::OsStr,
fs::File,
io::{Cursor, Read, Seek},
time::Duration,
};
use egui::{Color32, Direction, FontId, Layout, RichText}; use egui::{Color32, Direction, FontId, Layout, RichText};
use egui_extras::{Size, Strip}; use egui_extras::{Size, Strip};
use rodio::{source::Source, Decoder, OutputStream};
use crate::{Alarm, AIRHORN};
fn format_digits(digits: u64, size: f32, color: Color32) -> RichText { fn format_digits(digits: u64, size: f32, color: Color32) -> RichText {
RichText::new(format!("{:02}", digits)) RichText::new(format!("{:02}", digits))
@ -9,14 +17,29 @@ fn format_digits(digits: u64, size: f32, color: Color32) -> RichText {
.color(color) .color(color)
} }
pub fn alarm(path: String) { pub(crate) fn alarm(source: Alarm) {
use rodio::{source::Source, Decoder, OutputStream}; if let Alarm::User(path) = source {
use std::fs::File; play_user(&path)
use std::io::BufReader; } else {
let source = Cursor::new(AIRHORN.to_owned());
let decoder = Decoder::new(source).unwrap();
play_alarm(decoder);
}
}
let (_stream, stream_handle) = OutputStream::try_default().unwrap(); fn play_user(path: &OsStr) {
let file = BufReader::new(File::open(path).unwrap()); let mut f = File::open(path).unwrap_or_else(|_| panic!("Could not open file {:?}", path));
let metadata = std::fs::metadata(path).expect("unable to read metadata");
let mut buffer = vec![0; metadata.len() as usize];
f.read_exact(&mut buffer).expect("buffer overflow");
let file = Cursor::new(buffer);
let source = Decoder::new(file).unwrap(); let source = Decoder::new(file).unwrap();
play_alarm(source);
}
fn play_alarm<S: Read + Seek + Send + 'static>(source: Decoder<S>) {
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
let dur = if let Some(dur) = source.total_duration() { let dur = if let Some(dur) = source.total_duration() {
dur dur
} else { } else {