1#![forbid(unsafe_code)]
38
39use std::collections::HashMap;
40#[cfg(not(target_arch = "wasm32"))]
41use std::path::Path;
42use std::sync::Arc;
43
44use anyhow::{Result, anyhow};
45use kira::sound::static_sound::{StaticSoundData, StaticSoundSettings};
46use kira::track::{TrackBuilder, TrackHandle};
47use kira::{AudioManager, AudioManagerSettings, Decibels, Frame, Tween};
48
49mod handle;
50use handle::Slot;
51pub use handle::SoundHandle;
52mod stream;
53pub use kira;
54use stream::StreamData;
55pub use stream::{AudioStream, StreamHandle};
56
57#[cfg(target_arch = "wasm32")]
58mod web;
59#[cfg(target_arch = "wasm32")]
60pub use web::DetachHandle;
61
62#[cfg(target_arch = "wasm32")]
63type Backend = web::WebBackend;
64#[cfg(not(target_arch = "wasm32"))]
65type Backend = kira::DefaultBackend;
66
67#[cfg(feature = "synth")]
68mod music;
69#[cfg(feature = "synth")]
70use music::PreparedSynth;
71#[cfg(feature = "synth")]
72pub use music::SynthHandle;
73#[cfg(feature = "synth")]
74pub mod synth;
75#[cfg(feature = "synth")]
76pub use synth::Synth;
77#[cfg(feature = "synth")]
78pub use synthie;
79
80#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
81pub struct SoundId(pub u32);
82
83#[derive(Debug)]
85#[non_exhaustive]
86pub enum AudioError {
87 NotStarted,
91 UnknownSound(SoundId),
93 Backend(String),
95}
96
97impl std::fmt::Display for AudioError {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 match self {
100 Self::NotStarted => f.write_str("audio not started yet (waiting for a user gesture)"),
101 Self::UnknownSound(id) => write!(f, "unknown SoundId {id:?}"),
102 Self::Backend(e) => write!(f, "kira play: {e}"),
103 }
104 }
105}
106
107impl std::error::Error for AudioError {}
108
109#[derive(Copy, Clone, Debug, PartialEq)]
112pub struct PlayOptions {
113 pub looped: bool,
115 pub volume: f32,
117 pub deferred: bool,
121}
122
123impl Default for PlayOptions {
124 fn default() -> Self {
125 Self {
126 looped: false,
127 volume: 1.0,
128 deferred: false,
129 }
130 }
131}
132
133impl PlayOptions {
134 pub fn looped() -> Self {
136 Self {
137 looped: true,
138 ..Self::default()
139 }
140 }
141
142 pub fn volume(self, volume: f32) -> Self {
144 Self { volume, ..self }
145 }
146
147 pub fn deferred(self) -> Self {
149 Self {
150 deferred: true,
151 ..self
152 }
153 }
154}
155
156struct Manager {
158 kira: AudioManager<Backend>,
159 bus: TrackHandle,
162}
163
164pub struct Audio {
165 manager: Option<Manager>,
166 suspended: bool,
168 #[cfg(target_arch = "wasm32")]
169 detach: DetachHandle,
170 sounds: HashMap<SoundId, StaticSoundData>,
171 next_id: u32,
172 master_volume: Decibels,
174 queued: Vec<(StaticSoundData, SoundHandle)>,
176 queued_streams: Vec<StreamData>,
178 #[cfg(feature = "synth")]
180 queued_synths: Vec<PreparedSynth>,
181}
182
183impl Audio {
184 pub fn new() -> Result<Self> {
187 let mut audio = Self {
188 manager: None,
189 suspended: false,
190 #[cfg(target_arch = "wasm32")]
191 detach: DetachHandle::default(),
192 sounds: HashMap::new(),
193 next_id: 1,
194 master_volume: Decibels::IDENTITY,
195 queued: Vec::new(),
196 queued_streams: Vec::new(),
197 #[cfg(feature = "synth")]
198 queued_synths: Vec::new(),
199 };
200 if !cfg!(target_arch = "wasm32") {
201 audio.start()?;
202 }
203 Ok(audio)
204 }
205
206 pub fn start(&mut self) -> Result<()> {
210 if self.manager.is_some() {
211 return Ok(());
212 }
213 #[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))]
214 let mut settings = AudioManagerSettings::<Backend>::default();
215 #[cfg(target_arch = "wasm32")]
216 {
217 settings.backend_settings = self.detach.clone();
218 }
219 let mut kira = AudioManager::<Backend>::new(settings)
220 .map_err(|e| anyhow!("kira AudioManager init: {e}"))?;
221 kira.main_track()
222 .set_volume(self.master_volume, Tween::default());
223 let mut bus = kira
224 .add_sub_track(TrackBuilder::new())
225 .map_err(|e| anyhow!("kira bus track: {e}"))?;
226 if self.suspended {
227 bus.pause(Tween::default());
228 }
229 self.manager = Some(Manager { kira, bus });
230 for (data, handle) in std::mem::take(&mut self.queued) {
231 self.start_queued(data, &handle);
232 }
233 for data in std::mem::take(&mut self.queued_streams) {
234 if !data.handle().is_finished() {
235 self.start_stream(data);
236 }
237 }
238 #[cfg(feature = "synth")]
239 for prepared in std::mem::take(&mut self.queued_synths) {
240 if !prepared.handle().is_finished() {
241 self.start_synth(prepared);
242 }
243 }
244 Ok(())
245 }
246
247 fn start_queued(&mut self, data: StaticSoundData, handle: &SoundHandle) {
248 let mut slot = handle.lock();
249 let Slot::Pending { volume, paused } = &*slot else {
250 return; };
252 let (volume, paused) = (*volume, *paused);
253 let (data, fade) = match volume {
256 Some((db, duration)) if duration.is_zero() => (data.volume(db), None),
257 Some((db, duration)) => (data, Some((db, duration))),
258 None => (data, None),
259 };
260 match self.play_now(data) {
261 Ok(mut kira) => {
262 if let Some((db, duration)) = fade {
263 kira.set_volume(db, handle::tween(duration));
264 }
265 if paused {
266 kira.pause(Tween::default());
267 }
268 *slot = Slot::Playing(kira);
269 }
270 Err(e) => {
271 log::warn!("queued sound failed to start: {e}");
272 *slot = Slot::Cancelled;
273 }
274 }
275 }
276
277 pub fn is_started(&self) -> bool {
280 self.manager.is_some()
281 }
282
283 #[cfg(not(target_arch = "wasm32"))]
285 pub fn load_sound(&mut self, path: impl AsRef<Path>) -> Result<SoundId> {
286 let data = StaticSoundData::from_file(path.as_ref())
287 .map_err(|e| anyhow!("kira load {}: {e}", path.as_ref().display()))?;
288 Ok(self.insert(data))
289 }
290
291 pub fn load_sound_bytes(&mut self, bytes: impl Into<Vec<u8>>) -> Result<SoundId> {
293 let data = StaticSoundData::from_cursor(std::io::Cursor::new(bytes.into()))
294 .map_err(|e| anyhow!("kira decode: {e}"))?;
295 Ok(self.insert(data))
296 }
297
298 fn insert(&mut self, data: StaticSoundData) -> SoundId {
299 let id = SoundId(self.next_id);
300 self.next_id += 1;
301 self.sounds.insert(id, data);
302 id
303 }
304
305 pub fn play(&mut self, id: SoundId) -> Result<SoundHandle, AudioError> {
313 self.play_with(id, PlayOptions::default())
314 }
315
316 pub fn play_with(
319 &mut self,
320 id: SoundId,
321 options: PlayOptions,
322 ) -> Result<SoundHandle, AudioError> {
323 let data = self
324 .sounds
325 .get(&id)
326 .ok_or(AudioError::UnknownSound(id))?
327 .clone();
328 self.play_data(data, options)
329 }
330
331 pub fn play_samples(
335 &mut self,
336 samples: &[f32],
337 sample_rate: u32,
338 ) -> Result<SoundHandle, AudioError> {
339 self.play_samples_with(samples, sample_rate, PlayOptions::default())
340 }
341
342 pub fn play_samples_with(
344 &mut self,
345 samples: &[f32],
346 sample_rate: u32,
347 options: PlayOptions,
348 ) -> Result<SoundHandle, AudioError> {
349 let frames: Arc<[Frame]> = samples
350 .iter()
351 .map(|&s| Frame { left: s, right: s })
352 .collect();
353 let data = StaticSoundData {
354 sample_rate,
355 frames,
356 settings: StaticSoundSettings::default(),
357 slice: None,
358 };
359 self.play_data(data, options)
360 }
361
362 fn play_data(
363 &mut self,
364 data: StaticSoundData,
365 options: PlayOptions,
366 ) -> Result<SoundHandle, AudioError> {
367 let mut data = data.volume(amplitude_to_db(options.volume));
368 if options.looped {
369 data = data.loop_region(..);
370 }
371 if self.manager.is_none() && options.deferred {
372 let handle = SoundHandle::pending();
373 self.queued.push((data, handle.clone()));
374 return Ok(handle);
375 }
376 self.play_now(data).map(SoundHandle::playing)
377 }
378
379 fn play_now(
380 &mut self,
381 data: StaticSoundData,
382 ) -> Result<kira::sound::static_sound::StaticSoundHandle, AudioError> {
383 self.manager
384 .as_mut()
385 .ok_or(AudioError::NotStarted)?
386 .bus
387 .play(data)
388 .map_err(|e| AudioError::Backend(e.to_string()))
389 }
390
391 pub fn play_stream(
396 &mut self,
397 stream: impl AudioStream,
398 sample_rate: u32,
399 ) -> Result<StreamHandle, AudioError> {
400 self.play_stream_with(stream, sample_rate, PlayOptions::default())
401 }
402
403 pub fn play_stream_with(
406 &mut self,
407 stream: impl AudioStream,
408 sample_rate: u32,
409 options: PlayOptions,
410 ) -> Result<StreamHandle, AudioError> {
411 let pending = self.manager.is_none();
412 if pending && !options.deferred {
413 return Err(AudioError::NotStarted);
414 }
415 let (data, handle) =
416 StreamData::new(Box::new(stream), sample_rate, options.volume, pending);
417 if pending {
418 self.queued_streams.push(data);
419 } else {
420 self.start_stream(data);
421 }
422 Ok(handle)
423 }
424
425 fn start_stream(&mut self, data: StreamData) {
426 let handle = data.handle().clone();
427 let Some(manager) = self.manager.as_mut() else {
428 return;
429 };
430 match manager.bus.play(data) {
431 Ok(()) => handle.mark_started(),
432 Err(e) => {
433 log::warn!("stream failed to start: {e}");
434 handle.stop();
435 }
436 }
437 }
438
439 #[cfg(feature = "synth")]
465 pub fn play_synth(
466 &mut self,
467 mut song: synth::Song,
468 options: PlayOptions,
469 ) -> Result<SynthHandle, AudioError> {
470 song.looped = options.looped;
471 let data =
472 synthie::kira::SynthSoundData::new().with_sequencer(synth::SongPlayer::new(song));
473 self.play_synth_sound(data, options)
474 }
475
476 #[cfg(feature = "synth")]
481 pub fn play_synth_sound<S, const N: usize>(
482 &mut self,
483 data: synthie::kira::SynthSoundData<S, N>,
484 options: PlayOptions,
485 ) -> Result<SynthHandle, AudioError>
486 where
487 S: synthie::prelude::Sequencer + Send + 'static,
488 {
489 let pending = self.manager.is_none();
490 if pending && !options.deferred {
491 return Err(AudioError::NotStarted);
492 }
493 let data = data.with_volume(amplitude_to_db(options.volume));
494 let prepared = PreparedSynth::new(data, pending);
495 let handle = prepared.handle().clone();
496 if pending {
497 self.queued_synths.push(prepared);
498 } else {
499 self.start_synth(prepared);
500 }
501 Ok(handle)
502 }
503
504 #[cfg(feature = "synth")]
505 fn start_synth(&mut self, prepared: PreparedSynth) {
506 let handle = prepared.handle().clone();
507 let Some(manager) = self.manager.as_mut() else {
508 return;
509 };
510 match manager.bus.play(prepared) {
511 Ok(()) => handle.mark_started(),
512 Err(e) => {
513 log::warn!("synth failed to start: {e}");
514 handle.stop();
515 }
516 }
517 }
518
519 pub fn set_master_volume(&mut self, volume: f32) {
522 self.master_volume = amplitude_to_db(volume);
523 if let Some(manager) = self.manager.as_mut() {
524 manager
525 .kira
526 .main_track()
527 .set_volume(self.master_volume, Tween::default());
528 }
529 }
530
531 pub fn suspend(&mut self) {
539 self.set_suspended(true);
540 }
541
542 pub fn resume(&mut self) {
544 self.set_suspended(false);
545 }
546
547 pub fn is_suspended(&self) -> bool {
549 self.suspended
550 }
551
552 fn set_suspended(&mut self, suspended: bool) {
553 if self.suspended == suspended {
554 return;
555 }
556 self.suspended = suspended;
557 if let Some(manager) = self.manager.as_mut() {
558 if suspended {
559 manager.bus.pause(Tween::default());
560 } else {
561 manager.bus.resume(Tween::default());
562 }
563 }
564 }
565
566 #[cfg(target_arch = "wasm32")]
569 pub fn detach_handle(&self) -> DetachHandle {
570 self.detach.clone()
571 }
572}
573
574pub(crate) fn amplitude_to_db(volume: f32) -> Decibels {
576 if volume <= 0.0 {
577 Decibels::SILENCE
578 } else {
579 Decibels::from(20.0 * volume.log10())
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[test]
588 fn amplitude_to_db_maps_unity_and_silence() {
589 assert_eq!(amplitude_to_db(1.0), Decibels::IDENTITY);
590 assert_eq!(amplitude_to_db(0.0), Decibels::SILENCE);
591 assert!((amplitude_to_db(0.5).0 + 6.02).abs() < 0.01);
592 }
593
594 #[test]
595 fn play_options_builders() {
596 let o = PlayOptions::looped().volume(0.25).deferred();
597 assert!(o.looped && o.deferred);
598 assert_eq!(o.volume, 0.25);
599 assert_eq!(PlayOptions::default().volume, 1.0);
600 }
601
602 #[test]
603 fn suspend_before_start_is_remembered() {
604 let mut audio = Audio {
605 manager: None,
606 suspended: false,
607 #[cfg(target_arch = "wasm32")]
608 detach: DetachHandle::default(),
609 sounds: HashMap::new(),
610 next_id: 1,
611 master_volume: Decibels::IDENTITY,
612 queued: Vec::new(),
613 queued_streams: Vec::new(),
614 #[cfg(feature = "synth")]
615 queued_synths: Vec::new(),
616 };
617 assert!(!audio.is_suspended());
618 audio.suspend();
619 audio.suspend();
620 assert!(audio.is_suspended());
621 audio.resume();
622 assert!(!audio.is_suspended());
623 }
624}