Skip to main content

audio_code/audio/
mod.rs

1//! High level sound: one-liners for effects, 3D positioned sounds and music.
2//! Available as `ctx.audio`.
3//!
4//! ```no_run
5//! # use audio_code::prelude::*;
6//! # fn f(ctx: &mut Context) {
7//! // Fire and forget.
8//! ctx.audio.play("door_open");
9//!
10//! // A looping sound placed in the world. Keep the handle to move or stop it later.
11//! let engine = ctx.audio.play_looped_at("car_engine", vec3(10.0, 5.0, 0.0));
12//! engine.set_pitch(1.3);
13//!
14//! // Full control with the builder.
15//! ctx.audio.sound("explosion").volume(0.8).pitch(0.9).at(vec3(3.0, 0.0, 0.0)).play();
16//!
17//! // Background music with automatic cross fading.
18//! ctx.audio.play_music("music/level1");
19//! # }
20//! ```
21//!
22//! ## Finding files
23//!
24//! Sound names are resolved like this:
25//!
26//! 1. The name as given (`"sounds/step.ogg"`).
27//! 2. Inside the sound directory set with [`Engine::sound_dir`](crate::Engine::sound_dir)
28//!    (default `"sounds"`).
29//! 3. In a macOS app bundle, both of the above inside `Contents/Resources`.
30//! 4. If the name has no extension, `.ogg`, `.wav`, `.mp3`, `.flac`, `.opus` and `.m4a` are tried.
31//!
32//! So with the default settings `ctx.audio.play("step")` finds `sounds/step.ogg`.
33//!
34//! Loaded files are cached, the second play of the same name costs nothing.
35//! Missing or broken files print a warning once and return a silent handle, so a typo never
36//! crashes your game.
37
38mod 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
62/// The audio system. Reach it through `ctx.audio`.
63pub 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    /// Opens the default output device. If that fails, a silent audio system is returned
92    /// and a warning is printed, so the game still runs.
93    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    /// An audio system without an output device. Everything works, nothing is heard.
104    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    /// Sample rate of the output device in Hz.
154    pub fn sample_rate(&self) -> u32 {
155        self.sample_rate
156    }
157
158    /// Whether an output device is open (`false` means sounds are silently ignored).
159    pub fn has_output(&self) -> bool {
160        self._stream.is_some()
161    }
162
163    /// Changes the directory searched for sounds. See the [module docs](self).
164    pub fn set_sound_dir(&mut self, dir: impl Into<PathBuf>) {
165        self.sound_dir = dir.into();
166    }
167
168    // ----------------------------------------------------------------- loading
169
170    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        // A macOS app bundle ships its files in `Contents/Resources`.
174        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    /// Loads a sound into the cache now, so the first play has no delay.
220    pub fn preload(&mut self, name: &str) -> Result<()> {
221        self.slot(name).map(|_| ())
222    }
223
224    /// Loads every sound file found in `dir` (not recursive). Names are the file paths
225    /// relative to the sound directory without extension, e.g. `"step"`.
226    /// Returns how many files were loaded.
227    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    /// Registers sound data generated in code (or a [`Synth`]) under a name, so it can be
257    /// played like a file. Registering the same name again replaces the sound.
258    ///
259    /// ```no_run
260    /// # use audio_code::prelude::*;
261    /// # fn f(ctx: &mut Context) {
262    /// ctx.audio.register("beep", SoundData::tone(880.0, 0.1, 0.5));
263    /// ctx.audio.play("beep");
264    /// # }
265    /// ```
266    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    /// Whether a sound with this name is loaded or registered.
271    pub fn is_loaded(&self, name: &str) -> bool {
272        self.cache.contains_key(name)
273    }
274
275    /// Removes a sound from the cache to free memory. Playing sounds are not affected.
276    pub fn unload(&mut self, name: &str) {
277        self.cache.remove(name);
278    }
279
280    /// Length of a sound in seconds (loads it if needed).
281    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    // ----------------------------------------------------------------- playing
286
287    /// Starts building a sound with full control over its settings. Finish with
288    /// [`SoundBuilder::play`].
289    pub fn sound(&mut self, name: &str) -> SoundBuilder<'_> {
290        SoundBuilder::new(self, name)
291    }
292
293    /// Plays a sound once, centered. Returns a handle you may keep or ignore.
294    pub fn play(&mut self, name: &str) -> Sound {
295        self.sound(name).play()
296    }
297
298    /// Plays a sound once at a position in the world.
299    pub fn play_at(&mut self, name: &str, position: Vec3) -> Sound {
300        self.sound(name).at(position).play()
301    }
302
303    /// Plays a sound in an endless loop. Stop it with [`Sound::stop`].
304    pub fn play_looped(&mut self, name: &str) -> Sound {
305        self.sound(name).looped().play()
306    }
307
308    /// Plays a looping sound at a position in the world.
309    pub fn play_looped_at(&mut self, name: &str, position: Vec3) -> Sound {
310        self.sound(name).looped().at(position).play()
311    }
312
313    /// Plays one of several sounds picked at random, e.g. footstep variations.
314    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    /// Plays one of several sounds picked at random at a position.
322    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    /// Plays a generated sine beep of `frequency` hertz (e.g. 880.0) for `seconds`. Great
330    /// for prototyping before you have sound files.
331    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    // ----------------------------------------------------------------- music
368
369    /// Plays background music in a loop. The file is decoded in the background so the game
370    /// does not stutter. Any previous music fades out while the new one fades in.
371    /// Calling it again with the music that is already playing does nothing.
372    pub fn play_music(&mut self, name: &str) -> Sound {
373        self.play_music_with_fade(name, 1.0)
374    }
375
376    /// Like [`Audio::play_music`] with a custom cross fade time in seconds.
377    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    /// Fades out the current music over `fade` seconds.
416    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    /// Handle of the current music, if any.
424    pub fn music(&self) -> Option<&Sound> {
425        self.music.as_ref()
426    }
427
428    /// Name of the current music, as passed to [`Audio::play_music`].
429    pub fn music_name(&self) -> Option<&str> {
430        self.music_name.as_deref()
431    }
432
433    // ----------------------------------------------------------------- listener & volume
434
435    /// Places the listener (the player's ears) in the world. `yaw` is the compass direction
436    /// the player faces (0 = north, 90 = east). Call this whenever the player moves or turns.
437    /// The listener looks level with the horizon; use [`Audio::set_listener_3d`] to also
438    /// look up or down.
439    pub fn set_listener(&mut self, position: Vec3, yaw: f32) {
440        self.set_listener_3d(position, yaw, 0.0);
441    }
442
443    /// Like [`Audio::set_listener`], plus `pitch`: how far the player looks up (positive,
444    /// 90 = straight up) or down (negative, -90 = straight down), in degrees. For flying,
445    /// swimming or space games.
446    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    /// Current listener position.
453    pub fn listener_position(&self) -> Vec3 {
454        self.listener.0
455    }
456
457    /// Current listener facing in compass degrees.
458    pub fn listener_yaw(&self) -> f32 {
459        self.listener.1
460    }
461
462    /// Current listener pitch in degrees (0 = level, 90 = straight up).
463    pub fn listener_pitch(&self) -> f32 {
464        self.listener.2
465    }
466
467    /// Default audible range for positioned sounds in meters: full volume up to `min`,
468    /// silent from `max` on. The default is 1.0 and 30.0.
469    pub fn set_default_distance(&mut self, min: f32, max: f32) {
470        self.default_distance = (min, max);
471    }
472
473    /// Default rolloff for positioned sounds: how fast the volume drops between `min` and
474    /// `max` distance. 1.0 is physically accurate (twice the distance, half the volume),
475    /// lower values keep distant sounds louder, 0.0 keeps full volume until shortly before
476    /// `max`. Default is 0.7, which is at 32 % volume at 5 m and 18 % at 10 m.
477    pub fn set_default_rolloff(&mut self, rolloff: f32) {
478        self.default_rolloff = rolloff.max(0.0);
479    }
480
481    /// Sets the overall volume as a linear factor: 1.0 = unchanged (default), 0.5 = half,
482    /// 0.0 = silent. Higher values amplify and may distort.
483    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    /// Overall volume.
489    pub fn master_volume(&self) -> f32 {
490        self.master
491    }
492
493    /// Sets the volume of one group (sound effects, music or UI) as a linear factor,
494    /// 1.0 = unchanged (default). It is multiplied with each sound's own volume and the
495    /// master volume.
496    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    /// Volume of one group.
502    pub fn group_volume(&self, group: Group) -> f32 {
503        self.groups.get(&group).copied().unwrap_or(1.0)
504    }
505
506    /// Shortcut for `set_group_volume(Group::Music, volume)`.
507    pub fn set_music_volume(&mut self, volume: f32) {
508        self.set_group_volume(Group::Music, volume);
509    }
510
511    /// Shortcut for `set_group_volume(Group::Sound, volume)`.
512    pub fn set_sound_volume(&mut self, volume: f32) {
513        self.set_group_volume(Group::Sound, volume);
514    }
515
516    /// Stops every sound immediately (including music).
517    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    /// Fades out all sounds of a group over `fade` seconds and stops them.
524    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/// Configures a sound before playing it. Created by [`Audio::sound`].
566#[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    /// Linear volume factor: 1.0 = as recorded (default), 0.5 = half (about -6 dB),
613    /// 0.0 = silent. Values above 1.0 amplify and may distort.
614    pub fn volume(mut self, volume: f32) -> Self {
615        self.settings.volume = volume;
616        self
617    }
618
619    /// Volume in decibels: 0 = original, -6 = about half, -20 = a tenth, +6 = about double.
620    pub fn volume_db(mut self, db: f32) -> Self {
621        self.settings.volume = crate::math::db_to_linear(db);
622        self
623    }
624
625    /// Playback speed, which changes pitch and length together: 1.0 = original (default),
626    /// 2.0 = one octave up and twice as fast, 0.5 = one octave down and twice as long.
627    pub fn pitch(mut self, pitch: f32) -> Self {
628        self.settings.pitch = pitch;
629        self
630    }
631
632    /// Random pitch between `min` and `max` – makes repeated sounds less monotonous.
633    /// `0.9, 1.1` is a good range for footsteps and hits.
634    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    /// Stereo position for non-positioned sounds: -1.0 left, 0.0 center (default), 1.0
640    /// right. Ignored once the sound has a position ([`SoundBuilder::at`]).
641    pub fn pan(mut self, pan: f32) -> Self {
642        self.settings.pan = pan;
643        self
644    }
645
646    /// Loop endlessly.
647    pub fn looped(mut self) -> Self {
648        self.settings.looping = true;
649        self
650    }
651
652    /// Place the sound in the world. It is panned and attenuated relative to the listener.
653    pub fn at(mut self, position: Vec3) -> Self {
654        self.settings.position = Some(position);
655        self
656    }
657
658    /// Audible range: full volume up to `min` meters, silent from `max` meters. Default
659    /// 1.0 and 30.0 (see [`Audio::set_default_distance`]).
660    pub fn distance(mut self, min: f32, max: f32) -> Self {
661        self.settings.distance = (min, max);
662        self
663    }
664
665    /// How quickly volume drops with distance (see [`Audio::set_default_rolloff`]).
666    pub fn rolloff(mut self, rolloff: f32) -> Self {
667        self.settings.rolloff = rolloff.max(0.0);
668        self
669    }
670
671    /// Volume group (default [`Group::Sound`]).
672    pub fn group(mut self, group: Group) -> Self {
673        self.settings.group = group;
674        self
675    }
676
677    /// Fades in from silence to the volume over the given seconds.
678    pub fn fade_in(mut self, seconds: f32) -> Self {
679        self.settings.fade_in = seconds;
680        self
681    }
682
683    /// Start playback this many seconds into the sound.
684    pub fn start_at(mut self, seconds: f32) -> Self {
685        self.settings.start_at = seconds;
686        self
687    }
688
689    /// Create the sound paused; call [`Sound::resume`] to start it.
690    pub fn paused(mut self) -> Self {
691        self.settings.paused = true;
692        self
693    }
694
695    /// Plays the sound and returns its handle.
696    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/// Handle to a playing sound. Cheap to clone. Dropping it does **not** stop the sound.
706///
707/// All methods are safe to call after the sound finished; they simply do nothing.
708#[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    /// A handle that refers to no sound. Useful as a placeholder in structs.
729    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    /// Whether the sound is still playing (or paused, or loading).
741    pub fn is_playing(&self) -> bool {
742        self.shared.as_ref().is_some_and(|s| !s.finished.load(Ordering::Acquire))
743    }
744
745    /// Whether the sound has ended or was stopped.
746    pub fn is_finished(&self) -> bool {
747        !self.is_playing()
748    }
749
750    /// Current playback position in seconds.
751    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    /// Stops immediately.
756    pub fn stop(&self) {
757        self.send(Change::Stop { fade: 0.0 });
758    }
759
760    /// Fades to silence over `seconds`, then stops. Good for ending loops without a click.
761    pub fn fade_out(&self, seconds: f32) {
762        self.send(Change::Stop { fade: seconds });
763    }
764
765    /// Sets the linear volume immediately (1.0 = original, 0.0 = silent).
766    pub fn set_volume(&self, volume: f32) {
767        self.send(Change::Volume { target: volume, fade: 0.0 });
768    }
769
770    /// Fades the linear volume to `volume` over `seconds`. The sound keeps playing, also
771    /// at 0.0.
772    pub fn fade_to(&self, volume: f32, seconds: f32) {
773        self.send(Change::Volume { target: volume, fade: seconds });
774    }
775
776    /// Sets playback speed and pitch (1.0 = original, 2.0 = one octave up and twice as
777    /// fast). Can be changed every frame, e.g. for an engine sound.
778    pub fn set_pitch(&self, pitch: f32) {
779        self.send(Change::Pitch(pitch));
780    }
781
782    /// Sets stereo panning for non-positioned sounds (-1.0 left to 1.0 right).
783    pub fn set_pan(&self, pan: f32) {
784        self.send(Change::Pan(pan));
785    }
786
787    /// Moves a positioned sound (created with `at` / `play_at`).
788    pub fn set_position(&self, position: Vec3) {
789        self.send(Change::Position(position));
790    }
791
792    /// Changes the audible range of a positioned sound.
793    pub fn set_distance(&self, min: f32, max: f32) {
794        self.send(Change::Distance { min, max });
795    }
796
797    /// Pauses playback.
798    pub fn pause(&self) {
799        self.send(Change::Paused(true));
800    }
801
802    /// Continues after [`Sound::pause`].
803    pub fn resume(&self) {
804        self.send(Change::Paused(false));
805    }
806
807    /// Turns looping on or off while playing.
808    pub fn set_looping(&self, looping: bool) {
809        self.send(Change::Looping(looping));
810    }
811
812    /// Jumps to a position in seconds.
813    pub fn seek(&self, seconds: f32) {
814        self.send(Change::Seek(seconds));
815    }
816}