1use std::collections::HashMap;
2use std::io::Cursor;
3use std::sync::{mpsc, Arc};
4
5use rodio::Source;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum AudioBus {
10 Sfx = 0,
11 Music = 1,
12 Ambient = 2,
13 Voice = 3,
14}
15
16impl AudioBus {
17 pub fn from_u32(value: u32) -> Option<Self> {
18 match value {
19 0 => Some(Self::Sfx),
20 1 => Some(Self::Music),
21 2 => Some(Self::Ambient),
22 3 => Some(Self::Voice),
23 _ => None,
24 }
25 }
26}
27
28pub enum AudioCommand {
30 LoadSound { id: u32, data: Vec<u8> },
31 PlaySound { id: u32, volume: f32, looping: bool },
32 StopSound { id: u32 },
33 StopAll,
34 SetMasterVolume { volume: f32 },
35
36 PlaySoundEx {
38 sound_id: u32,
39 instance_id: u64,
40 volume: f32,
41 looping: bool,
42 bus: AudioBus,
43 pan: f32,
44 pitch: f32,
45 low_pass_freq: u32,
46 reverb_mix: f32,
47 reverb_delay_ms: u32,
48 },
49 PlaySoundSpatial {
50 sound_id: u32,
51 instance_id: u64,
52 volume: f32,
53 looping: bool,
54 bus: AudioBus,
55 pitch: f32,
56 source_x: f32,
57 source_y: f32,
58 listener_x: f32,
59 listener_y: f32,
60 },
61 StopInstance { instance_id: u64 },
62 SetInstanceVolume { instance_id: u64, volume: f32 },
63 SetInstancePitch { instance_id: u64, pitch: f32 },
64 UpdateSpatialPositions {
65 updates: Vec<(u64, f32, f32)>, listener_x: f32,
67 listener_y: f32,
68 },
69 SetBusVolume { bus: AudioBus, volume: f32 },
70
71 Shutdown,
72}
73
74pub type AudioSender = mpsc::Sender<AudioCommand>;
75pub type AudioReceiver = mpsc::Receiver<AudioCommand>;
76
77pub fn audio_channel() -> (AudioSender, AudioReceiver) {
79 mpsc::channel()
80}
81
82struct InstanceMetadata {
84 bus: AudioBus,
85 base_volume: f32,
86 is_spatial: bool,
87}
88
89const SPATIAL_SCALE: f32 = 0.01;
94
95pub fn start_audio_thread(rx: AudioReceiver) -> std::thread::JoinHandle<()> {
97 std::thread::spawn(move || {
98 let stream_handle = match rodio::OutputStream::try_default() {
100 Ok((stream, handle)) => {
101 std::mem::forget(stream);
103 handle
104 }
105 Err(e) => {
106 eprintln!("[audio] Failed to initialize audio output: {e}");
107 while let Ok(cmd) = rx.recv() {
109 if matches!(cmd, AudioCommand::Shutdown) {
110 break;
111 }
112 }
113 return;
114 }
115 };
116
117 let mut sounds: HashMap<u32, Arc<Vec<u8>>> = HashMap::new();
119
120 let mut sinks: HashMap<u64, rodio::Sink> = HashMap::new();
122 let mut spatial_sinks: HashMap<u64, rodio::SpatialSink> = HashMap::new();
123 let mut instance_metadata: HashMap<u64, InstanceMetadata> = HashMap::new();
124
125 let mut master_volume: f32 = 1.0;
127 let mut bus_volumes: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; let mut legacy_sinks: HashMap<u32, rodio::Sink> = HashMap::new();
131
132 let mut cleanup_counter = 0;
134
135 loop {
136 let cmd = match rx.recv() {
137 Ok(cmd) => cmd,
138 Err(_) => break, };
140
141 match cmd {
142 AudioCommand::LoadSound { id, data } => {
143 sounds.insert(id, Arc::new(data));
144 }
145
146 AudioCommand::PlaySound { id, volume, looping } => {
147 if let Some(data) = sounds.get(&id) {
149 match rodio::Sink::try_new(&stream_handle) {
150 Ok(sink) => {
151 sink.set_volume(volume * master_volume);
152 let cursor = Cursor::new((**data).clone());
153 match rodio::Decoder::new(cursor) {
154 Ok(source) => {
155 if looping {
156 sink.append(rodio::source::Source::repeat_infinite(source));
157 } else {
158 sink.append(source);
159 }
160 sink.play();
161 legacy_sinks.insert(id, sink);
162 }
163 Err(e) => {
164 eprintln!("[audio] Failed to decode sound {id}: {e}");
165 }
166 }
167 }
168 Err(e) => {
169 eprintln!("[audio] Failed to create sink for sound {id}: {e}");
170 }
171 }
172 }
173 }
174
175 AudioCommand::StopSound { id } => {
176 if let Some(sink) = legacy_sinks.remove(&id) {
177 sink.stop();
178 }
179 }
180
181 AudioCommand::StopAll => {
182 for (_, sink) in legacy_sinks.drain() {
183 sink.stop();
184 }
185 for (_, sink) in sinks.drain() {
186 sink.stop();
187 }
188 for (_, sink) in spatial_sinks.drain() {
189 sink.stop();
190 }
191 instance_metadata.clear();
192 }
193
194 AudioCommand::SetMasterVolume { volume } => {
195 master_volume = volume;
196 for sink in legacy_sinks.values() {
198 sink.set_volume(volume);
199 }
200 update_all_volumes(&sinks, &spatial_sinks, &instance_metadata, &bus_volumes, master_volume);
202 }
203
204 AudioCommand::PlaySoundEx {
206 sound_id,
207 instance_id,
208 volume,
209 looping,
210 bus,
211 pan,
212 pitch,
213 low_pass_freq,
214 reverb_mix: _,
215 reverb_delay_ms: _,
216 } => {
217 if let Some(data) = sounds.get(&sound_id) {
218 match rodio::Sink::try_new(&stream_handle) {
219 Ok(sink) => {
220 let cursor = Cursor::new((**data).clone());
221 match rodio::Decoder::new(cursor) {
222 Ok(source) => {
223 let source = source.convert_samples::<f32>();
225
226 let source = if low_pass_freq > 0 {
228 rodio::source::Source::low_pass(source, low_pass_freq)
229 } else {
230 rodio::source::Source::low_pass(source, 20000) };
232
233 if looping {
239 sink.append(rodio::source::Source::repeat_infinite(source));
240 } else {
241 sink.append(source);
242 }
243
244 let (_left, _right) = pan_to_volumes(pan);
249
250 sink.set_volume(volume * bus_volumes[bus as usize] * master_volume);
251
252 sink.set_speed(pitch);
254
255 sink.play();
256
257 instance_metadata.insert(instance_id, InstanceMetadata {
259 bus,
260 base_volume: volume,
261 is_spatial: false,
262 });
263
264 sinks.insert(instance_id, sink);
265 }
266 Err(e) => {
267 eprintln!("[audio] Failed to decode sound {sound_id} for instance {instance_id}: {e}");
268 }
269 }
270 }
271 Err(e) => {
272 eprintln!("[audio] Failed to create sink for sound {sound_id}: {e}");
273 }
274 }
275 }
276 }
277
278 AudioCommand::PlaySoundSpatial {
279 sound_id,
280 instance_id,
281 volume,
282 looping,
283 bus,
284 pitch,
285 source_x,
286 source_y,
287 listener_x,
288 listener_y,
289 } => {
290 if let Some(data) = sounds.get(&sound_id) {
291 let sx = source_x * SPATIAL_SCALE;
293 let sy = source_y * SPATIAL_SCALE;
294 let lx = listener_x * SPATIAL_SCALE;
295 let ly = listener_y * SPATIAL_SCALE;
296
297 match rodio::SpatialSink::try_new(
299 &stream_handle,
300 [sx, sy, 0.0],
301 [lx - 0.1, ly, 0.0], [lx + 0.1, ly, 0.0], ) {
304 Ok(sink) => {
305 let cursor = Cursor::new((**data).clone());
306 match rodio::Decoder::new(cursor) {
307 Ok(source) => {
308 if looping {
309 sink.append(rodio::source::Source::repeat_infinite(source));
310 } else {
311 sink.append(source);
312 }
313
314 sink.set_volume(volume * bus_volumes[bus as usize] * master_volume);
315 sink.set_speed(pitch);
316 sink.play();
317
318 instance_metadata.insert(instance_id, InstanceMetadata {
319 bus,
320 base_volume: volume,
321 is_spatial: true,
322 });
323
324 spatial_sinks.insert(instance_id, sink);
325 }
326 Err(e) => {
327 eprintln!("[audio] Failed to decode sound {sound_id} for spatial instance {instance_id}: {e}");
328 }
329 }
330 }
331 Err(e) => {
332 eprintln!("[audio] Failed to create spatial sink for sound {sound_id}: {e}");
333 }
334 }
335 }
336 }
337
338 AudioCommand::StopInstance { instance_id } => {
339 if let Some(sink) = sinks.remove(&instance_id) {
340 sink.stop();
341 instance_metadata.remove(&instance_id);
342 } else if let Some(sink) = spatial_sinks.remove(&instance_id) {
343 sink.stop();
344 instance_metadata.remove(&instance_id);
345 }
346 }
347
348 AudioCommand::SetInstanceVolume { instance_id, volume } => {
349 if let Some(metadata) = instance_metadata.get_mut(&instance_id) {
350 metadata.base_volume = volume;
351 let final_volume = volume * bus_volumes[metadata.bus as usize] * master_volume;
352
353 if metadata.is_spatial {
354 if let Some(sink) = spatial_sinks.get(&instance_id) {
355 sink.set_volume(final_volume);
356 }
357 } else {
358 if let Some(sink) = sinks.get(&instance_id) {
359 sink.set_volume(final_volume);
360 }
361 }
362 }
363 }
364
365 AudioCommand::SetInstancePitch { instance_id, pitch } => {
366 if let Some(metadata) = instance_metadata.get(&instance_id) {
367 if metadata.is_spatial {
368 if let Some(sink) = spatial_sinks.get(&instance_id) {
369 sink.set_speed(pitch);
370 }
371 } else {
372 if let Some(sink) = sinks.get(&instance_id) {
373 sink.set_speed(pitch);
374 }
375 }
376 }
377 }
378
379 AudioCommand::UpdateSpatialPositions { updates, listener_x, listener_y } => {
380 let lx = listener_x * SPATIAL_SCALE;
381 let ly = listener_y * SPATIAL_SCALE;
382 for (instance_id, source_x, source_y) in updates {
383 if let Some(sink) = spatial_sinks.get(&instance_id) {
384 sink.set_emitter_position([source_x * SPATIAL_SCALE, source_y * SPATIAL_SCALE, 0.0]);
385 sink.set_left_ear_position([lx - 0.1, ly, 0.0]);
386 sink.set_right_ear_position([lx + 0.1, ly, 0.0]);
387 }
388 }
389 }
390
391 AudioCommand::SetBusVolume { bus, volume } => {
392 bus_volumes[bus as usize] = volume;
393 update_all_volumes(&sinks, &spatial_sinks, &instance_metadata, &bus_volumes, master_volume);
394 }
395
396 AudioCommand::Shutdown => break,
397 }
398
399 cleanup_counter += 1;
401 if cleanup_counter >= 100 {
402 cleanup_counter = 0;
403 sinks.retain(|id, sink| {
404 let keep = !sink.empty();
405 if !keep {
406 instance_metadata.remove(id);
407 }
408 keep
409 });
410 spatial_sinks.retain(|id, sink| {
411 let keep = !sink.empty();
412 if !keep {
413 instance_metadata.remove(id);
414 }
415 keep
416 });
417 legacy_sinks.retain(|_, sink| !sink.empty());
418 }
419 }
420 })
421}
422
423fn pan_to_volumes(pan: f32) -> (f32, f32) {
426 let pan_clamped = pan.clamp(-1.0, 1.0);
427 let left = ((1.0 - pan_clamped) / 2.0).sqrt();
429 let right = ((1.0 + pan_clamped) / 2.0).sqrt();
430 (left, right)
431}
432
433fn update_all_volumes(
435 sinks: &HashMap<u64, rodio::Sink>,
436 spatial_sinks: &HashMap<u64, rodio::SpatialSink>,
437 metadata: &HashMap<u64, InstanceMetadata>,
438 bus_volumes: &[f32; 4],
439 master_volume: f32,
440) {
441 for (id, sink) in sinks {
442 if let Some(meta) = metadata.get(id) {
443 let final_volume = meta.base_volume * bus_volumes[meta.bus as usize] * master_volume;
444 sink.set_volume(final_volume);
445 }
446 }
447
448 for (id, sink) in spatial_sinks {
449 if let Some(meta) = metadata.get(id) {
450 let final_volume = meta.base_volume * bus_volumes[meta.bus as usize] * master_volume;
451 sink.set_volume(final_volume);
452 }
453 }
454}