use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::OnceLock;
const SAMPLE_RATE: u32 = 44_100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tone {
Low,
High,
Stale,
}
pub fn alarm(tone: Tone) {
if let Some(path) = wav_path(tone) {
play(&path);
}
}
fn wav_path(tone: Tone) -> Option<PathBuf> {
static PATHS: OnceLock<[Option<PathBuf>; 3]> = OnceLock::new();
let paths = PATHS.get_or_init(|| {
let dir = std::env::var_os("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir);
[Tone::Low, Tone::High, Tone::Stale].map(|t| {
let path = dir.join(format!("sugarrush-alarm-{}.wav", t.suffix()));
std::fs::write(&path, alarm_wav(t)).ok().map(|_| path)
})
});
paths[tone.index()].clone()
}
impl Tone {
fn index(self) -> usize {
match self {
Tone::Low => 0,
Tone::High => 1,
Tone::Stale => 2,
}
}
fn suffix(self) -> &'static str {
match self {
Tone::Low => "low",
Tone::High => "high",
Tone::Stale => "stale",
}
}
fn freqs(self) -> [f64; 4] {
match self {
Tone::Low => [1320.0, 1100.0, 880.0, 660.0],
Tone::High => [660.0, 880.0, 1100.0, 1320.0],
Tone::Stale => [880.0, 0.0, 880.0, 0.0],
}
}
}
fn play(path: &Path) {
let candidates: [(&str, &[&str]); 7] = [
("paplay", &[]),
("pw-play", &[]),
("aplay", &["-q"]),
("ffplay", &["-nodisp", "-autoexit", "-loglevel", "quiet"]),
("canberra-gtk-play", &["-f"]), ("afplay", &[]), ("cvlc", &["--play-and-exit", "--intf", "dummy"]),
];
for (prog, args) in candidates {
let mut cmd = Command::new(prog);
if prog == "canberra-gtk-play" {
cmd.arg(format!("--file={}", path.display()));
} else {
cmd.args(args).arg(path);
}
let spawned = cmd
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
if spawned.is_ok() {
return;
}
}
}
fn alarm_wav(tone: Tone) -> Vec<u8> {
let mut samples: Vec<i16> = Vec::new();
for freq in tone.freqs() {
let n = SAMPLE_RATE as usize * 110 / 1000;
for i in 0..n {
let t = i as f64 / SAMPLE_RATE as f64;
let fade_len = (SAMPLE_RATE as f64 * 0.004) as usize;
let amp = if i < fade_len {
i as f64 / fade_len as f64
} else if i > n - fade_len {
(n - i) as f64 / fade_len as f64
} else {
1.0
};
let s = (t * freq * std::f64::consts::TAU).sin() * amp * 0.5;
samples.push((s * i16::MAX as f64) as i16);
}
}
encode_wav(&samples)
}
fn encode_wav(samples: &[i16]) -> Vec<u8> {
let data_len = (samples.len() * 2) as u32;
let mut out = Vec::with_capacity(44 + data_len as usize);
let byte_rate = SAMPLE_RATE * 2;
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(36 + data_len).to_le_bytes());
out.extend_from_slice(b"WAVE");
out.extend_from_slice(b"fmt ");
out.extend_from_slice(&16u32.to_le_bytes()); out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&SAMPLE_RATE.to_le_bytes());
out.extend_from_slice(&byte_rate.to_le_bytes());
out.extend_from_slice(&2u16.to_le_bytes()); out.extend_from_slice(&16u16.to_le_bytes()); out.extend_from_slice(b"data");
out.extend_from_slice(&data_len.to_le_bytes());
for &s in samples {
out.extend_from_slice(&s.to_le_bytes());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wav_has_valid_header() {
let wav = alarm_wav(Tone::Low);
assert_eq!(&wav[0..4], b"RIFF");
assert_eq!(&wav[8..12], b"WAVE");
assert_eq!(&wav[36..40], b"data");
let declared = u32::from_le_bytes([wav[40], wav[41], wav[42], wav[43]]) as usize;
assert_eq!(declared, wav.len() - 44);
}
}