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;
|
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')]
|
||||||
|
|
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 cli;
|
||||||
pub mod timer;
|
pub mod timer;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) enum Alarm {
|
||||||
|
Airhon,
|
||||||
|
User(OsString),
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
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::{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())
|
||||||
|
|
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::{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 {
|
||||||
|
|
Loading…
Reference in a new issue