1use std::io::{Read, Seek};
26
27use crate::{midi, Result, Source, VolumeScale};
28
29pub struct Audio {
31 inner: parking_lot::Mutex<Inner>,
32}
33
34struct Inner {
35 output_stream_handle: rodio::OutputStreamHandle,
36 sinks: std::collections::HashMap<Source, rodio::Sink>,
37}
38
39impl Default for Audio {
40 fn default() -> Self {
41 #[cfg(target_arch = "wasm32")]
42 if web_sys::window().is_none() {
43 panic!("in web builds, `Audio` can only be created on the main thread");
44 }
45
46 let (output_stream, output_stream_handle) = rodio::OutputStream::try_default().unwrap();
47 std::mem::forget(output_stream); Self {
49 inner: parking_lot::Mutex::new(Inner {
50 output_stream_handle,
51 sinks: std::collections::HashMap::default(),
52 }),
53 }
54 }
55}
56
57fn apply_scale(volume: u8, scale: VolumeScale) -> f32 {
58 let volume = volume.min(100);
59 match scale {
60 VolumeScale::Linear => volume as f32 / 100.,
61 VolumeScale::Db35 => {
62 if volume == 0 {
63 0.
64 } else {
65 10f32.powf(-(0.35 / 20.) * (100 - volume) as f32)
67 }
68 }
69 }
70}
71
72impl Audio {
73 #[cfg(not(target_arch = "wasm32"))]
74 pub fn new() -> Self {
75 Default::default()
76 }
77
78 #[cfg(not(target_arch = "wasm32"))]
79 pub fn play<T>(
81 &self,
82 path: impl AsRef<camino::Utf8Path>,
83 filesystem: &T,
84 volume: u8,
85 pitch: u8,
86 source: Option<Source>,
87 scale: VolumeScale,
88 ) -> Result<()>
89 where
90 T: luminol_filesystem::FileSystem,
91 T::File: 'static,
92 {
93 let path = path.as_ref();
94 let file = filesystem.open_file(path, luminol_filesystem::OpenFlags::Read)?;
95
96 self.play_from_file(file, volume, pitch, source, scale)
97 }
98
99 pub fn play_from_slice(
101 &self,
102 slice: impl AsRef<[u8]> + Send + Sync + 'static,
103 volume: u8,
104 pitch: u8,
105 source: Option<Source>,
106 scale: VolumeScale,
107 ) -> Result<()> {
108 self.play_from_file(std::io::Cursor::new(slice), volume, pitch, source, scale)
109 }
110
111 fn play_from_file(
112 &self,
113 mut file: impl Read + Seek + Send + Sync + 'static,
114 volume: u8,
115 pitch: u8,
116 source: Option<Source>,
117 scale: VolumeScale,
118 ) -> Result<()> {
119 let mut magic_header_buf = [0u8; 4];
120 file.read_exact(&mut magic_header_buf)?;
121 file.seek(std::io::SeekFrom::Current(-4))?;
122 let is_midi = &magic_header_buf == b"MThd";
123
124 let mut inner = self.inner.lock();
125 let sink = rodio::Sink::try_new(&inner.output_stream_handle)?;
127
128 match source {
130 None | Some(Source::SE | Source::ME) => {
131 if is_midi {
133 sink.append(midi::MidiSource::new(file, false)?);
134 } else {
135 sink.append(rodio::Decoder::new(file)?);
136 }
137 }
138 _ => {
139 if is_midi {
141 sink.append(midi::MidiSource::new(file, true)?);
142 } else {
143 sink.append(rodio::Decoder::new_looped(file)?);
144 }
145 }
146 }
147
148 sink.set_speed(pitch as f32 / 100.);
150 sink.set_volume(apply_scale(volume, scale));
151 sink.play();
153
154 if let Some(source) = source {
155 if let Some(s) = inner.sinks.insert(source, sink) {
157 s.stop();
158 #[cfg(not(target_arch = "wasm32"))]
159 s.sleep_until_end(); };
161 } else {
162 sink.detach();
163 }
164
165 Ok(())
166 }
167
168 pub fn set_pitch(&self, pitch: u8, source: Source) {
170 let mut inner = self.inner.lock();
171 if let Some(s) = inner.sinks.get_mut(&source) {
172 s.set_speed(f32::from(pitch) / 100.);
173 }
174 }
175
176 pub fn set_volume(&self, volume: u8, source: Source, scale: VolumeScale) {
178 let mut inner = self.inner.lock();
179 if let Some(s) = inner.sinks.get_mut(&source) {
180 s.set_volume(apply_scale(volume, scale));
181 }
182 }
183
184 pub fn clear_sinks(&self) {
185 let mut inner = self.inner.lock();
186 for (_, sink) in inner.sinks.iter_mut() {
187 sink.stop();
188 #[cfg(not(target_arch = "wasm32"))]
189 sink.sleep_until_end();
191 }
192 inner.sinks.clear();
193 }
194
195 pub fn stop(&self, source: Source) {
197 let mut inner = self.inner.lock();
198 if let Some(s) = inner.sinks.get_mut(&source) {
199 s.stop();
200 }
201 }
202}