use std::io::{Read, Seek};
use crate::{midi, Result, Source, VolumeScale};
pub struct Audio {
inner: parking_lot::Mutex<Inner>,
}
struct Inner {
output_stream_handle: rodio::OutputStreamHandle,
sinks: std::collections::HashMap<Source, rodio::Sink>,
}
impl Default for Audio {
fn default() -> Self {
#[cfg(target_arch = "wasm32")]
if web_sys::window().is_none() {
panic!("in web builds, `Audio` can only be created on the main thread");
}
let (output_stream, output_stream_handle) = rodio::OutputStream::try_default().unwrap();
std::mem::forget(output_stream); Self {
inner: parking_lot::Mutex::new(Inner {
output_stream_handle,
sinks: std::collections::HashMap::default(),
}),
}
}
}
fn apply_scale(volume: u8, scale: VolumeScale) -> f32 {
let volume = volume.min(100);
match scale {
VolumeScale::Linear => volume as f32 / 100.,
VolumeScale::Db35 => {
if volume == 0 {
0.
} else {
10f32.powf(-(0.35 / 20.) * (100 - volume) as f32)
}
}
}
}
impl Audio {
#[cfg(not(target_arch = "wasm32"))]
pub fn new() -> Self {
Default::default()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn play<T>(
&self,
path: impl AsRef<camino::Utf8Path>,
filesystem: &T,
volume: u8,
pitch: u8,
source: Option<Source>,
scale: VolumeScale,
) -> Result<()>
where
T: luminol_filesystem::FileSystem,
T::File: 'static,
{
let path = path.as_ref();
let file = filesystem.open_file(path, luminol_filesystem::OpenFlags::Read)?;
self.play_from_file(file, volume, pitch, source, scale)
}
pub fn play_from_slice(
&self,
slice: impl AsRef<[u8]> + Send + Sync + 'static,
volume: u8,
pitch: u8,
source: Option<Source>,
scale: VolumeScale,
) -> Result<()> {
self.play_from_file(std::io::Cursor::new(slice), volume, pitch, source, scale)
}
fn play_from_file(
&self,
mut file: impl Read + Seek + Send + Sync + 'static,
volume: u8,
pitch: u8,
source: Option<Source>,
scale: VolumeScale,
) -> Result<()> {
let mut magic_header_buf = [0u8; 4];
file.read_exact(&mut magic_header_buf)?;
file.seek(std::io::SeekFrom::Current(-4))?;
let is_midi = &magic_header_buf == b"MThd";
let mut inner = self.inner.lock();
let sink = rodio::Sink::try_new(&inner.output_stream_handle)?;
match source {
None | Some(Source::SE | Source::ME) => {
if is_midi {
sink.append(midi::MidiSource::new(file, false)?);
} else {
sink.append(rodio::Decoder::new(file)?);
}
}
_ => {
if is_midi {
sink.append(midi::MidiSource::new(file, true)?);
} else {
sink.append(rodio::Decoder::new_looped(file)?);
}
}
}
sink.set_speed(pitch as f32 / 100.);
sink.set_volume(apply_scale(volume, scale));
sink.play();
if let Some(source) = source {
if let Some(s) = inner.sinks.insert(source, sink) {
s.stop();
#[cfg(not(target_arch = "wasm32"))]
s.sleep_until_end(); };
} else {
sink.detach();
}
Ok(())
}
pub fn set_pitch(&self, pitch: u8, source: Source) {
let mut inner = self.inner.lock();
if let Some(s) = inner.sinks.get_mut(&source) {
s.set_speed(f32::from(pitch) / 100.);
}
}
pub fn set_volume(&self, volume: u8, source: Source, scale: VolumeScale) {
let mut inner = self.inner.lock();
if let Some(s) = inner.sinks.get_mut(&source) {
s.set_volume(apply_scale(volume, scale));
}
}
pub fn clear_sinks(&self) {
let mut inner = self.inner.lock();
for (_, sink) in inner.sinks.iter_mut() {
sink.stop();
#[cfg(not(target_arch = "wasm32"))]
sink.sleep_until_end();
}
inner.sinks.clear();
}
pub fn stop(&self, source: Source) {
let mut inner = self.inner.lock();
if let Some(s) = inner.sinks.get_mut(&source) {
s.stop();
}
}
}