Skip to main content

cvkg_core/
audio_haptic.rs

1//! Audio and Haptic Feedback -- Item 14
2//!
3//! OS-agnostic audio and haptic feedback abstractions.
4//! Platform implementations are behind feature flags.
5//!
6//! # OS-agnostic design
7//! The traits here use no platform-specific types. Platform backends
8//! are selected via cfg flags in the renderer, not here.
9
10use std::sync::Arc;
11use std::sync::Mutex;
12
13/// Audio engine trait for playing sounds and spatial audio.
14///
15/// Implementations are platform-specific:
16/// - Desktop: rodio or cpal backend
17/// - Web: Web Audio API via wasm-bindgen
18pub trait AudioEngine: Send + Sync {
19    /// Play a named sound at the given volume (0.0 to 1.0).
20    fn play_sound(&self, name: &str, volume: f32);
21
22    /// Play a spatial sound at a 3D position relative to the listener.
23    fn play_spatial(&self, name: &str, position: [f32; 3], volume: f32);
24
25    /// Set the listener's position in 3D space for spatial audio.
26    fn set_listener_position(&self, _position: [f32; 3]) {}
27
28    /// Stop all currently playing sounds.
29    fn stop_all(&self) {}
30
31    /// Play an embedded audio buffer (e.g., from include_bytes!) at the given volume.
32    ///
33    /// The data slice must contain a valid WAV, OGG, or other supported audio format.
34    /// Default implementation is a no-op; backends that support buffer playback
35    /// should override this method.
36    fn play_buffer(&self, _data: &[u8], _volume: f32) {}
37}
38
39/// No-op audio engine used when no audio backend is available.
40pub struct NullAudioEngine;
41
42impl AudioEngine for NullAudioEngine {
43    fn play_sound(&self, _name: &str, _volume: f32) {}
44    fn play_spatial(&self, _name: &str, _position: [f32; 3], _volume: f32) {}
45    fn set_listener_position(&self, _position: [f32; 3]) {}
46    fn stop_all(&self) {}
47}
48
49/// Haptic feedback engine trait.
50///
51/// Implementations are platform-specific:
52/// - macOS: Core Haptics via objc2
53/// - iOS: UIImpactFeedbackGenerator via objc2
54/// - Web: Vibration API via wasm-bindgen
55/// - Other: no-op
56pub trait HapticEngine: Send + Sync {
57    /// Trigger an impact haptic with the given intensity.
58    fn impact(&self, _intensity: HapticIntensity) {}
59
60    /// Light tap for selection changes (e.g., picker wheel, toggle).
61    fn selection(&self) {}
62
63    /// Success notification haptic.
64    fn success(&self) {}
65
66    /// Warning notification haptic.
67    fn warning(&self) {}
68
69    /// Error notification haptic.
70    fn error(&self) {}
71
72    /// Visual micro-feedback tick -- subtle visual pulse for UI interactions.
73    ///
74    /// Unlike haptic methods which trigger physical feedback, this triggers
75    /// a brief visual animation (e.g., a glow or scale pulse) synchronized
76    /// with the interaction. Intensity ranges from 0.0 (barely visible) to
77    /// 1.0 (strong). Default implementation is a no-op.
78    fn visual_tick(&self, _intensity: f32) {}
79}
80
81/// Haptic impact intensity levels.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum HapticIntensity {
84    Light,
85    Medium,
86    Heavy,
87}
88
89/// No-op haptic engine used when no haptic backend is available.
90pub struct NullHapticEngine;
91
92impl HapticEngine for NullHapticEngine {
93    fn impact(&self, _intensity: HapticIntensity) {}
94    fn selection(&self) {}
95    fn success(&self) {}
96    fn warning(&self) {}
97    fn error(&self) {}
98    fn visual_tick(&self, _intensity: f32) {}
99}
100
101/// Named sound constants for the design system.
102///
103/// These are string identifiers for platform-loaded sounds.
104pub mod sounds {
105    pub const CLICK: &str = "click";
106    pub const TOGGLE_ON: &str = "toggle_on";
107    pub const TOGGLE_OFF: &str = "toggle_off";
108    pub const SUCCESS: &str = "success";
109    pub const ERROR: &str = "error";
110    pub const WARNING: &str = "warning";
111    pub const SCRUB: &str = "scrub";
112    pub const SELECTION: &str = "selection";
113
114    /// Embedded WAV data for the navigation tick sound.
115    ///
116    /// Play via `AudioEngine::play_buffer(sounds::NAVIGATION_TICK, 1.0)`.
117    pub const NAVIGATION_TICK: &[u8] = include_bytes!("../assets/sounds/nav_tick.wav");
118
119    /// Embedded WAV data for the success chime sound.
120    pub const SUCCESS_CHIME: &[u8] = include_bytes!("../assets/sounds/success_chime.wav");
121
122    /// Embedded WAV data for the warning tone sound.
123    pub const WARNING_TONE: &[u8] = include_bytes!("../assets/sounds/warning_tone.wav");
124}
125
126/// Global audio engine instance (wrapped in Mutex for runtime replacement).
127static AUDIO_ENGINE: once_cell::sync::Lazy<Mutex<Arc<dyn AudioEngine>>> =
128    once_cell::sync::Lazy::new(|| Mutex::new(Arc::new(NullAudioEngine)));
129
130/// Global haptic engine instance (wrapped in Mutex for runtime replacement).
131static HAPTIC_ENGINE: once_cell::sync::Lazy<Mutex<Arc<dyn HapticEngine>>> =
132    once_cell::sync::Lazy::new(|| Mutex::new(Arc::new(NullHapticEngine)));
133
134/// Set the global audio engine.
135pub fn set_audio_engine(engine: Arc<dyn AudioEngine>) {
136    if let Ok(mut guard) = AUDIO_ENGINE.lock() {
137        *guard = engine;
138    }
139}
140
141/// Set the global haptic engine.
142pub fn set_haptic_engine(engine: Arc<dyn HapticEngine>) {
143    if let Ok(mut guard) = HAPTIC_ENGINE.lock() {
144        *guard = engine;
145    }
146}
147
148/// Play a sound using the global audio engine.
149pub fn play_sound(name: &str, volume: f32) {
150    if let Ok(guard) = AUDIO_ENGINE.lock() {
151        guard.play_sound(name, volume);
152    }
153}
154
155/// Trigger a haptic using the global haptic engine.
156pub fn haptic_impact(intensity: HapticIntensity) {
157    if let Ok(guard) = HAPTIC_ENGINE.lock() {
158        guard.impact(intensity);
159    }
160}
161
162/// Trigger selection haptic.
163pub fn haptic_selection() {
164    if let Ok(guard) = HAPTIC_ENGINE.lock() {
165        guard.selection();
166    }
167}
168
169/// Trigger success haptic.
170pub fn haptic_success() {
171    if let Ok(guard) = HAPTIC_ENGINE.lock() {
172        guard.success();
173    }
174}
175
176/// Trigger error haptic.
177pub fn haptic_error() {
178    if let Ok(guard) = HAPTIC_ENGINE.lock() {
179        guard.error();
180    }
181}