add built-in airhorn alarm sound
This commit is contained in:
parent
f9590fe811
commit
b347386e99
6 changed files with 70 additions and 36 deletions
BIN
airhorn_alarm.mp3
Normal file
BIN
airhorn_alarm.mp3
Normal file
Binary file not shown.
|
@ -1,3 +1,5 @@
|
|||
use std::ffi::OsString;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
|
@ -17,7 +19,10 @@ pub struct Cli {
|
|||
|
||||
/// Audio file to play at the end of the countdown.
|
||||
#[clap(long, short)]
|
||||
pub alarm: Option<String>,
|
||||
pub alarm: Option<OsString>,
|
||||
|
||||
#[clap(short = 'A', conflicts_with = "alarm")]
|
||||
pub airhorn: bool,
|
||||
|
||||
/// Begin countdown immediately.
|
||||
#[clap(long = "immediate", long = "running", short = 'i', short = 'r')]
|
||||
|
|
11
src/lib.rs
11
src/lib.rs
|
@ -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 timer;
|
||||
mod util;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum Alarm {
|
||||
Airhon,
|
||||
User(OsString),
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ use katabastird::timer::Timer;
|
|||
fn main() {
|
||||
let options = eframe::NativeOptions {
|
||||
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()
|
||||
};
|
||||
|
||||
|
|
48
src/timer.rs
48
src/timer.rs
|
@ -5,10 +5,10 @@ use eframe::{App, CreationContext};
|
|||
use egui::{Color32, Direction, FontId, Layout, RichText, Ui};
|
||||
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 MAX_REPAINT: Duration = Duration::from_millis(500);
|
||||
const MIN_REPAINT: Duration = Duration::from_millis(100);
|
||||
const MAX_REPAINT: Duration = Duration::from_millis(250);
|
||||
|
||||
const DIGIT_FACTOR: f32 = 0.4;
|
||||
const TEXT_FACTOR: f32 = 0.2;
|
||||
|
@ -25,7 +25,7 @@ pub struct Timer {
|
|||
duration: Duration,
|
||||
state: TimerState,
|
||||
tstart: Instant, // so we can blink
|
||||
alarm: Option<String>,
|
||||
alarm: Option<Alarm>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
@ -67,9 +67,9 @@ impl App for Timer {
|
|||
let dur = Instant::now() - cs.updated;
|
||||
let dur = MIN_REPAINT.saturating_sub(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),
|
||||
}
|
||||
// check for quit key
|
||||
|
@ -95,11 +95,19 @@ impl Timer {
|
|||
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 {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
fonts.font_data.insert(
|
||||
"predator".to_owned(),
|
||||
egui::FontData::from_static(include_bytes!("../Predator.ttf")),
|
||||
egui::FontData::from_static(PREDATOR_FONT),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
|
@ -114,7 +122,7 @@ impl Timer {
|
|||
direction,
|
||||
state: TimerState::Unstarted,
|
||||
tstart: Instant::now(),
|
||||
alarm: cli.alarm,
|
||||
alarm,
|
||||
};
|
||||
if cli.running {
|
||||
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 text = RichText::new("PAUSE")
|
||||
.font(FontId::monospace(tsize))
|
||||
.color(Color32::GOLD);
|
||||
|
||||
let elapsed;
|
||||
let remaining;
|
||||
let mut is_paused = false;
|
||||
if let TimerState::Running(rs) = self.state {
|
||||
elapsed = Instant::now() - rs.updated;
|
||||
remaining = rs.remaining;
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
let remaining = remaining.saturating_sub(elapsed);
|
||||
let elapsed = Instant::now() - cs.updated;
|
||||
let remaining = cs.remaining.saturating_sub(elapsed);
|
||||
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::relative(0.3333))
|
||||
|
@ -216,17 +216,11 @@ impl Timer {
|
|||
}
|
||||
}
|
||||
|
||||
fn paused(&mut self, ui: &mut Ui, vsize: f32) {
|
||||
let elapsed;
|
||||
fn paused(&mut self, ui: &mut Ui, vsize: f32, cs: ChronoState) {
|
||||
let mut is_running = false;
|
||||
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)
|
||||
.size(Size::relative(0.33333))
|
||||
.size(Size::remainder())
|
||||
|
|
37
src/util.rs
37
src/util.rs
|
@ -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_extras::{Size, Strip};
|
||||
use rodio::{source::Source, Decoder, OutputStream};
|
||||
|
||||
use crate::{Alarm, AIRHORN};
|
||||
|
||||
fn format_digits(digits: u64, size: f32, color: Color32) -> RichText {
|
||||
RichText::new(format!("{:02}", digits))
|
||||
|
@ -9,14 +17,29 @@ fn format_digits(digits: u64, size: f32, color: Color32) -> RichText {
|
|||
.color(color)
|
||||
}
|
||||
|
||||
pub fn alarm(path: String) {
|
||||
use rodio::{source::Source, Decoder, OutputStream};
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
pub(crate) fn alarm(source: Alarm) {
|
||||
if let Alarm::User(path) = source {
|
||||
play_user(&path)
|
||||
} 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();
|
||||
let file = BufReader::new(File::open(path).unwrap());
|
||||
fn play_user(path: &OsStr) {
|
||||
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();
|
||||
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() {
|
||||
dur
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue