Skip to main content

hard_sync_core/
sounds.rs

1use std::path::{Path, PathBuf};
2use std::thread;
3use std::time::Duration;
4
5use crate::config::SoundConfig;
6
7// ── Public types ──────────────────────────────────────────────────────────────
8
9#[derive(Clone, Copy)]
10pub enum SoundEvent {
11    SyncStart,
12    SyncDone,
13    SyncError,
14}
15
16// ── Public API ────────────────────────────────────────────────────────────────
17
18/// Play the configured sound for a given event.
19/// Falls back to a synthesized tone when no custom path is configured.
20/// Playback runs on a background thread — this call returns immediately.
21/// Audio errors are silently ignored (sound never blocks sync).
22pub fn play_event_sound(config: &SoundConfig, event: SoundEvent) {
23    let path = match event {
24        SoundEvent::SyncStart => config.sync_start.as_ref(),
25        SoundEvent::SyncDone  => config.sync_done.as_ref(),
26        SoundEvent::SyncError => config.sync_error.as_ref(),
27    };
28
29    match path {
30        Some(p) => play_file_async(p.clone()),
31        None    => play_default_async(event),
32    }
33}
34
35// ── Internal — file playback ──────────────────────────────────────────────────
36
37fn play_file_async(path: PathBuf) {
38    thread::spawn(move || {
39        let _ = play_file_blocking(&path);
40    });
41}
42
43fn play_file_blocking(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
44    let handle = rodio::DeviceSinkBuilder::open_default_sink()?;
45    let file   = std::fs::File::open(path)?;
46    let player = rodio::play(&handle.mixer(), std::io::BufReader::new(file))?;
47    player.sleep_until_end();
48    Ok(())
49}
50
51// ── Internal — synthesized default tones ─────────────────────────────────────
52
53fn play_default_async(event: SoundEvent) {
54    thread::spawn(move || {
55        let _ = play_default_blocking(event);
56    });
57}
58
59fn play_default_blocking(event: SoundEvent) -> Result<(), Box<dyn std::error::Error>> {
60    use rodio::source::{SineWave, Source};
61
62    let mut handle = rodio::DeviceSinkBuilder::open_default_sink()?;
63    handle.log_on_drop(false); // we manage lifetime intentionally; suppress the warning
64
65    // mixer().add() accepts any Source; we sleep the known duration to block
66    let play_tone = |freq: f32, ms: u64, vol: f32| {
67        let src = SineWave::new(freq).take_duration(Duration::from_millis(ms)).amplify(vol);
68        handle.mixer().add(src);
69        std::thread::sleep(Duration::from_millis(ms + 20));
70    };
71
72    match event {
73        SoundEvent::SyncStart => play_tone(660.0, 180, 0.4),
74        SoundEvent::SyncDone => {
75            play_tone(523.0, 120, 0.4);
76            play_tone(784.0, 200, 0.4);
77        }
78        SoundEvent::SyncError => {
79            play_tone(440.0, 120, 0.4);
80            play_tone(220.0, 220, 0.4);
81        }
82    }
83    Ok(())
84}