luminol_audio/
native.rs

1// Copyright (C) 2024 Melody Madeline Lyons
2//
3// This file is part of Luminol.
4//
5// Luminol is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Luminol is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Luminol.  If not, see <http://www.gnu.org/licenses/>.
17//
18//     Additional permission under GNU GPL version 3 section 7
19//
20// If you modify this Program, or any covered work, by linking or combining
21// it with Steamworks API by Valve Corporation, containing parts covered by
22// terms of the Steamworks API by Valve Corporation, the licensors of this
23// Program grant you additional permission to convey the resulting work.
24
25use std::io::{Read, Seek};
26
27use crate::{midi, Result, Source, VolumeScale};
28
29/// A struct for playing Audio.
30pub 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); // Prevent the stream from being dropped
48        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                // -0.35 dB per percent below 100% volume
66                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    /// Play a sound on a source.
80    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    /// Play a sound on a source from audio file data.
100    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        // Create a sink
126        let sink = rodio::Sink::try_new(&inner.output_stream_handle)?;
127
128        // Select decoder type based on sound source
129        match source {
130            None | Some(Source::SE | Source::ME) => {
131                // Non looping
132                if is_midi {
133                    sink.append(midi::MidiSource::new(file, false)?);
134                } else {
135                    sink.append(rodio::Decoder::new(file)?);
136                }
137            }
138            _ => {
139                // Looping
140                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        // Set pitch and volume
149        sink.set_speed(pitch as f32 / 100.);
150        sink.set_volume(apply_scale(volume, scale));
151        // Play sound.
152        sink.play();
153
154        if let Some(source) = source {
155            // Add sink to hash, stop the current one if it's there.
156            if let Some(s) = inner.sinks.insert(source, sink) {
157                s.stop();
158                #[cfg(not(target_arch = "wasm32"))]
159                s.sleep_until_end(); // wait for the sink to stop, there is a ~5ms delay where it will not
160            };
161        } else {
162            sink.detach();
163        }
164
165        Ok(())
166    }
167
168    /// Set the pitch of a source.
169    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    /// Set the volume of a source.
177    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            // Sleeping ensures that the inner file is dropped. There is a delay of ~5ms where it is not dropped and this could lead to a panic
190            sink.sleep_until_end();
191        }
192        inner.sinks.clear();
193    }
194
195    /// Stop a source.
196    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}