Skip to main content

cvkg_render_native/
audio.rs

1use std::sync::Arc;
2
3// =============================================================================
4// AUDIO / HAPTIC ENGINES -- Cross-platform micro-feedback
5// =============================================================================
6
7/// Cross-platform audio engine using rodio for spatialized sound cues.
8/// Rodio 0.22 API: DeviceSinkBuilder::open_default_sink() → MixerDeviceSink.
9/// Playback via rodio::play(&mixer, cursor).
10pub struct RodioAudioEngine {
11    sink: rodio::MixerDeviceSink,
12}
13
14// MixerDeviceSink is not Send+Sync on some platforms, but we only use it
15// from the main thread. The AudioEngine trait requires Send+Sync for use in
16// App struct fields, which is safe here because we never move it across threads.
17unsafe impl Send for RodioAudioEngine {}
18unsafe impl Sync for RodioAudioEngine {}
19
20impl RodioAudioEngine {
21    /// Create a new audio engine. Falls back to None if audio init fails.
22    pub fn new() -> Option<Self> {
23        match rodio::DeviceSinkBuilder::open_default_sink() {
24            Ok(sink) => {
25                tracing::info!("[Native] Audio engine initialized (rodio)");
26                Some(Self { sink })
27            }
28            Err(e) => {
29                tracing::warn!("[Native] Audio init failed (no sound): {}", e);
30                None
31            }
32        }
33    }
34}
35
36impl cvkg_core::AudioEngine for RodioAudioEngine {
37    fn play_sound(&self, name: &str, volume: f32) {
38        let data: &[u8] = match name {
39            "nav_tick" => cvkg_core::sounds::NAVIGATION_TICK,
40            "success_chime" => cvkg_core::sounds::SUCCESS_CHIME,
41            "warning_tone" => cvkg_core::sounds::WARNING_TONE,
42            _ => {
43                tracing::warn!("[Native] Unknown sound: {}", name);
44                return;
45            }
46        };
47        self.play_buffer(data, volume);
48    }
49
50    fn play_buffer(&self, data: &[u8], _volume: f32) {
51        use std::io::Cursor;
52        let cursor = Cursor::new(data.to_vec());
53        let mixer = self.sink.mixer();
54        match rodio::play(mixer, cursor) {
55            Ok(_sink) => {}
56            Err(e) => tracing::warn!("[Native] Audio play failed: {}", e),
57        }
58    }
59
60    fn play_spatial(&self, name: &str, _position: [f32; 3], volume: f32) {
61        // Spatial audio: play sound without positional attenuation (OS-agnostic fallback)
62        self.play_sound(name, volume);
63    }
64}
65
66/// Visual haptic engine that translates haptic requests into visual micro-animations.
67/// Used as a cross-platform fallback where native haptics are unavailable.
68pub struct VisualHapticEngine {
69    last_impact: std::sync::Mutex<std::time::Instant>,
70}
71
72impl Default for VisualHapticEngine {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl VisualHapticEngine {
79    pub fn new() -> Self {
80        Self {
81            last_impact: std::sync::Mutex::new(std::time::Instant::now()),
82        }
83    }
84}
85
86impl cvkg_core::HapticEngine for VisualHapticEngine {
87    fn impact(&self, intensity: cvkg_core::HapticIntensity) {
88        let _ = intensity;
89        *self.last_impact.lock().unwrap_or_else(|p| p.into_inner()) = std::time::Instant::now();
90    }
91    fn selection(&self) {
92        self.impact(cvkg_core::HapticIntensity::Light);
93    }
94    fn success(&self) {
95        self.impact(cvkg_core::HapticIntensity::Medium);
96    }
97    fn warning(&self) {
98        self.impact(cvkg_core::HapticIntensity::Medium);
99    }
100    fn error(&self) {
101        self.impact(cvkg_core::HapticIntensity::Heavy);
102    }
103    fn visual_tick(&self, _intensity: f32) {
104        *self.last_impact.lock().unwrap_or_else(|p| p.into_inner()) = std::time::Instant::now();
105    }
106}