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