mum_cli/audio/
sound_effects.rs

1use dasp_interpolate::linear::Linear;
2use dasp_signal::{self as signal, Signal};
3use log::warn;
4use mumlib::config::SoundEffect;
5use std::borrow::Cow;
6use std::collections::HashMap;
7use std::fs::File;
8#[cfg(feature = "ogg")]
9use std::io::Cursor;
10use std::io::Read;
11use std::path::Path;
12use strum::IntoEnumIterator;
13use strum_macros::EnumIter;
14
15use crate::audio::SAMPLE_RATE;
16
17/// The different kinds of files we can open.
18enum AudioFileKind {
19    Ogg,
20    Wav,
21}
22
23impl TryFrom<&str> for AudioFileKind {
24    type Error = ();
25
26    fn try_from(s: &str) -> Result<Self, Self::Error> {
27        match s {
28            "ogg" => Ok(AudioFileKind::Ogg),
29            "wav" => Ok(AudioFileKind::Wav),
30            _ => Err(()),
31        }
32    }
33}
34
35/// A specification accompanying some audio data.
36struct AudioSpec {
37    channels: u32,
38    sample_rate: u32,
39}
40
41/// An event where a notification is shown and a sound effect is played.
42#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, EnumIter)]
43pub enum NotificationEvents {
44    ServerConnect,
45    ServerDisconnect,
46    UserConnected,
47    UserDisconnected,
48    UserJoinedChannel,
49    UserLeftChannel,
50    Mute,
51    Unmute,
52    Deafen,
53    Undeafen,
54}
55
56impl TryFrom<&str> for NotificationEvents {
57    type Error = ();
58
59    fn try_from(s: &str) -> Result<Self, Self::Error> {
60        match s {
61            "server_connect" => Ok(NotificationEvents::ServerConnect),
62            "server_disconnect" => Ok(NotificationEvents::ServerDisconnect),
63            "user_connected" => Ok(NotificationEvents::UserConnected),
64            "user_disconnected" => Ok(NotificationEvents::UserDisconnected),
65            "user_joined_channel" => Ok(NotificationEvents::UserJoinedChannel),
66            "user_left_channel" => Ok(NotificationEvents::UserLeftChannel),
67            "mute" => Ok(NotificationEvents::Mute),
68            "unmute" => Ok(NotificationEvents::Unmute),
69            "deafen" => Ok(NotificationEvents::Deafen),
70            "undeafen" => Ok(NotificationEvents::Undeafen),
71            _ => Err(()),
72        }
73    }
74}
75
76/// Loads files into an "event -> data"-map, with support for overriding
77/// specific events with another sound file.
78pub fn load_sound_effects(
79    overrides: &[SoundEffect],
80    num_channels: usize,
81) -> HashMap<NotificationEvents, Vec<f32>> {
82    let overrides: HashMap<_, _> = overrides
83        .iter()
84        .filter_map(|sound_effect| {
85            let (event, file) = (&sound_effect.event, &sound_effect.file);
86            if let Ok(event) = NotificationEvents::try_from(event.as_str()) {
87                Some((event, file))
88            } else {
89                warn!("Unknown notification event '{}'", event);
90                None
91            }
92        })
93        .collect();
94
95    // Construct a hashmap that maps every [NotificationEvent] to a vector of
96    // plain floating point audio data with the global sample rate as a
97    // Vec<f32>. We do this by iterating over all [NotificationEvent]-variants
98    // and opening either the file passed as an override or the fallback sound
99    // effect (if omitted). We then use dasp to convert to the correct sample rate.
100    NotificationEvents::iter()
101        .map(|event| {
102            let file = overrides.get(&event);
103            // Try to open the file if overriden, otherwise use the default sound effect.
104            let (data, kind) = file
105                .and_then(|file| {
106                    // Try to get the file kind from the extension.
107                    let kind = file
108                        .split('.')
109                        .last()
110                        .and_then(|ext| AudioFileKind::try_from(ext).ok())?;
111                    Some((get_sfx(file), kind))
112                })
113                .unwrap_or_else(|| (get_default_sfx(), AudioFileKind::Wav));
114            // Unpack the samples.
115            let (samples, spec) = unpack_audio(data, kind);
116            // If the audio is mono (single channel), pad every sample with
117            // itself, since we later assume that audio is stored interleaved as
118            // LRLRLR (or RLRLRL). Without this, mono audio would be played in
119            // double speed.
120            let iter: Box<dyn Iterator<Item = f32>> = match spec.channels {
121                1 => Box::new(samples.into_iter().flat_map(|e| [e, e])),
122                2 => Box::new(samples.into_iter()),
123                _ => unimplemented!("Only mono and stereo sound is supported. See #80."),
124            };
125            // Create a dasp signal containing stereo sound.
126            let mut signal = signal::from_interleaved_samples_iter::<_, [f32; 2]>(iter);
127            // Create a linear interpolator, in case we need to convert the sample rate.
128            let interp = Linear::new(Signal::next(&mut signal), Signal::next(&mut signal));
129            // Create our resulting samples.
130            let samples = signal
131                .from_hz_to_hz(interp, spec.sample_rate as f64, SAMPLE_RATE as f64)
132                .until_exhausted()
133                // If the source audio is stereo and is being played as mono, discard the first channel.
134                .flat_map(|e| {
135                    if num_channels == 1 {
136                        vec![e[0]]
137                    } else {
138                        e.to_vec()
139                    }
140                })
141                .collect::<Vec<f32>>();
142            (event, samples)
143        })
144        .collect()
145}
146
147/// Unpack audio data. The required audio spec is read from the file and returned as well.
148fn unpack_audio(data: Cow<'_, [u8]>, kind: AudioFileKind) -> (Vec<f32>, AudioSpec) {
149    match kind {
150        AudioFileKind::Ogg => unpack_ogg(data),
151        AudioFileKind::Wav => unpack_wav(data),
152    }
153}
154
155#[cfg(feature = "ogg")]
156/// Unpack ogg data.
157fn unpack_ogg(data: Cow<'_, [u8]>) -> (Vec<f32>, AudioSpec) {
158    let mut reader = lewton::inside_ogg::OggStreamReader::new(Cursor::new(data.as_ref())).unwrap();
159    let mut samples = Vec::new();
160    while let Ok(Some(mut frame)) = reader.read_dec_packet_itl() {
161        samples.append(&mut frame);
162    }
163    let samples = samples.iter().map(|s| cpal::Sample::to_f32(s)).collect();
164    let spec = AudioSpec {
165        channels: reader.ident_hdr.audio_channels as u32,
166        sample_rate: reader.ident_hdr.audio_sample_rate,
167    };
168    (samples, spec)
169}
170
171#[cfg(not(feature = "ogg"))]
172/// Fallback to default sound effect since ogg is disabled.
173fn unpack_ogg(_: Cow<'_, [u8]>) -> (Vec<f32>, AudioSpec) {
174    warn!("Can't open .ogg without the ogg-feature enabled.");
175    unpack_wav(get_default_sfx())
176}
177
178/// Unpack wav data.
179fn unpack_wav(data: Cow<'_, [u8]>) -> (Vec<f32>, AudioSpec) {
180    let reader = hound::WavReader::new(data.as_ref()).unwrap();
181    let spec = reader.spec();
182    let samples = match spec.sample_format {
183        hound::SampleFormat::Float => reader
184            .into_samples::<f32>()
185            .map(|e| e.unwrap())
186            .collect::<Vec<_>>(),
187        hound::SampleFormat::Int => reader
188            .into_samples::<i16>()
189            .map(|e| cpal::Sample::to_f32(&e.unwrap()))
190            .collect::<Vec<_>>(),
191    };
192    let spec = AudioSpec {
193        channels: spec.channels as u32,
194        sample_rate: spec.sample_rate,
195    };
196    (samples, spec)
197}
198
199/// Open and return the data contained in a file, or the default sound effect if
200/// the file couldn't be found.
201// moo
202fn get_sfx<P: AsRef<Path>>(file: P) -> Cow<'static, [u8]> {
203    let mut buf: Vec<u8> = Vec::new();
204    if let Ok(mut file) = File::open(file.as_ref()) {
205        file.read_to_end(&mut buf).unwrap();
206        Cow::from(buf)
207    } else {
208        warn!("File not found: '{}'", file.as_ref().display());
209        get_default_sfx()
210    }
211}
212
213/// Get the default sound effect.
214fn get_default_sfx() -> Cow<'static, [u8]> {
215    Cow::from(include_bytes!("fallback_sfx.wav").as_ref())
216}