feat(clock): sound notification (experimental) (#62)

This commit is contained in:
Jens Krause
2025-02-04 17:28:41 +01:00
committed by GitHub
parent 8f50bc5fc6
commit a54b1b409a
10 changed files with 818 additions and 52 deletions

View File

@@ -1,28 +1,72 @@
use rodio::{Decoder, OutputStream, Sink};
use std::fs;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SoundError {
#[error("Sound output stream error: {0}")]
OutputStream(String),
#[error("Sound file error: {0}")]
File(String),
#[error("Sound sink error: {0}")]
Sink(String),
#[error("Sound decoder error: {0}")]
Decoder(String),
}
pub fn validate_sound_file(path: &PathBuf) -> Result<&PathBuf, SoundError> {
// validate path
if !path.exists() {
let err = SoundError::File(format!("File not found: {:?}", path));
return Err(err);
};
// Validate file extension
path.extension()
.and_then(|ext| ext.to_str())
.filter(|ext| ["mp3", "wav"].contains(&ext.to_lowercase().as_str()))
.ok_or_else(|| {
SoundError::File(
"Unsupported file extension. Only .mp3 and .wav are supported".to_owned(),
)
})?;
Ok(path)
}
// #[derive(Clone)]
pub struct Sound {
sound_data: Vec<u8>,
path: PathBuf,
}
impl Sound {
pub fn new(path: &str) -> Result<Self, String> {
let sound_data = fs::read(path).map_err(|e| format!("Failed to read sound file: {}", e))?;
Ok(Self { sound_data })
pub fn new(path: PathBuf) -> Result<Self, SoundError> {
Ok(Self { path })
}
pub fn play(&self) -> Result<(), String> {
let (_stream, stream_handle) = OutputStream::try_default()
.map_err(|e| format!("Failed to get audio output: {}", e))?;
pub fn play(&self) -> Result<(), SoundError> {
// validate file again
validate_sound_file(&self.path)?;
// before playing the sound
let path = self.path.clone();
let sink = Sink::try_new(&stream_handle)
.map_err(|e| format!("Failed to create audio sink: {}", e))?;
std::thread::spawn(move || -> Result<(), SoundError> {
// Important note: Never (ever) use a single `_` as a placeholder here. `_stream` or something is fine!
// The value will dropped and the sound will fail without any errors
// see https://github.com/RustAudio/rodio/issues/330
let (_stream, handle) =
OutputStream::try_default().map_err(|e| SoundError::OutputStream(e.to_string()))?;
let file = File::open(&path).map_err(|e| SoundError::File(e.to_string()))?;
let sink = Sink::try_new(&handle).map_err(|e| SoundError::Sink(e.to_string()))?;
let decoder = Decoder::new(BufReader::new(file))
.map_err(|e| SoundError::Decoder(e.to_string()))?;
sink.append(decoder);
sink.sleep_until_end();
let cursor = std::io::Cursor::new(self.sound_data.clone());
let source = Decoder::new(cursor).map_err(|e| format!("Failed to decode audio: {}", e))?;
sink.append(source);
sink.sleep_until_end();
Ok(())
});
Ok(())
}