summaryrefslogtreecommitdiff
path: root/src-tauri
diff options
context:
space:
mode:
authorSolstice <solstice@local>2026-06-09 00:58:04 -0700
committerSolstice <solstice@local>2026-06-09 00:58:04 -0700
commitf43f549ffbe3074977116c9f35aa7064d6a4bd95 (patch)
treec285fbcf8a768d0de2631a93e379d61cea0313f2 /src-tauri
parenta7e2a94ddb19864d4a1f78a420fcf66a9e38f4e5 (diff)
refactor: switch ambient audio to user-supplied assets
Diffstat (limited to 'src-tauri')
-rw-r--r--src-tauri/audio/README.md6
-rw-r--r--src-tauri/audio/cafe.oggbin254428 -> 0 bytes
-rw-r--r--src-tauri/audio/rain.oggbin256573 -> 0 bytes
-rw-r--r--src-tauri/audio/white_noise.oggbin263937 -> 0 bytes
-rw-r--r--src-tauri/src/audio.rs117
-rw-r--r--src-tauri/src/lib.rs9
-rw-r--r--src-tauri/tauri.conf.json3
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
deleted file mode 100644
index cbc1007..0000000
--- a/src-tauri/audio/cafe.ogg
+++ /dev/null
Binary files differ
diff --git a/src-tauri/audio/rain.ogg b/src-tauri/audio/rain.ogg
deleted file mode 100644
index fd5d478..0000000
--- a/src-tauri/audio/rain.ogg
+++ /dev/null
Binary files differ
diff --git a/src-tauri/audio/white_noise.ogg b/src-tauri/audio/white_noise.ogg
deleted file mode 100644
index fcbd7bc..0000000
--- a/src-tauri/audio/white_noise.ogg
+++ /dev/null
Binary files differ
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/*"]
+ ]
}
}