1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//! Audio components.

use std::io::Cursor;

use crate::prelude::*;

pub use kira::{self, sound::static_sound::StaticSoundData};
use kira::{
    manager::{
        backend::{cpal::CpalBackend, mock::MockBackend, Backend},
        AudioManager as KiraAudioManager,
    },
    sound::SoundData,
};

/// The game plugin for the audio system.
pub fn game_plugin(game: &mut Game) {
    AudioSource::register_schema();
    game.insert_shared_resource(AudioManager::default());
    game.init_shared_resource::<AssetServer>();
}

/// The audio manager resource which can be used to play sounds.
#[derive(HasSchema, Deref, DerefMut)]
#[schema(no_clone)]
pub struct AudioManager(KiraAudioManager<CpalWithFallbackBackend>);
impl Default for AudioManager {
    fn default() -> Self {
        Self(KiraAudioManager::<CpalWithFallbackBackend>::new(default()).unwrap())
    }
}

/// Kira audio backend that will fall back to a dummy backend if setting up the Cpal backend
/// fails with an error.
#[allow(clippy::large_enum_variant)]
pub enum CpalWithFallbackBackend {
    /// This is a working Cpal backend.
    Cpal(CpalBackend),
    /// This is a dummy backend since Cpal didn't work.
    Dummy(MockBackend),
}

impl Backend for CpalWithFallbackBackend {
    type Settings = <CpalBackend as Backend>::Settings;
    type Error = <CpalBackend as Backend>::Error;

    fn setup(settings: Self::Settings) -> Result<(Self, u32), Self::Error> {
        match CpalBackend::setup(settings) {
            Ok((back, bit)) => Ok((Self::Cpal(back), bit)),
            Err(e) => {
                tracing::error!("Error starting audio backend, using dummy backend instead: {e}");
                Ok(MockBackend::setup(default())
                    .map(|(back, bit)| (Self::Dummy(back), bit))
                    .unwrap())
            }
        }
    }

    fn start(&mut self, renderer: kira::manager::backend::Renderer) -> Result<(), Self::Error> {
        match self {
            CpalWithFallbackBackend::Cpal(cpal) => cpal.start(renderer),
            CpalWithFallbackBackend::Dummy(dummy) => {
                dummy.start(renderer).unwrap();
                Ok(())
            }
        }
    }
}

/// The audio source asset type, contains no data, but [`Handle<AudioSource>`] is still useful
/// because it uniquely represents a sound/music that may be played outside of bones.
#[derive(Clone, HasSchema, Debug, Deref, DerefMut)]
#[schema(no_default)]
#[type_data(asset_loader(["ogg", "mp3", "flac", "wav"], AudioLoader))]
pub struct AudioSource(pub StaticSoundData);

impl SoundData for &AudioSource {
    type Error = <StaticSoundData as SoundData>::Error;
    type Handle = <StaticSoundData as SoundData>::Handle;

    fn into_sound(self) -> Result<(Box<dyn kira::sound::Sound>, Self::Handle), Self::Error> {
        self.0.clone().into_sound()
    }
}

/// The audio file asset loader.
pub struct AudioLoader;
impl AssetLoader for AudioLoader {
    fn load(
        &self,
        _ctx: AssetLoadCtx,
        bytes: &[u8],
    ) -> futures::future::Boxed<anyhow::Result<SchemaBox>> {
        let bytes = bytes.to_vec();
        Box::pin(async move {
            let data = StaticSoundData::from_cursor(Cursor::new(bytes), default())?;
            Ok(SchemaBox::new(AudioSource(data)))
        })
    }
}