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 StopAll,
32 SetMasterVolume { volume: f32 },
33
34 PlaySoundEx {
36 sound_id: u32,
37 instance_id: u64,
38 volume: f32,
39 looping: bool,
40 bus: AudioBus,
41 pan: f32,
42 pitch: f32,
43 low_pass_freq: u32,
44 reverb_mix: f32,
45 reverb_delay_ms: u32,
46 },
47 PlaySoundSpatial {
48 sound_id: u32,
49 instance_id: u64,
50 volume: f32,
51 looping: bool,
52 bus: AudioBus,
53 pitch: f32,
54 source_x: f32,
55 source_y: f32,
56 listener_x: f32,
57 listener_y: f32,
58 },
59 StopInstance { instance_id: u64 },
60 SetInstanceVolume { instance_id: u64, volume: f32 },
61 SetInstancePitch { instance_id: u64, pitch: f32 },
62 UpdateSpatialPositions {
63 updates: Vec<(u64, f32, f32)>, listener_x: f32,
65 listener_y: f32,
66 },
67 SetBusVolume { bus: AudioBus, volume: f32 },
68
69 Shutdown,
70}
71
72pub type AudioSender = mpsc::Sender<AudioCommand>;
73pub type AudioReceiver = mpsc::Receiver<AudioCommand>;
74
75pub fn audio_channel() -> (AudioSender, AudioReceiver) {
77 mpsc::channel()
78}
79
80struct InstanceMetadata {
82 bus: AudioBus,
83 base_volume: f32,
84 is_spatial: bool,
85}
86
87const SPATIAL_SCALE: f32 = 0.01;
92
93pub fn start_audio_thread(rx: AudioReceiver) -> std::thread::JoinHandle<()> {
95 std::thread::spawn(move || {
96 let stream_handle = match rodio::OutputStream::try_default() {
98 Ok((stream, handle)) => {
99 std::mem::forget(stream);
101 handle
102 }
103 Err(e) => {
104 eprintln!("[audio] Failed to initialize audio output: {e}");
105 while let Ok(cmd) = rx.recv() {
107 if matches!(cmd, AudioCommand::Shutdown) {
108 break;
109 }
110 }
111 return;
112 }
113 };
114
115 let mut sounds: HashMap<u32, Arc<Vec<u8>>> = HashMap::new();
117
118 let mut sinks: HashMap<u64, rodio::Sink> = HashMap::new();
120 let mut spatial_sinks: HashMap<u64, rodio::SpatialSink> = HashMap::new();
121 let mut instance_metadata: HashMap<u64, InstanceMetadata> = HashMap::new();
122
123 let mut master_volume: f32 = 1.0;
125 let mut bus_volumes: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; let mut cleanup_counter = 0;
129
130 loop {
131 let cmd = match rx.recv() {
132 Ok(cmd) => cmd,
133 Err(_) => break, };
135
136 match cmd {
137 AudioCommand::LoadSound { id, data } => {
138 sounds.insert(id, Arc::new(data));
139 }
140
141 AudioCommand::StopAll => {
142 for (_, sink) in sinks.drain() {
143 sink.stop();
144 }
145 for (_, sink) in spatial_sinks.drain() {
146 sink.stop();
147 }
148 instance_metadata.clear();
149 }
150
151 AudioCommand::SetMasterVolume { volume } => {
152 master_volume = volume;
153 update_all_volumes(&sinks, &spatial_sinks, &instance_metadata, &bus_volumes, master_volume);
154 }
155
156 AudioCommand::PlaySoundEx {
158 sound_id,
159 instance_id,
160 volume,
161 looping,
162 bus,
163 pan,
164 pitch,
165 low_pass_freq,
166 reverb_mix: _,
167 reverb_delay_ms: _,
168 } => {
169 if let Some(data) = sounds.get(&sound_id) {
170 match rodio::Sink::try_new(&stream_handle) {
171 Ok(sink) => {
172 let cursor = Cursor::new((**data).clone());
173 match rodio::Decoder::new(cursor) {
174 Ok(source) => {
175 let source = source.convert_samples::<f32>();
177
178 let source = if low_pass_freq > 0 {
180 rodio::source::Source::low_pass(source, low_pass_freq)
181 } else {
182 rodio::source::Source::low_pass(source, 20000) };
184
185 if looping {
191 sink.append(rodio::source::Source::repeat_infinite(source));
192 } else {
193 sink.append(source);
194 }
195
196 let (_left, _right) = pan_to_volumes(pan);
201
202 sink.set_volume(volume * bus_volumes[bus as usize] * master_volume);
203
204 sink.set_speed(pitch);
206
207 sink.play();
208
209 instance_metadata.insert(instance_id, InstanceMetadata {
211 bus,
212 base_volume: volume,
213 is_spatial: false,
214 });
215
216 sinks.insert(instance_id, sink);
217 }
218 Err(e) => {
219 eprintln!("[audio] Failed to decode sound {sound_id} for instance {instance_id}: {e}");
220 }
221 }
222 }
223 Err(e) => {
224 eprintln!("[audio] Failed to create sink for sound {sound_id}: {e}");
225 }
226 }
227 }
228 }
229
230 AudioCommand::PlaySoundSpatial {
231 sound_id,
232 instance_id,
233 volume,
234 looping,
235 bus,
236 pitch,
237 source_x,
238 source_y,
239 listener_x,
240 listener_y,
241 } => {
242 if let Some(data) = sounds.get(&sound_id) {
243 let sx = source_x * SPATIAL_SCALE;
245 let sy = source_y * SPATIAL_SCALE;
246 let lx = listener_x * SPATIAL_SCALE;
247 let ly = listener_y * SPATIAL_SCALE;
248
249 match rodio::SpatialSink::try_new(
251 &stream_handle,
252 [sx, sy, 0.0],
253 [lx - 0.1, ly, 0.0], [lx + 0.1, ly, 0.0], ) {
256 Ok(sink) => {
257 let cursor = Cursor::new((**data).clone());
258 match rodio::Decoder::new(cursor) {
259 Ok(source) => {
260 if looping {
261 sink.append(rodio::source::Source::repeat_infinite(source));
262 } else {
263 sink.append(source);
264 }
265
266 sink.set_volume(volume * bus_volumes[bus as usize] * master_volume);
267 sink.set_speed(pitch);
268 sink.play();
269
270 instance_metadata.insert(instance_id, InstanceMetadata {
271 bus,
272 base_volume: volume,
273 is_spatial: true,
274 });
275
276 spatial_sinks.insert(instance_id, sink);
277 }
278 Err(e) => {
279 eprintln!("[audio] Failed to decode sound {sound_id} for spatial instance {instance_id}: {e}");
280 }
281 }
282 }
283 Err(e) => {
284 eprintln!("[audio] Failed to create spatial sink for sound {sound_id}: {e}");
285 }
286 }
287 }
288 }
289
290 AudioCommand::StopInstance { instance_id } => {
291 if let Some(sink) = sinks.remove(&instance_id) {
292 sink.stop();
293 instance_metadata.remove(&instance_id);
294 } else if let Some(sink) = spatial_sinks.remove(&instance_id) {
295 sink.stop();
296 instance_metadata.remove(&instance_id);
297 }
298 }
299
300 AudioCommand::SetInstanceVolume { instance_id, volume } => {
301 if let Some(metadata) = instance_metadata.get_mut(&instance_id) {
302 metadata.base_volume = volume;
303 let final_volume = volume * bus_volumes[metadata.bus as usize] * master_volume;
304
305 if metadata.is_spatial {
306 if let Some(sink) = spatial_sinks.get(&instance_id) {
307 sink.set_volume(final_volume);
308 }
309 } else {
310 if let Some(sink) = sinks.get(&instance_id) {
311 sink.set_volume(final_volume);
312 }
313 }
314 }
315 }
316
317 AudioCommand::SetInstancePitch { instance_id, pitch } => {
318 if let Some(metadata) = instance_metadata.get(&instance_id) {
319 if metadata.is_spatial {
320 if let Some(sink) = spatial_sinks.get(&instance_id) {
321 sink.set_speed(pitch);
322 }
323 } else {
324 if let Some(sink) = sinks.get(&instance_id) {
325 sink.set_speed(pitch);
326 }
327 }
328 }
329 }
330
331 AudioCommand::UpdateSpatialPositions { updates, listener_x, listener_y } => {
332 let lx = listener_x * SPATIAL_SCALE;
333 let ly = listener_y * SPATIAL_SCALE;
334 for (instance_id, source_x, source_y) in updates {
335 if let Some(sink) = spatial_sinks.get(&instance_id) {
336 sink.set_emitter_position([source_x * SPATIAL_SCALE, source_y * SPATIAL_SCALE, 0.0]);
337 sink.set_left_ear_position([lx - 0.1, ly, 0.0]);
338 sink.set_right_ear_position([lx + 0.1, ly, 0.0]);
339 }
340 }
341 }
342
343 AudioCommand::SetBusVolume { bus, volume } => {
344 bus_volumes[bus as usize] = volume;
345 update_all_volumes(&sinks, &spatial_sinks, &instance_metadata, &bus_volumes, master_volume);
346 }
347
348 AudioCommand::Shutdown => break,
349 }
350
351 cleanup_counter += 1;
353 if cleanup_counter >= 100 {
354 cleanup_counter = 0;
355 sinks.retain(|id, sink| {
356 let keep = !sink.empty();
357 if !keep {
358 instance_metadata.remove(id);
359 }
360 keep
361 });
362 spatial_sinks.retain(|id, sink| {
363 let keep = !sink.empty();
364 if !keep {
365 instance_metadata.remove(id);
366 }
367 keep
368 });
369 }
370 }
371 })
372}
373
374fn pan_to_volumes(pan: f32) -> (f32, f32) {
377 let pan_clamped = pan.clamp(-1.0, 1.0);
378 let left = ((1.0 - pan_clamped) / 2.0).sqrt();
380 let right = ((1.0 + pan_clamped) / 2.0).sqrt();
381 (left, right)
382}
383
384fn update_all_volumes(
386 sinks: &HashMap<u64, rodio::Sink>,
387 spatial_sinks: &HashMap<u64, rodio::SpatialSink>,
388 metadata: &HashMap<u64, InstanceMetadata>,
389 bus_volumes: &[f32; 4],
390 master_volume: f32,
391) {
392 for (id, sink) in sinks {
393 if let Some(meta) = metadata.get(id) {
394 let final_volume = meta.base_volume * bus_volumes[meta.bus as usize] * master_volume;
395 sink.set_volume(final_volume);
396 }
397 }
398
399 for (id, sink) in spatial_sinks {
400 if let Some(meta) = metadata.get(id) {
401 let final_volume = meta.base_volume * bus_volumes[meta.bus as usize] * master_volume;
402 sink.set_volume(final_volume);
403 }
404 }
405}