use anyhow::{Context as _, Result};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use std::collections::HashMap;
use std::fmt;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use symphonia::core::audio::{AudioBufferRef, Signal};
use symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions};
use symphonia::core::errors::Error;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
pub struct AudioSystem(pub(crate) Arc<AudioSystemInner>);
impl Clone for AudioSystem {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
type AudioRegistrationQueue = Arc<Mutex<Vec<(u32, Vec<u8>)>>>;
pub(crate) struct AudioSystemInner {
#[allow(dead_code)]
stream: cpal::Stream,
handler: Arc<Mutex<MixerHandler>>,
next_sound_id: AtomicU32,
registration_queue: AudioRegistrationQueue,
}
unsafe impl Send for AudioSystemInner {}
unsafe impl Sync for AudioSystemInner {}
impl fmt::Debug for AudioSystem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AudioSystem").finish()
}
}
impl fmt::Debug for AudioSystemInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AudioSystemInner").finish()
}
}
pub(crate) struct MixerHandler {
sample_rate: u32,
channels: u16,
next_play_id: u64,
sound_registry: HashMap<u32, SoundData>,
sounds: Vec<PlayingSound>,
}
impl fmt::Debug for MixerHandler {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MixerHandler").finish()
}
}
impl MixerHandler {
fn process(&mut self, output: &mut [f32]) {
let channels = self.channels.max(1) as usize;
let frames = output.len() / channels;
for frame in 0..frames {
let mut mix = 0.0f32;
for sound in &mut self.sounds {
if sound.finished || sound.paused {
continue;
}
mix += sound.next_sample(self.sample_rate);
}
let mix = mix.clamp(-1.0, 1.0);
let base = frame * channels;
for ch in 0..channels {
output[base + ch] = mix;
}
}
self.sounds.retain(|sound| !sound.finished);
}
fn unregister_sound(&mut self, sound_id: u32) {
self.sound_registry.remove(&sound_id);
}
}
impl AudioSystem {
pub fn new() -> Result<Self> {
Ok(Self(Arc::new(AudioSystemInner::new()?)))
}
pub(crate) fn play_sine(&self, freq: f32, volume: f32) -> Option<u64> {
self.0.play_sine(freq, volume)
}
pub(crate) fn play_registered_sound_with_options(
&self,
sound_id: u32,
options: PlayOptions,
) -> Option<u64> {
self.0.play_registered_sound_with_options(sound_id, options)
}
pub(crate) fn register_sound(&self, bytes: Vec<u8>) -> u32 {
let sound_id = self.0.next_sound_id.fetch_add(1, Ordering::SeqCst);
if let Ok(mut queue) = self.0.registration_queue.lock() {
queue.push((sound_id, bytes));
}
sound_id
}
pub(crate) fn pause_play_id(&self, play_id: u64) {
self.0.pause_play_id(play_id);
}
pub(crate) fn resume_play_id(&self, play_id: u64) {
self.0.resume_play_id(play_id);
}
pub(crate) fn stop_play_id(&self, play_id: u64) {
self.0.stop_play_id(play_id);
}
pub(crate) fn stop_all_sounds(&self) {
self.0.stop_all_sounds();
}
pub(crate) fn fade_in_play_id(&self, play_id: u64, duration: Duration) {
self.0.fade_in_play_id(play_id, duration);
}
pub(crate) fn fade_out_play_id(&self, play_id: u64, duration: Duration) {
self.0.fade_out_play_id(play_id, duration);
}
pub(crate) fn set_volume_play_id(&self, play_id: u64, volume: f32) {
self.0.set_volume_play_id(play_id, volume);
}
pub(crate) fn is_playing_play_id(&self, play_id: u64) -> bool {
self.0.is_playing_play_id(play_id)
}
pub(crate) fn unregister_sound(&self, sound_id: u32) {
self.0.unregister_sound(sound_id);
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
pub(crate) fn try_resume(&self) {
self.0.try_resume();
}
}
impl AudioSystemInner {
fn new() -> Result<Self> {
let host = cpal::default_host();
let device = host
.default_output_device()
.context("no output device available")?;
let config = device.default_output_config()?;
let sample_rate = config.sample_rate();
let channels = config.channels();
let handler = Arc::new(Mutex::new(MixerHandler {
sample_rate,
channels,
next_play_id: 1,
sound_registry: HashMap::new(),
sounds: Vec::new(),
}));
let registration_queue = Arc::new(Mutex::new(Vec::new()));
let registration_queue_clone = Arc::clone(®istration_queue);
let handler_clone = Arc::clone(&handler);
let stream = match config.sample_format() {
cpal::SampleFormat::F32 => device.build_output_stream(
&config.into(),
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
if let Ok(mut queue) = registration_queue_clone.try_lock()
&& !queue.is_empty()
&& let Ok(mut h) = handler_clone.try_lock()
{
promote_pending_registrations_locked(&mut h, &mut queue);
}
if let Ok(mut h) = handler_clone.try_lock() {
h.process(data);
}
},
|err| eprintln!("audio stream error: {}", err),
None,
)?,
_ => return Err(anyhow::anyhow!("Unsupported sample format")),
};
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
if let Err(e) = stream.play() {
web_sys::console::warn_1(
&format!("[spot][audio] play deferred (autoplay policy): {e:?}").into(),
);
}
}
#[cfg(not(target_arch = "wasm32"))]
stream.play()?;
Ok(Self {
stream,
handler,
next_sound_id: AtomicU32::new(1),
registration_queue,
})
}
fn play_sine(&self, freq: f32, volume: f32) -> Option<u64> {
let sample_rate = {
let Ok(h) = self.handler.lock() else {
return None;
};
h.sample_rate
};
let duration = Duration::from_millis(600);
let total_samples = (duration.as_secs_f64() * sample_rate as f64) as usize;
let mut samples = Vec::with_capacity(total_samples);
for i in 0..total_samples {
let t = i as f32 / sample_rate as f32;
samples.push((t * freq * std::f32::consts::TAU).sin());
}
let sound = SoundData {
samples: Arc::new(samples),
sample_rate,
channels: 1,
};
self.play_sound_with_options(
&sound,
PlayOptions {
volume,
fade_in: Duration::from_millis(20),
fade_out: Some(Duration::from_millis(80)),
start_paused: false,
},
)
}
fn play_registered_sound_with_options(
&self,
sound_id: u32,
options: PlayOptions,
) -> Option<u64> {
self.promote_pending_registrations();
let Ok(mut handler) = self.handler.lock() else {
return None;
};
let sound = handler.sound_registry.get(&sound_id)?.clone();
Some(Self::add_playing_sound_locked(
&mut handler,
&sound,
options,
))
}
fn play_sound_with_options(&self, sound: &SoundData, options: PlayOptions) -> Option<u64> {
if sound.samples.is_empty() || sound.sample_rate == 0 {
return None;
}
if let Ok(mut handler) = self.handler.lock() {
Some(Self::add_playing_sound_locked(&mut handler, sound, options))
} else {
None
}
}
fn unregister_sound(&self, sound_id: u32) {
if let Ok(mut queue) = self.registration_queue.lock() {
remove_pending_registration_locked(&mut queue, sound_id);
}
if let Ok(mut handler) = self.handler.lock() {
handler.unregister_sound(sound_id);
}
}
fn promote_pending_registrations(&self) {
let Ok(mut queue) = self.registration_queue.lock() else {
return;
};
if queue.is_empty() {
return;
}
let Ok(mut handler) = self.handler.lock() else {
return;
};
promote_pending_registrations_locked(&mut handler, &mut queue);
}
fn add_playing_sound_locked(
handler: &mut MixerHandler,
sound: &SoundData,
options: PlayOptions,
) -> u64 {
let play_id = handler.next_play_id;
handler.next_play_id = handler.next_play_id.wrapping_add(1).max(1);
let source_rate = sound.sample_rate;
let step = source_rate as f64 / handler.sample_rate as f64;
let total_frames = if step > 0.0 {
(sound.samples.len() as f64 / step).ceil() as u64
} else {
sound.samples.len() as u64
};
let mut playing = PlayingSound::new(play_id, Arc::clone(&sound.samples), source_rate);
playing.volume = options.volume.max(0.0);
playing.paused = options.start_paused;
playing.total_frames = total_frames;
if options.fade_in > Duration::ZERO {
playing.fade_gain = 0.0;
playing.fade = Some(FadeState::new(
0.0,
1.0,
duration_to_frames(options.fade_in, handler.sample_rate),
false,
));
}
if let Some(fade_out) = options.fade_out {
let frames = duration_to_frames(fade_out, handler.sample_rate);
if frames > 0 {
playing.fade_out_on_end = Some(FadeOnEnd {
frames,
started: false,
});
}
}
handler.sounds.push(playing);
play_id
}
fn pause_play_id(&self, play_id: u64) {
self.update_playing(play_id, |sound| sound.paused = true);
}
fn resume_play_id(&self, play_id: u64) {
self.update_playing(play_id, |sound| sound.paused = false);
}
fn stop_play_id(&self, play_id: u64) {
self.update_playing(play_id, |sound| sound.finished = true);
}
fn stop_all_sounds(&self) {
let Ok(mut handler) = self.handler.lock() else {
return;
};
for sound in &mut handler.sounds {
sound.finished = true;
}
}
fn fade_in_play_id(&self, play_id: u64, duration: Duration) {
let frames = duration_to_frames(duration, self.sample_rate());
if frames == 0 {
return;
}
self.update_playing(play_id, |sound| {
sound.fade_gain = 0.0;
sound.fade = Some(FadeState::new(0.0, 1.0, frames, false));
});
}
fn fade_out_play_id(&self, play_id: u64, duration: Duration) {
let frames = duration_to_frames(duration, self.sample_rate());
if frames == 0 {
self.stop_play_id(play_id);
return;
}
self.update_playing(play_id, |sound| {
let start = sound.fade_gain;
sound.fade = Some(FadeState::new(start, 0.0, frames, true));
});
}
fn set_volume_play_id(&self, play_id: u64, volume: f32) {
self.update_playing(play_id, |sound| sound.volume = volume.max(0.0));
}
fn is_playing_play_id(&self, play_id: u64) -> bool {
let Ok(handler) = self.handler.lock() else {
return false;
};
handler
.sounds
.iter()
.find(|sound| sound.id == play_id)
.map(|sound| !sound.finished && !sound.paused)
.unwrap_or(false)
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
fn try_resume(&self) {
let _ = self.stream.play();
}
fn sample_rate(&self) -> u32 {
let Ok(handler) = self.handler.lock() else {
return 0;
};
handler.sample_rate
}
fn update_playing(&self, play_id: u64, f: impl FnOnce(&mut PlayingSound)) {
let Ok(mut handler) = self.handler.lock() else {
return;
};
if let Some(sound) = handler.sounds.iter_mut().find(|sound| sound.id == play_id) {
f(sound);
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct PlayOptions {
pub volume: f32,
pub fade_in: Duration,
pub fade_out: Option<Duration>,
pub start_paused: bool,
}
impl Default for PlayOptions {
fn default() -> Self {
Self {
volume: 1.0,
fade_in: Duration::ZERO,
fade_out: None,
start_paused: false,
}
}
}
#[derive(Clone)]
pub(crate) struct SoundData {
pub samples: Arc<Vec<f32>>,
pub sample_rate: u32,
#[allow(dead_code)]
pub channels: u16,
}
struct PlayingSound {
id: u64,
samples: Arc<Vec<f32>>,
source_rate: u32,
position: f64,
volume: f32,
paused: bool,
fade_gain: f32,
fade: Option<FadeState>,
fade_out_on_end: Option<FadeOnEnd>,
total_frames: u64,
frames_played: u64,
finished: bool,
}
impl PlayingSound {
fn new(id: u64, samples: Arc<Vec<f32>>, source_rate: u32) -> Self {
Self {
id,
samples,
source_rate,
position: 0.0,
volume: 1.0,
paused: false,
fade_gain: 1.0,
fade: None,
fade_out_on_end: None,
total_frames: 0,
frames_played: 0,
finished: false,
}
}
fn next_sample(&mut self, output_rate: u32) -> f32 {
if self.finished || output_rate == 0 || self.source_rate == 0 {
return 0.0;
}
if let Some(fade_out) = &mut self.fade_out_on_end
&& !fade_out.started
{
let start_at = self.total_frames.saturating_sub(fade_out.frames);
if self.frames_played >= start_at {
let start_gain = self.fade_gain;
self.fade = Some(FadeState::new(start_gain, 0.0, fade_out.frames, true));
fade_out.started = true;
}
}
let sample = self.sample_at_position();
if let Some(fade) = &mut self.fade {
let gain = fade.next_gain();
self.fade_gain = gain;
if fade.finished() {
let stop = fade.stop_on_end && fade.end <= 0.0001;
self.fade = None;
if stop {
self.finished = true;
}
}
}
let step = self.source_rate as f64 / output_rate as f64;
self.position += step;
self.frames_played = self.frames_played.saturating_add(1);
if self.position >= self.samples.len() as f64 {
self.finished = true;
}
sample * self.volume * self.fade_gain
}
fn sample_at_position(&self) -> f32 {
let len = self.samples.len();
if len == 0 {
return 0.0;
}
let idx = self.position.floor() as usize;
if idx >= len {
return 0.0;
}
let next_idx = idx + 1;
let frac = (self.position - idx as f64) as f32;
let s0 = self.samples[idx];
let s1 = if next_idx < len {
self.samples[next_idx]
} else {
0.0
};
s0 + (s1 - s0) * frac
}
}
struct FadeState {
current: f32,
step: f32,
remaining: u64,
end: f32,
stop_on_end: bool,
}
impl FadeState {
fn new(start: f32, end: f32, frames: u64, stop_on_end: bool) -> Self {
if frames == 0 {
return Self {
current: end,
step: 0.0,
remaining: 0,
end,
stop_on_end,
};
}
let step = (end - start) / frames as f32;
Self {
current: start,
step,
remaining: frames,
end,
stop_on_end,
}
}
fn next_gain(&mut self) -> f32 {
if self.remaining == 0 {
return self.end;
}
let gain = self.current;
self.current += self.step;
self.remaining = self.remaining.saturating_sub(1);
if self.remaining == 0 {
self.current = self.end;
}
gain
}
fn finished(&self) -> bool {
self.remaining == 0
}
}
struct FadeOnEnd {
frames: u64,
started: bool,
}
fn duration_to_frames(duration: Duration, sample_rate: u32) -> u64 {
if duration == Duration::ZERO || sample_rate == 0 {
return 0;
}
(duration.as_secs_f64() * sample_rate as f64).round() as u64
}
pub(crate) fn decode_sound_from_bytes(bytes: Vec<u8>) -> Result<SoundData> {
let src = std::io::Cursor::new(bytes);
let mss = MediaSourceStream::new(Box::new(src), Default::default());
let hint = Hint::new();
let probed = symphonia::default::get_probe().format(
&hint,
mss,
&FormatOptions::default(),
&MetadataOptions::default(),
)?;
let mut format = probed.format;
let track = format
.tracks()
.iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.context("no supported audio track found")?;
let dec_opts = DecoderOptions::default();
let mut decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts)?;
let track_id = track.id;
let mut sample_rate = 0;
let mut channels = 0;
let mut samples = Vec::new();
loop {
let packet = match format.next_packet() {
Ok(packet) => packet,
Err(Error::IoError(ref err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
break;
}
Err(err) => return Err(err.into()),
};
if packet.track_id() != track_id {
continue;
}
let decoded = decoder.decode(&packet)?;
if sample_rate == 0 {
let spec = decoded.spec();
sample_rate = spec.rate;
channels = spec.channels.count() as u16;
}
match decoded {
AudioBufferRef::F32(buf) => {
let channel_count = buf.spec().channels.count();
for i in 0..buf.frames() {
let mut sum = 0.0f32;
for ch in 0..channel_count {
sum += buf.chan(ch)[i];
}
samples.push(sum / channel_count as f32);
}
}
AudioBufferRef::U8(buf) => {
let channel_count = buf.spec().channels.count();
for i in 0..buf.frames() {
let mut sum = 0.0f32;
for ch in 0..channel_count {
sum += (buf.chan(ch)[i] as f32 - 128.0) / 128.0;
}
samples.push(sum / channel_count as f32);
}
}
AudioBufferRef::S16(buf) => {
let channel_count = buf.spec().channels.count();
for i in 0..buf.frames() {
let mut sum = 0.0f32;
for ch in 0..channel_count {
sum += buf.chan(ch)[i] as f32 / 32768.0;
}
samples.push(sum / channel_count as f32);
}
}
AudioBufferRef::S32(buf) => {
let channel_count = buf.spec().channels.count();
for i in 0..buf.frames() {
let mut sum = 0.0f32;
for ch in 0..channel_count {
sum += buf.chan(ch)[i] as f32 / 2147483648.0;
}
samples.push(sum / channel_count as f32);
}
}
_ => {}
}
}
Ok(SoundData {
samples: Arc::new(samples),
sample_rate,
channels,
})
}
fn promote_pending_registrations_locked(
handler: &mut MixerHandler,
queue: &mut Vec<(u32, Vec<u8>)>,
) {
for (id, sound_data) in decode_pending_registrations(std::mem::take(queue)) {
handler.sound_registry.insert(id, sound_data);
}
}
fn decode_pending_registrations(pending: Vec<(u32, Vec<u8>)>) -> Vec<(u32, SoundData)> {
pending
.into_iter()
.filter_map(|(id, bytes)| decode_sound_from_bytes(bytes).ok().map(|sound| (id, sound)))
.collect()
}
fn remove_pending_registration_locked(queue: &mut Vec<(u32, Vec<u8>)>, sound_id: u32) {
queue.retain(|(id, _)| *id != sound_id);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pending_registration_is_available_after_promotion() {
let mut handler = MixerHandler {
sample_rate: 48_000,
channels: 2,
next_play_id: 1,
sound_registry: HashMap::new(),
sounds: Vec::new(),
};
let mut queue = vec![(7, test_wav_bytes())];
promote_pending_registrations_locked(&mut handler, &mut queue);
assert!(queue.is_empty());
assert!(handler.sound_registry.contains_key(&7));
}
#[test]
fn unregister_removes_pending_registration() {
let mut queue = vec![(7, test_wav_bytes()), (8, test_wav_bytes())];
remove_pending_registration_locked(&mut queue, 7);
assert_eq!(queue.len(), 1);
assert_eq!(queue[0].0, 8);
}
fn test_wav_bytes() -> Vec<u8> {
let sample_rate = 8_000u32;
let bits_per_sample = 16u16;
let channels = 1u16;
let samples: [i16; 4] = [0, 8_000, -8_000, 0];
let data_len = (samples.len() * std::mem::size_of::<i16>()) as u32;
let byte_rate = sample_rate * channels as u32 * bits_per_sample as u32 / 8;
let block_align = channels * bits_per_sample / 8;
let riff_len = 36 + data_len;
let mut bytes = Vec::new();
bytes.extend_from_slice(b"RIFF");
bytes.extend_from_slice(&riff_len.to_le_bytes());
bytes.extend_from_slice(b"WAVE");
bytes.extend_from_slice(b"fmt ");
bytes.extend_from_slice(&16u32.to_le_bytes());
bytes.extend_from_slice(&1u16.to_le_bytes());
bytes.extend_from_slice(&channels.to_le_bytes());
bytes.extend_from_slice(&sample_rate.to_le_bytes());
bytes.extend_from_slice(&byte_rate.to_le_bytes());
bytes.extend_from_slice(&block_align.to_le_bytes());
bytes.extend_from_slice(&bits_per_sample.to_le_bytes());
bytes.extend_from_slice(b"data");
bytes.extend_from_slice(&data_len.to_le_bytes());
for sample in samples {
bytes.extend_from_slice(&sample.to_le_bytes());
}
bytes
}
}