mum_cli/audio/
sound_effects.rs1use 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
17enum 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
35struct AudioSpec {
37 channels: u32,
38 sample_rate: u32,
39}
40
41#[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
76pub 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 NotificationEvents::iter()
101 .map(|event| {
102 let file = overrides.get(&event);
103 let (data, kind) = file
105 .and_then(|file| {
106 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 let (samples, spec) = unpack_audio(data, kind);
116 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 let mut signal = signal::from_interleaved_samples_iter::<_, [f32; 2]>(iter);
127 let interp = Linear::new(Signal::next(&mut signal), Signal::next(&mut signal));
129 let samples = signal
131 .from_hz_to_hz(interp, spec.sample_rate as f64, SAMPLE_RATE as f64)
132 .until_exhausted()
133 .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
147fn 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")]
156fn 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"))]
172fn 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
178fn 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
199fn 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
213fn get_default_sfx() -> Cow<'static, [u8]> {
215 Cow::from(include_bytes!("fallback_sfx.wav").as_ref())
216}