diff options
Diffstat (limited to 'src-tauri')
| -rw-r--r-- | src-tauri/audio/README.md | 6 | ||||
| -rw-r--r-- | src-tauri/audio/cafe.ogg | bin | 254428 -> 0 bytes | |||
| -rw-r--r-- | src-tauri/audio/rain.ogg | bin | 256573 -> 0 bytes | |||
| -rw-r--r-- | src-tauri/audio/white_noise.ogg | bin | 263937 -> 0 bytes | |||
| -rw-r--r-- | src-tauri/src/audio.rs | 117 | ||||
| -rw-r--r-- | src-tauri/src/lib.rs | 9 | ||||
| -rw-r--r-- | src-tauri/tauri.conf.json | 3 |
7 files changed, 122 insertions, 13 deletions
diff --git a/src-tauri/audio/README.md b/src-tauri/audio/README.md new file mode 100644 index 0000000..422af3e --- /dev/null +++ b/src-tauri/audio/README.md @@ -0,0 +1,6 @@ +Place royalty-free OGG loop files here for local development. + +Expected filenames: +- `rain.ogg` +- `cafe.ogg` +- `white_noise.ogg` diff --git a/src-tauri/audio/cafe.ogg b/src-tauri/audio/cafe.ogg Binary files differdeleted file mode 100644 index cbc1007..0000000 --- a/src-tauri/audio/cafe.ogg +++ /dev/null diff --git a/src-tauri/audio/rain.ogg b/src-tauri/audio/rain.ogg Binary files differdeleted file mode 100644 index fd5d478..0000000 --- a/src-tauri/audio/rain.ogg +++ /dev/null diff --git a/src-tauri/audio/white_noise.ogg b/src-tauri/audio/white_noise.ogg Binary files differdeleted file mode 100644 index fcbd7bc..0000000 --- a/src-tauri/audio/white_noise.ogg +++ /dev/null diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index a439a0c..11c970e 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -1,7 +1,7 @@ use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink, Source}; use std::fs::File; use std::io::BufReader; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread; @@ -17,6 +17,22 @@ pub enum AmbientSound { WhiteNoise, } +const EXPECTED_AUDIO_FILES: [&str; 3] = ["rain.ogg", "cafe.ogg", "white_noise.ogg"]; + +pub fn directory_has_any_audio_files(dir: &Path) -> bool { + EXPECTED_AUDIO_FILES + .iter() + .any(|filename| dir.join(filename).is_file()) +} + +pub fn choose_audio_dir(app_data_audio_dir: PathBuf, bundled_audio_dir: PathBuf) -> PathBuf { + if directory_has_any_audio_files(&app_data_audio_dir) { + app_data_audio_dir + } else { + bundled_audio_dir + } +} + impl AmbientSound { pub fn filename(&self) -> &'static str { match self { @@ -51,11 +67,16 @@ pub struct AudioEngine { volume: f32, ducked: bool, duck_volume: f32, - audio_dir: PathBuf, + app_data_audio_dir: PathBuf, + bundled_audio_dir: PathBuf, } impl AudioEngine { - fn new_with_handle(stream_handle: OutputStreamHandle, audio_dir: PathBuf) -> Self { + fn new_with_handle( + stream_handle: OutputStreamHandle, + app_data_audio_dir: PathBuf, + bundled_audio_dir: PathBuf, + ) -> Self { Self { stream_handle, sink: None, @@ -63,14 +84,33 @@ impl AudioEngine { volume: 0.5, ducked: false, duck_volume: 0.2, - audio_dir, + app_data_audio_dir, + bundled_audio_dir, + } + } + + fn resolve_audio_path(&self, filename: &str) -> PathBuf { + let app_data_path = self.app_data_audio_dir.join(filename); + if app_data_path.is_file() { + app_data_path + } else { + let bundled_path = self.bundled_audio_dir.join(filename); + if bundled_path.is_file() { + bundled_path + } else { + choose_audio_dir( + self.app_data_audio_dir.clone(), + self.bundled_audio_dir.clone(), + ) + .join(filename) + } } } pub fn play(&mut self, sound: AmbientSound) -> Result<(), String> { self.stop(); let filename = sound.filename(); - let path = self.audio_dir.join(filename); + let path = self.resolve_audio_path(filename); let file = File::open(&path).map_err(|e| format!("Cannot open {filename}: {e}"))?; let source = Decoder::new(BufReader::new(file)) .map_err(|e| format!("Decode error: {e}"))?; @@ -125,6 +165,11 @@ impl AudioEngine { pub fn volume(&self) -> f32 { self.volume } + + pub fn has_any_audio_files(&self) -> bool { + directory_has_any_audio_files(&self.app_data_audio_dir) + || directory_has_any_audio_files(&self.bundled_audio_dir) + } } pub fn should_duck_for_phase(phase: TimerPhase) -> bool { @@ -139,14 +184,14 @@ pub struct AudioState(pub Arc<Mutex<Option<AudioEngine>>>); /// /// Spawns a dedicated thread to keep `OutputStream` alive (it is `!Send`). /// Returns `AudioState` whose inner `Option` is `None` if no audio device exists. -pub fn init_audio(audio_dir: PathBuf) -> AudioState { +pub fn init_audio(app_data_audio_dir: PathBuf, bundled_audio_dir: PathBuf) -> AudioState { let state: Arc<Mutex<Option<AudioEngine>>> = Arc::new(Mutex::new(None)); let state_clone = Arc::clone(&state); thread::spawn(move || { match OutputStream::try_default() { Ok((_stream, handle)) => { - let engine = AudioEngine::new_with_handle(handle, audio_dir); + let engine = AudioEngine::new_with_handle(handle, app_data_audio_dir, bundled_audio_dir); { let mut guard = state_clone.lock().unwrap(); *guard = Some(engine); @@ -172,9 +217,29 @@ pub fn init_audio(audio_dir: PathBuf) -> AudioState { #[cfg(test)] mod tests { - use super::should_duck_for_phase; + use std::fs; + use std::path::Path; + use std::time::{SystemTime, UNIX_EPOCH}; + + use super::{choose_audio_dir, directory_has_any_audio_files, should_duck_for_phase}; use crate::state::TimerPhase; + fn temp_path(label: &str) -> std::path::PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + std::env::temp_dir().join(format!("solstice-audio-{label}-{unique}")) + } + + fn make_dir(path: &Path) { + fs::create_dir_all(path).unwrap(); + } + + fn write_audio_file(dir: &Path, name: &str) { + fs::write(dir.join(name), b"test-audio").unwrap(); + } + #[test] fn work_phase_does_not_duck() { assert!(!should_duck_for_phase(TimerPhase::Work)); @@ -185,4 +250,40 @@ mod tests { assert!(should_duck_for_phase(TimerPhase::ShortBreak)); assert!(should_duck_for_phase(TimerPhase::LongBreak)); } + + #[test] + fn detects_expected_audio_files_in_directory() { + let dir = temp_path("detect-files"); + make_dir(&dir); + write_audio_file(&dir, "white_noise.ogg"); + + assert!(directory_has_any_audio_files(&dir)); + } + + #[test] + fn prefers_app_data_audio_when_files_exist_there() { + let app_dir = temp_path("app-data"); + let bundled_dir = temp_path("bundled"); + make_dir(&app_dir); + make_dir(&bundled_dir); + write_audio_file(&app_dir, "rain.ogg"); + write_audio_file(&bundled_dir, "cafe.ogg"); + + let chosen = choose_audio_dir(app_dir.clone(), bundled_dir.clone()); + + assert_eq!(chosen, app_dir); + } + + #[test] + fn falls_back_to_bundled_audio_when_app_data_is_empty() { + let app_dir = temp_path("app-data-empty"); + let bundled_dir = temp_path("bundled-fallback"); + make_dir(&app_dir); + make_dir(&bundled_dir); + write_audio_file(&bundled_dir, "rain.ogg"); + + let chosen = choose_audio_dir(app_dir, bundled_dir.clone()); + + assert_eq!(chosen, bundled_dir); + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 11bb2f9..c298fdf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -338,7 +338,7 @@ fn get_audio_status(audio: State<'_, AudioState>) -> AudioStatus { match audio.0.lock().unwrap().as_ref() { None => AudioStatus { available: false, playing: false, sound: None, volume: 0.5 }, Some(engine) => AudioStatus { - available: true, + available: engine.has_any_audio_files(), playing: engine.is_playing(), sound: engine.current_sound().map(|s| s.name().to_string()), volume: engine.volume(), @@ -356,6 +356,9 @@ pub fn run() { .setup(|app| { let data_dir = app.handle().path().app_data_dir() .expect("Failed to resolve app data dir"); + let app_audio_dir = data_dir.join("audio"); + std::fs::create_dir_all(&app_audio_dir) + .expect("Failed to create app audio dir"); // Load persisted data let app_data = storage::load(&data_dir); @@ -376,10 +379,10 @@ pub fn run() { }); // Initialise audio engine (graceful if no device) - let audio_dir = app.handle().path().resource_dir() + let bundled_audio_dir = app.handle().path().resource_dir() .unwrap_or_else(|_| std::path::PathBuf::from(".")) .join("audio"); - let audio_state = audio::init_audio(audio_dir); + let audio_state = audio::init_audio(app_audio_dir, bundled_audio_dir); let audio_arc = Arc::clone(&audio_state.0); app.manage(audio_state); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e634dca..b39e99f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -34,7 +34,6 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ], - "resources": ["audio/*"] + ] } } |
