feat: add sound notification #6
Generated
+1
-1
@@ -2120,7 +2120,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "termato"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"crossterm",
|
||||
|
||||
+12
@@ -1,6 +1,7 @@
|
||||
use crate::constants::SECONDS_PER_MIN;
|
||||
use crate::msg::Message;
|
||||
use crate::notification::send_desktop_notification;
|
||||
use crate::sound::play_notification_sound;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum TimerMode {
|
||||
@@ -17,6 +18,7 @@ pub struct Termato {
|
||||
pub time_remaining_in_sec: u32,
|
||||
pub show_help: bool,
|
||||
pub enable_notifications: bool,
|
||||
pub enable_sound: bool,
|
||||
should_quit: bool,
|
||||
}
|
||||
|
||||
@@ -31,6 +33,7 @@ impl Termato {
|
||||
time_remaining_in_sec: work_mins * SECONDS_PER_MIN,
|
||||
show_help: false,
|
||||
enable_notifications: false,
|
||||
enable_sound: false,
|
||||
should_quit: false,
|
||||
}
|
||||
}
|
||||
@@ -44,6 +47,11 @@ impl Termato {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_sound(mut self, enable: bool) -> Self {
|
||||
self.enable_sound = enable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn update(&mut self, msg: Message) {
|
||||
match msg {
|
||||
Message::Tick => self.tick(),
|
||||
@@ -83,6 +91,10 @@ impl Termato {
|
||||
}
|
||||
}
|
||||
|
||||
if self.enable_sound {
|
||||
play_notification_sound();
|
||||
}
|
||||
|
||||
self.toggle_mode();
|
||||
}
|
||||
}
|
||||
|
||||
+19
-4
@@ -8,6 +8,8 @@ pub const FLAG_VERSION: &str = "-v";
|
||||
pub const FLAG_VERSION_LONG: &str = "--version";
|
||||
pub const FLAG_NOTIFY: &str = "-n";
|
||||
pub const FLAG_NOTIFY_LONG: &str = "--notify";
|
||||
pub const FLAG_SOUND: &str = "-s";
|
||||
pub const FLAG_SOUND_LONG: &str = "--sound";
|
||||
pub const FLAG_VISUALIZER: &str = "-z";
|
||||
pub const FLAG_VISUALIZER_LONG: &str = "--visualizer";
|
||||
|
||||
@@ -16,6 +18,7 @@ pub struct CliConfig {
|
||||
pub work_mins: u32,
|
||||
pub break_mins: u32,
|
||||
pub enable_notifications: bool,
|
||||
pub enable_sound: bool,
|
||||
pub enable_visualizer: bool,
|
||||
}
|
||||
|
||||
@@ -37,6 +40,7 @@ impl Cli {
|
||||
pub fn parse_args(args: Vec<String>) -> CliAction {
|
||||
let mut enable_notifications = false;
|
||||
let mut enable_visualizer = false;
|
||||
let mut enable_sound = false;
|
||||
let mut positional = Vec::new();
|
||||
|
||||
for arg in args.into_iter().skip(1) {
|
||||
@@ -44,6 +48,7 @@ impl Cli {
|
||||
FLAG_HELP | FLAG_HELP_LONG => return CliAction::PrintHelp,
|
||||
FLAG_VERSION | FLAG_VERSION_LONG => return CliAction::PrintVersion,
|
||||
FLAG_NOTIFY | FLAG_NOTIFY_LONG => enable_notifications = true,
|
||||
FLAG_SOUND | FLAG_SOUND_LONG => enable_sound = true,
|
||||
FLAG_VISUALIZER | FLAG_VISUALIZER_LONG => enable_visualizer = true,
|
||||
"--" => {}
|
||||
s if s.starts_with('-') => {
|
||||
@@ -69,6 +74,7 @@ impl Cli {
|
||||
work_mins,
|
||||
break_mins,
|
||||
enable_notifications,
|
||||
enable_sound,
|
||||
enable_visualizer,
|
||||
})
|
||||
}
|
||||
@@ -82,22 +88,27 @@ impl Cli {
|
||||
|
||||
println!(
|
||||
" {:<19} Enable desktop notifications",
|
||||
format_args!("{}, {}", FLAG_NOTIFY, FLAG_NOTIFY_LONG)
|
||||
format!("{}, {}", FLAG_NOTIFY, FLAG_NOTIFY_LONG)
|
||||
);
|
||||
|
||||
println!(
|
||||
" {:<19} Enable sound notifications",
|
||||
format!("{}, {}", FLAG_SOUND, FLAG_SOUND_LONG)
|
||||
);
|
||||
|
||||
println!(
|
||||
" {:<19} Display visualizer of playing audio",
|
||||
format_args!("{}, {}", FLAG_VISUALIZER, FLAG_VISUALIZER_LONG)
|
||||
format!("{}, {}", FLAG_VISUALIZER, FLAG_VISUALIZER_LONG)
|
||||
);
|
||||
|
||||
println!(
|
||||
" {:<19} Print this help message",
|
||||
format_args!("{}, {}", FLAG_HELP, FLAG_HELP_LONG)
|
||||
format!("{}, {}", FLAG_HELP, FLAG_HELP_LONG)
|
||||
);
|
||||
|
||||
println!(
|
||||
" {:<19} Print the version number",
|
||||
format_args!("{}, {}", FLAG_VERSION, FLAG_VERSION_LONG)
|
||||
format!("{}, {}", FLAG_VERSION, FLAG_VERSION_LONG)
|
||||
);
|
||||
|
||||
println!();
|
||||
@@ -126,6 +137,7 @@ mod tests {
|
||||
work_mins: 25,
|
||||
break_mins: 5,
|
||||
enable_notifications: false,
|
||||
enable_sound: false,
|
||||
enable_visualizer: false,
|
||||
})
|
||||
);
|
||||
@@ -145,6 +157,7 @@ mod tests {
|
||||
work_mins: 25,
|
||||
break_mins: 5,
|
||||
enable_notifications: false,
|
||||
enable_sound: false,
|
||||
enable_visualizer: true,
|
||||
})
|
||||
);
|
||||
@@ -165,6 +178,7 @@ mod tests {
|
||||
work_mins: 50,
|
||||
break_mins: 10,
|
||||
enable_notifications: true,
|
||||
enable_sound: false,
|
||||
enable_visualizer: true,
|
||||
})
|
||||
);
|
||||
@@ -194,6 +208,7 @@ mod tests {
|
||||
work_mins: 25,
|
||||
break_mins: 5,
|
||||
enable_notifications: false,
|
||||
enable_sound: false,
|
||||
enable_visualizer: false,
|
||||
})
|
||||
);
|
||||
|
||||
+3
-1
@@ -5,6 +5,7 @@ mod constants;
|
||||
mod events;
|
||||
mod msg;
|
||||
mod notification;
|
||||
mod sound;
|
||||
mod terminal;
|
||||
mod ui;
|
||||
|
||||
@@ -61,7 +62,8 @@ fn main() -> Result<(), io::Error> {
|
||||
};
|
||||
|
||||
let mut termato = Termato::new(config.work_mins, config.break_mins)
|
||||
.with_notifications(config.enable_notifications);
|
||||
.with_notifications(config.enable_notifications)
|
||||
.with_sound(config.enable_sound);
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
|
||||
const FREQ_D5_HZ: f32 = 587.33;
|
||||
const FREQ_A5_HZ: f32 = 880.00;
|
||||
|
||||
const NOTE_1_DURATION_SECS: f32 = 0.15;
|
||||
const NOTE_2_DURATION_SECS: f32 = 0.25;
|
||||
const TOTAL_CHIME_DURATION_SECS: f32 = NOTE_1_DURATION_SECS + NOTE_2_DURATION_SECS;
|
||||
const CHIME_VOLUME_GAIN: f32 = 0.25;
|
||||
|
||||
pub fn play_notification_sound() {
|
||||
thread::spawn(move || {
|
||||
// term bell fallback
|
||||
print!("\x07");
|
||||
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
let _ = play_audio_chime();
|
||||
});
|
||||
}
|
||||
|
||||
fn synthesize_sine_wave(freq_hz: f32, t_seconds: f32) -> f32 {
|
||||
(2.0 * std::f32::consts::PI * freq_hz * t_seconds).sin()
|
||||
}
|
||||
|
||||
fn calculate_linear_decay(t_seconds: f32, duration_secs: f32) -> f32 {
|
||||
(1.0 - (t_seconds / duration_secs)).max(0.0)
|
||||
}
|
||||
|
||||
fn play_audio_chime() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.ok_or("No default output audio device found")?;
|
||||
|
||||
let config: cpal::StreamConfig = device.default_output_config()?.into();
|
||||
|
||||
let sample_rate = config.sample_rate as f32;
|
||||
let channels = config.channels as usize;
|
||||
|
||||
let mut sample_clock = 0u64;
|
||||
let (tx, rx) = mpsc::channel::<()>();
|
||||
|
||||
let stream = device.build_output_stream(
|
||||
config,
|
||||
move |data: &mut [f32], _| {
|
||||
for frame in data.chunks_mut(channels) {
|
||||
// Convert sample counter to elapsed time in seconds
|
||||
let elapsed_time = sample_clock as f32 / sample_rate;
|
||||
sample_clock += 1;
|
||||
|
||||
let sample = if elapsed_time < NOTE_1_DURATION_SECS {
|
||||
let wave = synthesize_sine_wave(FREQ_D5_HZ, elapsed_time);
|
||||
let envelope = calculate_linear_decay(elapsed_time, NOTE_1_DURATION_SECS);
|
||||
wave * envelope * CHIME_VOLUME_GAIN
|
||||
} else if elapsed_time < TOTAL_CHIME_DURATION_SECS {
|
||||
let t_note2 = elapsed_time - NOTE_1_DURATION_SECS;
|
||||
let wave = synthesize_sine_wave(FREQ_A5_HZ, t_note2);
|
||||
let envelope = calculate_linear_decay(t_note2, NOTE_2_DURATION_SECS);
|
||||
wave * envelope * CHIME_VOLUME_GAIN
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
for channel in frame.iter_mut() {
|
||||
*channel = sample;
|
||||
}
|
||||
|
||||
if elapsed_time >= TOTAL_CHIME_DURATION_SECS {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
},
|
||||
|err| eprintln!("Audio output stream error: {}", err),
|
||||
None,
|
||||
)?;
|
||||
|
||||
stream.play()?;
|
||||
|
||||
let _ = rx.recv_timeout(Duration::from_millis(500));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user