1mod data;
39mod mixer;
40mod synth;
41
42use std::collections::{HashMap, HashSet};
43use std::path::{Path, PathBuf};
44use std::sync::Arc;
45use std::sync::atomic::Ordering;
46use std::sync::mpsc::{Sender, channel};
47
48use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
49use cpal::{FromSample, SampleFormat, SizedSample};
50use glam::Vec3;
51
52pub use data::SoundData;
53pub use mixer::Group;
54pub use synth::{Synth, Wave};
55
56use crate::error::{Error, Result};
57use data::SoundSlot;
58use mixer::{Change, Command, Mixer, Spatial, Voice, VoiceShared};
59
60const EXTENSIONS: [&str; 6] = ["ogg", "wav", "mp3", "flac", "opus", "m4a"];
61
62pub struct Audio {
64 tx: Sender<Command>,
65 _stream: Option<cpal::Stream>,
66 sample_rate: u32,
67 sound_dir: PathBuf,
68 cache: HashMap<String, Arc<SoundSlot>>,
69 warned: HashSet<String>,
70 next_id: u64,
71 music: Option<Sound>,
72 music_name: Option<String>,
73 listener: (Vec3, f32, f32),
74 master: f32,
75 groups: HashMap<Group, f32>,
76 default_distance: (f32, f32),
77 default_rolloff: f32,
78}
79
80impl std::fmt::Debug for Audio {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("Audio")
83 .field("sample_rate", &self.sample_rate)
84 .field("output", &self._stream.is_some())
85 .field("cached_sounds", &self.cache.len())
86 .finish()
87 }
88}
89
90impl Audio {
91 pub(crate) fn new(sound_dir: PathBuf) -> Self {
94 match Self::open(sound_dir.clone()) {
95 Ok(audio) => audio,
96 Err(e) => {
97 eprintln!("audio-code: {e}; continuing without sound");
98 Self::silent(sound_dir)
99 }
100 }
101 }
102
103 pub fn silent(sound_dir: impl Into<PathBuf>) -> Self {
105 let (tx, _) = channel();
106 Self::with_parts(tx, None, 44_100, sound_dir.into())
107 }
108
109 fn with_parts(tx: Sender<Command>, stream: Option<cpal::Stream>, rate: u32, sound_dir: PathBuf) -> Self {
110 Self {
111 tx,
112 _stream: stream,
113 sample_rate: rate,
114 sound_dir,
115 cache: HashMap::new(),
116 warned: HashSet::new(),
117 next_id: 1,
118 music: None,
119 music_name: None,
120 listener: (Vec3::ZERO, 0.0, 0.0),
121 master: 1.0,
122 groups: HashMap::new(),
123 default_distance: (1.0, 30.0),
124 default_rolloff: 0.7,
125 }
126 }
127
128 fn open(sound_dir: PathBuf) -> Result<Self> {
129 let host = cpal::default_host();
130 let device = host
131 .default_output_device()
132 .ok_or_else(|| Error::Audio("no output device found".into()))?;
133 let supported = device.default_output_config().map_err(|e| Error::Audio(e.to_string()))?;
134 let format = supported.sample_format();
135 let config: cpal::StreamConfig = supported.into();
136 let rate = config.sample_rate;
137 let (tx, rx) = channel();
138 let mixer = Mixer::new(rx, rate);
139 let stream = match format {
140 SampleFormat::F32 => build_stream::<f32>(&device, config, mixer),
141 SampleFormat::F64 => build_stream::<f64>(&device, config, mixer),
142 SampleFormat::I16 => build_stream::<i16>(&device, config, mixer),
143 SampleFormat::I32 => build_stream::<i32>(&device, config, mixer),
144 SampleFormat::U16 => build_stream::<u16>(&device, config, mixer),
145 SampleFormat::U8 => build_stream::<u8>(&device, config, mixer),
146 SampleFormat::I8 => build_stream::<i8>(&device, config, mixer),
147 other => return Err(Error::Audio(format!("unsupported sample format {other}"))),
148 }?;
149 stream.play().map_err(|e| Error::Audio(e.to_string()))?;
150 Ok(Self::with_parts(tx, Some(stream), rate, sound_dir))
151 }
152
153 pub fn sample_rate(&self) -> u32 {
155 self.sample_rate
156 }
157
158 pub fn has_output(&self) -> bool {
160 self._stream.is_some()
161 }
162
163 pub fn set_sound_dir(&mut self, dir: impl Into<PathBuf>) {
165 self.sound_dir = dir.into();
166 }
167
168 fn resolve(&self, name: &str) -> Option<PathBuf> {
171 let given = Path::new(name);
172 let mut bases = vec![given.to_path_buf(), self.sound_dir.join(given)];
173 if given.is_relative()
175 && let Some(resources) = crate::bundle::resources()
176 {
177 bases.extend([resources.join(&bases[0]), resources.join(&bases[1])]);
178 }
179 for base in &bases {
180 if base.is_file() {
181 return Some(base.clone());
182 }
183 if base.extension().is_none() {
184 for ext in EXTENSIONS {
185 let candidate = base.with_extension(ext);
186 if candidate.is_file() {
187 return Some(candidate);
188 }
189 }
190 }
191 }
192 None
193 }
194
195 fn slot(&mut self, name: &str) -> Result<Arc<SoundSlot>> {
196 if let Some(slot) = self.cache.get(name) {
197 return Ok(slot.clone());
198 }
199 let path = self
200 .resolve(name)
201 .ok_or_else(|| Error::Io(std::io::Error::new(std::io::ErrorKind::NotFound, format!("sound \"{name}\" not found"))))?;
202 let slot = Arc::new(SoundSlot::ready(SoundData::load(path)?));
203 self.cache.insert(name.to_string(), slot.clone());
204 Ok(slot)
205 }
206
207 fn slot_or_warn(&mut self, name: &str) -> Option<Arc<SoundSlot>> {
208 match self.slot(name) {
209 Ok(slot) => Some(slot),
210 Err(e) => {
211 if self.warned.insert(name.to_string()) {
212 eprintln!("audio-code: {e}");
213 }
214 None
215 }
216 }
217 }
218
219 pub fn preload(&mut self, name: &str) -> Result<()> {
221 self.slot(name).map(|_| ())
222 }
223
224 pub fn preload_dir(&mut self, dir: impl AsRef<Path>) -> Result<usize> {
228 let mut root = self.sound_dir.clone();
229 if !root.join(dir.as_ref()).is_dir()
230 && root.is_relative()
231 && let Some(resources) = crate::bundle::resources()
232 {
233 root = resources.join(root);
234 }
235 let dir = root.join(dir.as_ref());
236 let mut count = 0;
237 for entry in std::fs::read_dir(&dir)? {
238 let path = entry?.path();
239 let is_sound = path
240 .extension()
241 .and_then(|e| e.to_str())
242 .is_some_and(|e| EXTENSIONS.contains(&e.to_ascii_lowercase().as_str()));
243 if !is_sound {
244 continue;
245 }
246 let rel = path.strip_prefix(&root).unwrap_or(&path).with_extension("");
247 let name = rel.to_string_lossy().replace('\\', "/");
248 if let std::collections::hash_map::Entry::Vacant(entry) = self.cache.entry(name) {
249 entry.insert(Arc::new(SoundSlot::ready(SoundData::load(&path)?)));
250 count += 1;
251 }
252 }
253 Ok(count)
254 }
255
256 pub fn register(&mut self, name: impl Into<String>, data: impl Into<SoundData>) {
267 self.cache.insert(name.into(), Arc::new(SoundSlot::ready(data.into())));
268 }
269
270 pub fn is_loaded(&self, name: &str) -> bool {
272 self.cache.contains_key(name)
273 }
274
275 pub fn unload(&mut self, name: &str) {
277 self.cache.remove(name);
278 }
279
280 pub fn duration(&mut self, name: &str) -> Option<f32> {
282 self.slot_or_warn(name).and_then(|s| s.get().map(SoundData::duration))
283 }
284
285 pub fn sound(&mut self, name: &str) -> SoundBuilder<'_> {
290 SoundBuilder::new(self, name)
291 }
292
293 pub fn play(&mut self, name: &str) -> Sound {
295 self.sound(name).play()
296 }
297
298 pub fn play_at(&mut self, name: &str, position: Vec3) -> Sound {
300 self.sound(name).at(position).play()
301 }
302
303 pub fn play_looped(&mut self, name: &str) -> Sound {
305 self.sound(name).looped().play()
306 }
307
308 pub fn play_looped_at(&mut self, name: &str, position: Vec3) -> Sound {
310 self.sound(name).looped().at(position).play()
311 }
312
313 pub fn play_random(&mut self, names: &[&str]) -> Sound {
315 match crate::math::random::pick(names) {
316 Some(name) => self.play(name),
317 None => Sound::none(),
318 }
319 }
320
321 pub fn play_random_at(&mut self, names: &[&str], position: Vec3) -> Sound {
323 match crate::math::random::pick(names) {
324 Some(name) => self.play_at(name, position),
325 None => Sound::none(),
326 }
327 }
328
329 pub fn play_tone(&mut self, frequency: f32, seconds: f32) -> Sound {
332 let name = format!("tone:{frequency}:{seconds}");
333 if !self.cache.contains_key(&name) {
334 self.register(name.clone(), SoundData::tone(frequency, seconds, 0.4));
335 }
336 self.play(&name)
337 }
338
339 fn start(&mut self, b: Settings, slot: Arc<SoundSlot>) -> Sound {
340 let id = self.next_id;
341 self.next_id += 1;
342 let shared = Arc::new(VoiceShared::default());
343 let mut voice = Voice::new(id, slot, shared.clone(), b.group);
344 voice.looping = b.looping;
345 voice.paused = b.paused;
346 voice.pitch = b.pitch.clamp(0.01, 16.0);
347 voice.pan = b.pan.clamp(-1.0, 1.0);
348 voice.volume = b.volume.max(0.0);
349 voice.target_volume = voice.volume;
350 voice.spatial = b.position.map(|position| Spatial {
351 position,
352 min_distance: b.distance.0.max(0.01),
353 max_distance: b.distance.1.max(b.distance.0 + 0.01),
354 rolloff: b.rolloff,
355 });
356 if b.fade_in > 0.0 {
357 voice.fade_in(b.fade_in, self.sample_rate as f32);
358 }
359 let _ = self.tx.send(Command::Play(Box::new(voice)));
360 let sound = Sound { id, shared: Some(shared), tx: self.tx.clone() };
361 if b.start_at > 0.0 {
362 sound.seek(b.start_at);
363 }
364 sound
365 }
366
367 pub fn play_music(&mut self, name: &str) -> Sound {
373 self.play_music_with_fade(name, 1.0)
374 }
375
376 pub fn play_music_with_fade(&mut self, name: &str, fade: f32) -> Sound {
378 if let (Some(current), Some(music)) = (&self.music_name, &self.music)
379 && current == name
380 && music.is_playing()
381 {
382 return music.clone();
383 }
384 self.stop_music(fade);
385
386 let slot = match self.cache.get(name) {
387 Some(slot) => slot.clone(),
388 None => {
389 let Some(path) = self.resolve(name) else {
390 if self.warned.insert(name.to_string()) {
391 eprintln!("audio-code: music \"{name}\" not found");
392 }
393 return Sound::none();
394 };
395 let slot = Arc::new(SoundSlot::default());
396 let loader = slot.clone();
397 std::thread::spawn(move || match SoundData::load(&path) {
398 Ok(data) => loader.fill(data),
399 Err(e) => {
400 eprintln!("audio-code: {e}");
401 loader.fail();
402 }
403 });
404 self.cache.insert(name.to_string(), slot.clone());
405 slot
406 }
407 };
408 let settings = Settings { looping: true, group: Group::Music, fade_in: fade, ..Settings::new(self) };
409 let sound = self.start(settings, slot);
410 self.music = Some(sound.clone());
411 self.music_name = Some(name.to_string());
412 sound
413 }
414
415 pub fn stop_music(&mut self, fade: f32) {
417 if let Some(music) = self.music.take() {
418 music.fade_out(fade);
419 }
420 self.music_name = None;
421 }
422
423 pub fn music(&self) -> Option<&Sound> {
425 self.music.as_ref()
426 }
427
428 pub fn music_name(&self) -> Option<&str> {
430 self.music_name.as_deref()
431 }
432
433 pub fn set_listener(&mut self, position: Vec3, yaw: f32) {
440 self.set_listener_3d(position, yaw, 0.0);
441 }
442
443 pub fn set_listener_3d(&mut self, position: Vec3, yaw: f32, pitch: f32) {
447 let pitch = pitch.clamp(-90.0, 90.0);
448 self.listener = (position, yaw, pitch);
449 let _ = self.tx.send(Command::Listener { position, yaw, pitch });
450 }
451
452 pub fn listener_position(&self) -> Vec3 {
454 self.listener.0
455 }
456
457 pub fn listener_yaw(&self) -> f32 {
459 self.listener.1
460 }
461
462 pub fn listener_pitch(&self) -> f32 {
464 self.listener.2
465 }
466
467 pub fn set_default_distance(&mut self, min: f32, max: f32) {
470 self.default_distance = (min, max);
471 }
472
473 pub fn set_default_rolloff(&mut self, rolloff: f32) {
478 self.default_rolloff = rolloff.max(0.0);
479 }
480
481 pub fn set_master_volume(&mut self, volume: f32) {
484 self.master = volume.max(0.0);
485 let _ = self.tx.send(Command::MasterVolume(self.master));
486 }
487
488 pub fn master_volume(&self) -> f32 {
490 self.master
491 }
492
493 pub fn set_group_volume(&mut self, group: Group, volume: f32) {
497 self.groups.insert(group, volume.max(0.0));
498 let _ = self.tx.send(Command::GroupVolume(group, volume));
499 }
500
501 pub fn group_volume(&self, group: Group) -> f32 {
503 self.groups.get(&group).copied().unwrap_or(1.0)
504 }
505
506 pub fn set_music_volume(&mut self, volume: f32) {
508 self.set_group_volume(Group::Music, volume);
509 }
510
511 pub fn set_sound_volume(&mut self, volume: f32) {
513 self.set_group_volume(Group::Sound, volume);
514 }
515
516 pub fn stop_all(&mut self) {
518 self.music = None;
519 self.music_name = None;
520 let _ = self.tx.send(Command::StopAll { group: None, fade: 0.0 });
521 }
522
523 pub fn stop_group(&mut self, group: Group, fade: f32) {
525 if group == Group::Music {
526 self.music = None;
527 self.music_name = None;
528 }
529 let _ = self.tx.send(Command::StopAll { group: Some(group), fade });
530 }
531}
532
533fn build_stream<T>(device: &cpal::Device, config: cpal::StreamConfig, mut mixer: Mixer) -> Result<cpal::Stream>
534where
535 T: SizedSample + FromSample<f32>,
536{
537 let channels = config.channels as usize;
538 let mut scratch: Vec<f32> = Vec::new();
539 device
540 .build_output_stream(
541 config,
542 move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
543 let frames = data.len() / channels.max(1);
544 scratch.resize(frames * 2, 0.0);
545 mixer.render(&mut scratch);
546 for (frame, stereo) in data.chunks_mut(channels).zip(scratch.chunks(2)) {
547 match frame.len() {
548 1 => frame[0] = T::from_sample((stereo[0] + stereo[1]) * 0.5),
549 _ => {
550 frame[0] = T::from_sample(stereo[0]);
551 frame[1] = T::from_sample(stereo[1]);
552 for extra in &mut frame[2..] {
553 *extra = T::from_sample(0.0f32);
554 }
555 }
556 }
557 }
558 },
559 |err| eprintln!("audio-code: audio stream error: {err}"),
560 None,
561 )
562 .map_err(|e| Error::Audio(e.to_string()))
563}
564
565#[must_use = "call .play() to actually play the sound"]
567pub struct SoundBuilder<'a> {
568 audio: &'a mut Audio,
569 name: String,
570 settings: Settings,
571}
572
573#[derive(Debug, Clone)]
574struct Settings {
575 volume: f32,
576 pitch: f32,
577 pan: f32,
578 looping: bool,
579 paused: bool,
580 position: Option<Vec3>,
581 distance: (f32, f32),
582 rolloff: f32,
583 group: Group,
584 fade_in: f32,
585 start_at: f32,
586}
587
588impl Settings {
589 fn new(audio: &Audio) -> Self {
590 Self {
591 volume: 1.0,
592 pitch: 1.0,
593 pan: 0.0,
594 looping: false,
595 paused: false,
596 position: None,
597 distance: audio.default_distance,
598 rolloff: audio.default_rolloff,
599 group: Group::Sound,
600 fade_in: 0.0,
601 start_at: 0.0,
602 }
603 }
604}
605
606impl<'a> SoundBuilder<'a> {
607 fn new(audio: &'a mut Audio, name: &str) -> Self {
608 let settings = Settings::new(audio);
609 Self { audio, name: name.to_string(), settings }
610 }
611
612 pub fn volume(mut self, volume: f32) -> Self {
615 self.settings.volume = volume;
616 self
617 }
618
619 pub fn volume_db(mut self, db: f32) -> Self {
621 self.settings.volume = crate::math::db_to_linear(db);
622 self
623 }
624
625 pub fn pitch(mut self, pitch: f32) -> Self {
628 self.settings.pitch = pitch;
629 self
630 }
631
632 pub fn random_pitch(mut self, min: f32, max: f32) -> Self {
635 self.settings.pitch = crate::math::random::range(min, max);
636 self
637 }
638
639 pub fn pan(mut self, pan: f32) -> Self {
642 self.settings.pan = pan;
643 self
644 }
645
646 pub fn looped(mut self) -> Self {
648 self.settings.looping = true;
649 self
650 }
651
652 pub fn at(mut self, position: Vec3) -> Self {
654 self.settings.position = Some(position);
655 self
656 }
657
658 pub fn distance(mut self, min: f32, max: f32) -> Self {
661 self.settings.distance = (min, max);
662 self
663 }
664
665 pub fn rolloff(mut self, rolloff: f32) -> Self {
667 self.settings.rolloff = rolloff.max(0.0);
668 self
669 }
670
671 pub fn group(mut self, group: Group) -> Self {
673 self.settings.group = group;
674 self
675 }
676
677 pub fn fade_in(mut self, seconds: f32) -> Self {
679 self.settings.fade_in = seconds;
680 self
681 }
682
683 pub fn start_at(mut self, seconds: f32) -> Self {
685 self.settings.start_at = seconds;
686 self
687 }
688
689 pub fn paused(mut self) -> Self {
691 self.settings.paused = true;
692 self
693 }
694
695 pub fn play(self) -> Sound {
697 let SoundBuilder { audio, name, settings } = self;
698 match audio.slot_or_warn(&name) {
699 Some(slot) => audio.start(settings, slot),
700 None => Sound::none(),
701 }
702 }
703}
704
705#[derive(Clone)]
709pub struct Sound {
710 id: u64,
711 shared: Option<Arc<VoiceShared>>,
712 tx: Sender<Command>,
713}
714
715impl std::fmt::Debug for Sound {
716 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
717 f.debug_struct("Sound").field("id", &self.id).field("playing", &self.is_playing()).finish()
718 }
719}
720
721impl Default for Sound {
722 fn default() -> Self {
723 Self::none()
724 }
725}
726
727impl Sound {
728 pub fn none() -> Self {
730 let (tx, _) = channel();
731 Self { id: 0, shared: None, tx }
732 }
733
734 fn send(&self, change: Change) {
735 if self.shared.is_some() {
736 let _ = self.tx.send(Command::Change(self.id, change));
737 }
738 }
739
740 pub fn is_playing(&self) -> bool {
742 self.shared.as_ref().is_some_and(|s| !s.finished.load(Ordering::Acquire))
743 }
744
745 pub fn is_finished(&self) -> bool {
747 !self.is_playing()
748 }
749
750 pub fn position(&self) -> f32 {
752 self.shared.as_ref().map_or(0.0, |s| f64::from_bits(s.position.load(Ordering::Relaxed)) as f32)
753 }
754
755 pub fn stop(&self) {
757 self.send(Change::Stop { fade: 0.0 });
758 }
759
760 pub fn fade_out(&self, seconds: f32) {
762 self.send(Change::Stop { fade: seconds });
763 }
764
765 pub fn set_volume(&self, volume: f32) {
767 self.send(Change::Volume { target: volume, fade: 0.0 });
768 }
769
770 pub fn fade_to(&self, volume: f32, seconds: f32) {
773 self.send(Change::Volume { target: volume, fade: seconds });
774 }
775
776 pub fn set_pitch(&self, pitch: f32) {
779 self.send(Change::Pitch(pitch));
780 }
781
782 pub fn set_pan(&self, pan: f32) {
784 self.send(Change::Pan(pan));
785 }
786
787 pub fn set_position(&self, position: Vec3) {
789 self.send(Change::Position(position));
790 }
791
792 pub fn set_distance(&self, min: f32, max: f32) {
794 self.send(Change::Distance { min, max });
795 }
796
797 pub fn pause(&self) {
799 self.send(Change::Paused(true));
800 }
801
802 pub fn resume(&self) {
804 self.send(Change::Paused(false));
805 }
806
807 pub fn set_looping(&self, looping: bool) {
809 self.send(Change::Looping(looping));
810 }
811
812 pub fn seek(&self, seconds: f32) {
814 self.send(Change::Seek(seconds));
815 }
816}