1use kira::AudioManager as KiraManager;
14use kira::AudioManagerSettings;
15use kira::DefaultBackend;
16use kira::Tween;
17use kira::sound::static_sound::{StaticSoundData, StaticSoundHandle};
18use kira::track::{TrackBuilder, TrackHandle};
19use std::io::Cursor;
20use std::time::Duration;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum AudioTrack {
25 Music,
26 Ambience,
27 Sfx,
28 Ui,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub enum UiAudioEvent {
34 Hover,
35 Click,
36 CheckboxOn,
37 CheckboxOff,
38 TabChange,
39}
40
41struct Tracks {
42 music: TrackHandle,
43 ambience: TrackHandle,
44 sfx: TrackHandle,
45 ui: TrackHandle,
46}
47
48struct ActiveSounds {
50 current_music: Option<StaticSoundHandle>,
51 current_ambience: Option<StaticSoundHandle>,
52 current_loop_sfx: Option<StaticSoundHandle>,
53}
54
55pub struct AudioManager {
57 inner: Option<KiraManager<DefaultBackend>>,
58 tracks: Option<Tracks>,
59 active: ActiveSounds,
60 master_volume: f64,
61 music_volume: f64,
62 ambience_volume: f64,
63 sfx_volume: f64,
64 ui_volume: f64,
65 init_attempted: bool,
66 #[cfg(target_arch = "wasm32")]
67 awaiting_user_gesture: bool,
68}
69
70impl Default for AudioManager {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl AudioManager {
77 pub fn new() -> Self {
78 let mgr = Self {
79 inner: None,
80 tracks: None,
81 active: ActiveSounds {
82 current_music: None,
83 current_ambience: None,
84 current_loop_sfx: None,
85 },
86 master_volume: 1.0,
87 music_volume: 1.0,
88 ambience_volume: 1.0,
89 sfx_volume: 1.0,
90 ui_volume: 1.0,
91 init_attempted: false,
92 #[cfg(target_arch = "wasm32")]
93 awaiting_user_gesture: true,
94 };
95 #[cfg(not(target_arch = "wasm32"))]
96 {
97 let mut mgr = mgr;
98 mgr.try_init();
99 mgr
100 }
101 #[cfg(target_arch = "wasm32")]
102 {
103 mgr
104 }
105 }
106
107 fn try_init(&mut self) {
110 if self.inner.is_some() {
111 return;
112 }
113 self.init_attempted = true;
114
115 match KiraManager::<DefaultBackend>::new(AudioManagerSettings::default()) {
116 Ok(mut manager) => {
117 let music = manager.add_sub_track(TrackBuilder::default());
118 let ambience = manager.add_sub_track(TrackBuilder::default());
119 let sfx = manager.add_sub_track(TrackBuilder::default());
120 let ui = manager.add_sub_track(TrackBuilder::default());
121
122 match (music, ambience, sfx, ui) {
123 (Ok(music), Ok(ambience), Ok(sfx), Ok(ui)) => {
124 self.tracks = Some(Tracks {
125 music,
126 ambience,
127 sfx,
128 ui,
129 });
130 self.inner = Some(manager);
131 log::info!("Audio system initialized successfully");
132 }
133 _ => {
134 log::warn!("Audio: failed to create sub-tracks");
135 }
136 }
137 }
138 Err(e) => {
139 log::warn!("Audio: backend init failed ({e}), will retry on next play");
140 }
141 }
142 }
143
144 fn ensure_init(&mut self) -> bool {
146 #[cfg(target_arch = "wasm32")]
147 if self.awaiting_user_gesture {
148 return false;
150 }
151
152 if self.inner.is_none() {
153 self.try_init();
154 }
155 self.inner.is_some()
156 }
157
158 pub fn notify_user_gesture(&mut self) {
160 #[cfg(target_arch = "wasm32")]
161 {
162 if self.awaiting_user_gesture {
163 self.awaiting_user_gesture = false;
164 self.try_init();
165 }
166 }
167 }
168
169 fn track_handle(&mut self, track: AudioTrack) -> Option<&mut TrackHandle> {
170 self.tracks.as_mut().map(|t| match track {
171 AudioTrack::Music => &mut t.music,
172 AudioTrack::Ambience => &mut t.ambience,
173 AudioTrack::Sfx => &mut t.sfx,
174 AudioTrack::Ui => &mut t.ui,
175 })
176 }
177
178 pub fn effective_volume(&self, track: AudioTrack) -> f64 {
181 let track_vol = match track {
182 AudioTrack::Music => self.music_volume,
183 AudioTrack::Ambience => self.ambience_volume,
184 AudioTrack::Sfx => self.sfx_volume,
185 AudioTrack::Ui => self.ui_volume,
186 };
187 self.master_volume * track_vol
188 }
189
190 pub fn has_active_music(&self) -> bool {
192 self.active.current_music.is_some()
193 }
194
195 pub fn has_active_ambience(&self) -> bool {
197 self.active.current_ambience.is_some()
198 }
199
200 fn amplitude_to_db(amp: f64) -> kira::Decibels {
202 if amp <= 0.0 {
203 kira::Decibels::SILENCE
204 } else {
205 kira::Decibels((20.0_f64 * amp.log10()).max(-60.0) as f32)
206 }
207 }
208
209 pub fn play_oneshot(&mut self, data: &StaticSoundData, track: AudioTrack) {
210 if !self.ensure_init() {
211 return;
212 }
213 let vol = self.effective_volume(track);
214 if let Some(track_handle) = self.track_handle(track) {
215 let sound = data.clone().volume(Self::amplitude_to_db(vol));
216 let _ = track_handle.play(sound);
217 }
218 }
219
220 pub fn play_music(&mut self, data: &StaticSoundData, fade_in_secs: f32) {
222 if !self.ensure_init() {
223 return;
224 }
225 self.stop_music(fade_in_secs);
226
227 let vol = self.effective_volume(AudioTrack::Music);
228 let sound = data
229 .clone()
230 .loop_region(..)
231 .volume(Self::amplitude_to_db(vol))
232 .fade_in_tween(Tween {
233 duration: Duration::from_secs_f32(fade_in_secs),
234 ..Default::default()
235 });
236
237 if let Some(track_handle) = self.track_handle(AudioTrack::Music) {
238 match track_handle.play(sound) {
239 Ok(handle) => self.active.current_music = Some(handle),
240 Err(e) => log::warn!("Audio: failed to play music: {e}"),
241 }
242 }
243 }
244
245 pub fn stop_music(&mut self, fade_out_secs: f32) {
246 if let Some(ref mut handle) = self.active.current_music {
247 handle.stop(Tween {
248 duration: Duration::from_secs_f32(fade_out_secs),
249 ..Default::default()
250 });
251 }
252 self.active.current_music = None;
253 }
254
255 pub fn play_ambience(&mut self, data: &StaticSoundData, fade_in_secs: f32) {
257 if !self.ensure_init() {
258 return;
259 }
260 self.stop_ambience(fade_in_secs);
261
262 let vol = self.effective_volume(AudioTrack::Ambience);
263 let sound = data
264 .clone()
265 .loop_region(..)
266 .volume(Self::amplitude_to_db(vol))
267 .fade_in_tween(Tween {
268 duration: Duration::from_secs_f32(fade_in_secs),
269 ..Default::default()
270 });
271
272 if let Some(track_handle) = self.track_handle(AudioTrack::Ambience) {
273 match track_handle.play(sound) {
274 Ok(handle) => self.active.current_ambience = Some(handle),
275 Err(e) => log::warn!("Audio: failed to play ambience: {e}"),
276 }
277 }
278 }
279
280 pub fn stop_ambience(&mut self, fade_out_secs: f32) {
281 if let Some(ref mut handle) = self.active.current_ambience {
282 handle.stop(Tween {
283 duration: Duration::from_secs_f32(fade_out_secs),
284 ..Default::default()
285 });
286 }
287 self.active.current_ambience = None;
288 }
289
290 pub fn stop_loop_sfx(&mut self, fade_out_secs: f32) {
291 if let Some(ref mut handle) = self.active.current_loop_sfx {
292 handle.stop(Tween {
293 duration: Duration::from_secs_f32(fade_out_secs),
294 ..Default::default()
295 });
296 }
297 self.active.current_loop_sfx = None;
298 }
299
300 pub fn play_loop_sfx(&mut self, data: &StaticSoundData) {
302 if !self.ensure_init() {
303 return;
304 }
305 self.stop_loop_sfx(0.05);
306 let vol = self.effective_volume(AudioTrack::Sfx);
307 let sound = data.clone().loop_region(..);
308 if let Some(track_handle) = self.track_handle(AudioTrack::Sfx) {
309 match track_handle.play(sound.volume(Self::amplitude_to_db(vol))) {
310 Ok(handle) => self.active.current_loop_sfx = Some(handle),
311 Err(e) => log::warn!("Audio: loop sfx failed: {e}"),
312 }
313 }
314 }
315
316 pub fn set_music_live_volume(&mut self, amp: f64, fade_secs: f32) {
318 if let Some(ref mut handle) = self.active.current_music {
319 handle.set_volume(
320 Self::amplitude_to_db(amp.clamp(0.0, 1.0)),
321 Tween {
322 duration: Duration::from_secs_f32(fade_secs),
323 ..Default::default()
324 },
325 );
326 }
327 }
328
329 pub fn set_ambience_live_volume(&mut self, amp: f64, fade_secs: f32) {
331 if let Some(ref mut handle) = self.active.current_ambience {
332 handle.set_volume(
333 Self::amplitude_to_db(amp.clamp(0.0, 1.0)),
334 Tween {
335 duration: Duration::from_secs_f32(fade_secs),
336 ..Default::default()
337 },
338 );
339 }
340 }
341
342 pub fn set_master_volume(&mut self, volume: f64) {
343 self.master_volume = volume.clamp(0.0, 1.0);
344 }
345
346 pub fn set_music_volume(&mut self, volume: f64) {
347 self.music_volume = volume.clamp(0.0, 1.0);
348 }
349
350 pub fn set_ambience_volume(&mut self, volume: f64) {
351 self.ambience_volume = volume.clamp(0.0, 1.0);
352 }
353
354 pub fn set_sfx_volume(&mut self, volume: f64) {
355 self.sfx_volume = volume.clamp(0.0, 1.0);
356 }
357
358 pub fn set_ui_volume(&mut self, volume: f64) {
359 self.ui_volume = volume.clamp(0.0, 1.0);
360 }
361
362 pub fn master_volume(&self) -> f64 {
363 self.master_volume
364 }
365 pub fn music_volume(&self) -> f64 {
366 self.music_volume
367 }
368 pub fn ambience_volume(&self) -> f64 {
369 self.ambience_volume
370 }
371 pub fn sfx_volume(&self) -> f64 {
372 self.sfx_volume
373 }
374 pub fn ui_volume(&self) -> f64 {
375 self.ui_volume
376 }
377}
378
379pub fn load_sound_data(bytes: &'static [u8]) -> Option<StaticSoundData> {
381 match StaticSoundData::from_cursor(Cursor::new(bytes)) {
382 Ok(data) => Some(data),
383 Err(e) => {
384 log::warn!("Audio: failed to decode sound: {e}");
385 None
386 }
387 }
388}
389
390pub trait AudioResponse {
393 fn with_ui_sound(self, pending: &mut Vec<UiAudioEvent>) -> Self;
394 fn with_checkbox_sound(self, checked: bool, pending: &mut Vec<UiAudioEvent>) -> Self;
395 fn with_tab_sound(self, pending: &mut Vec<UiAudioEvent>) -> Self;
396}
397
398impl AudioResponse for egui::Response {
399 fn with_ui_sound(self, pending: &mut Vec<UiAudioEvent>) -> Self {
400 let id = self.id;
401 let was_hovered = self.ctx.data(|d| d.get_temp::<bool>(id).unwrap_or(false));
402 let now_hovered = self.hovered();
403 self.ctx.data_mut(|d| d.insert_temp(id, now_hovered));
404 if now_hovered && !was_hovered {
405 pending.push(UiAudioEvent::Hover);
406 }
407 if self.clicked() {
408 pending.push(UiAudioEvent::Click);
409 }
410 self
411 }
412
413 fn with_checkbox_sound(self, checked: bool, pending: &mut Vec<UiAudioEvent>) -> Self {
414 if self.changed() {
415 pending.push(if checked {
416 UiAudioEvent::CheckboxOn
417 } else {
418 UiAudioEvent::CheckboxOff
419 });
420 }
421 self
422 }
423
424 fn with_tab_sound(self, pending: &mut Vec<UiAudioEvent>) -> Self {
425 if self.clicked() {
426 pending.push(UiAudioEvent::TabChange);
427 }
428 self
429 }
430}