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}