use std::collections::HashMap;
use std::io::Cursor;
use std::sync::{mpsc, Arc};
use rodio::Source;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AudioBus {
Sfx = 0,
Music = 1,
Ambient = 2,
Voice = 3,
}
impl AudioBus {
pub fn from_u32(value: u32) -> Option<Self> {
match value {
0 => Some(Self::Sfx),
1 => Some(Self::Music),
2 => Some(Self::Ambient),
3 => Some(Self::Voice),
_ => None,
}
}
}
pub enum AudioCommand {
LoadSound { id: u32, data: Vec<u8> },
StopAll,
SetMasterVolume { volume: f32 },
PlaySoundEx {
sound_id: u32,
instance_id: u64,
volume: f32,
looping: bool,
bus: AudioBus,
pan: f32,
pitch: f32,
low_pass_freq: u32,
reverb_mix: f32,
reverb_delay_ms: u32,
},
PlaySoundSpatial {
sound_id: u32,
instance_id: u64,
volume: f32,
looping: bool,
bus: AudioBus,
pitch: f32,
source_x: f32,
source_y: f32,
listener_x: f32,
listener_y: f32,
},
StopInstance { instance_id: u64 },
SetInstanceVolume { instance_id: u64, volume: f32 },
SetInstancePitch { instance_id: u64, pitch: f32 },
UpdateSpatialPositions {
updates: Vec<(u64, f32, f32)>, listener_x: f32,
listener_y: f32,
},
SetBusVolume { bus: AudioBus, volume: f32 },
Shutdown,
}
pub type AudioSender = mpsc::Sender<AudioCommand>;
pub type AudioReceiver = mpsc::Receiver<AudioCommand>;
pub fn audio_channel() -> (AudioSender, AudioReceiver) {
mpsc::channel()
}
struct InstanceMetadata {
bus: AudioBus,
base_volume: f32,
is_spatial: bool,
}
const SPATIAL_SCALE: f32 = 0.01;
pub fn start_audio_thread(rx: AudioReceiver) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
let stream_handle = match rodio::OutputStream::try_default() {
Ok((stream, handle)) => {
std::mem::forget(stream);
handle
}
Err(e) => {
eprintln!("[audio] Failed to initialize audio output: {e}");
while let Ok(cmd) = rx.recv() {
if matches!(cmd, AudioCommand::Shutdown) {
break;
}
}
return;
}
};
let mut sounds: HashMap<u32, Arc<Vec<u8>>> = HashMap::new();
let mut sinks: HashMap<u64, rodio::Sink> = HashMap::new();
let mut spatial_sinks: HashMap<u64, rodio::SpatialSink> = HashMap::new();
let mut instance_metadata: HashMap<u64, InstanceMetadata> = HashMap::new();
let mut master_volume: f32 = 1.0;
let mut bus_volumes: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
let mut cleanup_counter = 0;
loop {
let cmd = match rx.recv() {
Ok(cmd) => cmd,
Err(_) => break, };
match cmd {
AudioCommand::LoadSound { id, data } => {
sounds.insert(id, Arc::new(data));
}
AudioCommand::StopAll => {
for (_, sink) in sinks.drain() {
sink.stop();
}
for (_, sink) in spatial_sinks.drain() {
sink.stop();
}
instance_metadata.clear();
}
AudioCommand::SetMasterVolume { volume } => {
master_volume = volume;
update_all_volumes(&sinks, &spatial_sinks, &instance_metadata, &bus_volumes, master_volume);
}
AudioCommand::PlaySoundEx {
sound_id,
instance_id,
volume,
looping,
bus,
pan,
pitch,
low_pass_freq,
reverb_mix: _,
reverb_delay_ms: _,
} => {
if let Some(data) = sounds.get(&sound_id) {
match rodio::Sink::try_new(&stream_handle) {
Ok(sink) => {
let cursor = Cursor::new((**data).clone());
match rodio::Decoder::new(cursor) {
Ok(source) => {
let source = source.convert_samples::<f32>();
let source = if low_pass_freq > 0 {
rodio::source::Source::low_pass(source, low_pass_freq)
} else {
rodio::source::Source::low_pass(source, 20000) };
if looping {
sink.append(rodio::source::Source::repeat_infinite(source));
} else {
sink.append(source);
}
let (_left, _right) = pan_to_volumes(pan);
sink.set_volume(volume * bus_volumes[bus as usize] * master_volume);
sink.set_speed(pitch);
sink.play();
instance_metadata.insert(instance_id, InstanceMetadata {
bus,
base_volume: volume,
is_spatial: false,
});
sinks.insert(instance_id, sink);
}
Err(e) => {
eprintln!("[audio] Failed to decode sound {sound_id} for instance {instance_id}: {e}");
}
}
}
Err(e) => {
eprintln!("[audio] Failed to create sink for sound {sound_id}: {e}");
}
}
}
}
AudioCommand::PlaySoundSpatial {
sound_id,
instance_id,
volume,
looping,
bus,
pitch,
source_x,
source_y,
listener_x,
listener_y,
} => {
if let Some(data) = sounds.get(&sound_id) {
let sx = source_x * SPATIAL_SCALE;
let sy = source_y * SPATIAL_SCALE;
let lx = listener_x * SPATIAL_SCALE;
let ly = listener_y * SPATIAL_SCALE;
match rodio::SpatialSink::try_new(
&stream_handle,
[sx, sy, 0.0],
[lx - 0.1, ly, 0.0], [lx + 0.1, ly, 0.0], ) {
Ok(sink) => {
let cursor = Cursor::new((**data).clone());
match rodio::Decoder::new(cursor) {
Ok(source) => {
if looping {
sink.append(rodio::source::Source::repeat_infinite(source));
} else {
sink.append(source);
}
sink.set_volume(volume * bus_volumes[bus as usize] * master_volume);
sink.set_speed(pitch);
sink.play();
instance_metadata.insert(instance_id, InstanceMetadata {
bus,
base_volume: volume,
is_spatial: true,
});
spatial_sinks.insert(instance_id, sink);
}
Err(e) => {
eprintln!("[audio] Failed to decode sound {sound_id} for spatial instance {instance_id}: {e}");
}
}
}
Err(e) => {
eprintln!("[audio] Failed to create spatial sink for sound {sound_id}: {e}");
}
}
}
}
AudioCommand::StopInstance { instance_id } => {
if let Some(sink) = sinks.remove(&instance_id) {
sink.stop();
instance_metadata.remove(&instance_id);
} else if let Some(sink) = spatial_sinks.remove(&instance_id) {
sink.stop();
instance_metadata.remove(&instance_id);
}
}
AudioCommand::SetInstanceVolume { instance_id, volume } => {
if let Some(metadata) = instance_metadata.get_mut(&instance_id) {
metadata.base_volume = volume;
let final_volume = volume * bus_volumes[metadata.bus as usize] * master_volume;
if metadata.is_spatial {
if let Some(sink) = spatial_sinks.get(&instance_id) {
sink.set_volume(final_volume);
}
} else {
if let Some(sink) = sinks.get(&instance_id) {
sink.set_volume(final_volume);
}
}
}
}
AudioCommand::SetInstancePitch { instance_id, pitch } => {
if let Some(metadata) = instance_metadata.get(&instance_id) {
if metadata.is_spatial {
if let Some(sink) = spatial_sinks.get(&instance_id) {
sink.set_speed(pitch);
}
} else {
if let Some(sink) = sinks.get(&instance_id) {
sink.set_speed(pitch);
}
}
}
}
AudioCommand::UpdateSpatialPositions { updates, listener_x, listener_y } => {
let lx = listener_x * SPATIAL_SCALE;
let ly = listener_y * SPATIAL_SCALE;
for (instance_id, source_x, source_y) in updates {
if let Some(sink) = spatial_sinks.get(&instance_id) {
sink.set_emitter_position([source_x * SPATIAL_SCALE, source_y * SPATIAL_SCALE, 0.0]);
sink.set_left_ear_position([lx - 0.1, ly, 0.0]);
sink.set_right_ear_position([lx + 0.1, ly, 0.0]);
}
}
}
AudioCommand::SetBusVolume { bus, volume } => {
bus_volumes[bus as usize] = volume;
update_all_volumes(&sinks, &spatial_sinks, &instance_metadata, &bus_volumes, master_volume);
}
AudioCommand::Shutdown => break,
}
cleanup_counter += 1;
if cleanup_counter >= 100 {
cleanup_counter = 0;
sinks.retain(|id, sink| {
let keep = !sink.empty();
if !keep {
instance_metadata.remove(id);
}
keep
});
spatial_sinks.retain(|id, sink| {
let keep = !sink.empty();
if !keep {
instance_metadata.remove(id);
}
keep
});
}
}
})
}
fn pan_to_volumes(pan: f32) -> (f32, f32) {
let pan_clamped = pan.clamp(-1.0, 1.0);
let left = ((1.0 - pan_clamped) / 2.0).sqrt();
let right = ((1.0 + pan_clamped) / 2.0).sqrt();
(left, right)
}
fn update_all_volumes(
sinks: &HashMap<u64, rodio::Sink>,
spatial_sinks: &HashMap<u64, rodio::SpatialSink>,
metadata: &HashMap<u64, InstanceMetadata>,
bus_volumes: &[f32; 4],
master_volume: f32,
) {
for (id, sink) in sinks {
if let Some(meta) = metadata.get(id) {
let final_volume = meta.base_volume * bus_volumes[meta.bus as usize] * master_volume;
sink.set_volume(final_volume);
}
}
for (id, sink) in spatial_sinks {
if let Some(meta) = metadata.get(id) {
let final_volume = meta.base_volume * bus_volumes[meta.bus as usize] * master_volume;
sink.set_volume(final_volume);
}
}
}