Skip to main content

goud_engine/ecs/components/
audiosource.rs

1//! ## Play a Sound Effect
2//!
3//! ```
4//! use goud_engine::ecs::{World, Component};
5//! use goud_engine::ecs::components::{AudioSource, AudioChannel};
6//! use goud_engine::assets::AssetHandle;
7//! use goud_engine::assets::loaders::audio::AudioAsset;
8//!
9//! let mut world = World::new();
10//!
11//! // Assume we have a loaded audio asset
12//! let audio_handle: AssetHandle<AudioAsset> = AssetHandle::default();
13//!
14//! // Spawn entity with audio source
15//! let entity = world.spawn_empty();
16//! world.insert(entity, AudioSource::new(audio_handle)
17//!     .with_volume(0.8)
18//!     .with_looping(false)
19//!     .with_auto_play(true)
20//!     .with_channel(AudioChannel::SFX));
21//! ```
22//!
23//! ## Background Music with Looping
24//!
25//! ```
26//! use goud_engine::ecs::components::{AudioSource, AudioChannel};
27//! use goud_engine::assets::AssetHandle;
28//! use goud_engine::assets::loaders::audio::AudioAsset;
29//!
30//! let audio_handle: AssetHandle<AudioAsset> = AssetHandle::default();
31//!
32//! let music = AudioSource::new(audio_handle)
33//!     .with_volume(0.5)
34//!     .with_looping(true)
35//!     .with_auto_play(true)
36//!     .with_channel(AudioChannel::Music);
37//! ```
38//!
39//! ## Spatial Audio with Attenuation
40//!
41//! ```
42//! use goud_engine::ecs::components::{AudioSource, AudioChannel, AttenuationModel};
43//! use goud_engine::assets::AssetHandle;
44//! use goud_engine::assets::loaders::audio::AudioAsset;
45//!
46//! let audio_handle: AssetHandle<AudioAsset> = AssetHandle::default();
47//!
48//! let spatial_audio = AudioSource::new(audio_handle)
49//!     .with_volume(1.0)
50//!     .with_spatial(true)
51//!     .with_max_distance(100.0)
52//!     .with_attenuation(AttenuationModel::InverseDistance)
53//!     .with_channel(AudioChannel::Ambience);
54//! ```
55
56use crate::assets::loaders::audio::AudioAsset;
57use crate::assets::AssetHandle;
58use crate::ecs::Component;
59
60/// Audio channel enumeration for audio mixing and grouping.
61///
62/// Channels allow you to group audio sources together for volume control,
63/// filtering, and organization. Each audio source belongs to one channel.
64///
65/// # Built-in Channels
66///
67/// - **Music**: Background music tracks (typically looped)
68/// - **SFX**: Sound effects (footsteps, impacts, UI clicks)
69/// - **Voice**: Voice-overs, dialogue, speech
70/// - **Ambience**: Ambient environment sounds (wind, rain, room tone)
71/// - **UI**: User interface sounds (button clicks, menu navigation)
72/// - **Custom**: User-defined channels (bits 5-31)
73///
74/// # Examples
75///
76/// ```
77/// use goud_engine::ecs::components::AudioChannel;
78///
79/// let music = AudioChannel::Music;
80/// let sfx = AudioChannel::SFX;
81/// let custom = AudioChannel::Custom(8); // Custom channel ID 8
82///
83/// assert_eq!(music.id(), 0);
84/// assert_eq!(sfx.id(), 1);
85/// assert_eq!(custom.id(), 8);
86/// ```
87#[repr(u8)]
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
89pub enum AudioChannel {
90    /// Background music tracks (channel ID: 0)
91    Music = 0,
92    /// Sound effects (channel ID: 1)
93    SFX = 1,
94    /// Voice-overs and dialogue (channel ID: 2)
95    Voice = 2,
96    /// Ambient environment sounds (channel ID: 3)
97    Ambience = 3,
98    /// User interface sounds (channel ID: 4)
99    UI = 4,
100    /// Custom channel (ID 5-31)
101    Custom(u8),
102}
103
104impl AudioChannel {
105    /// Returns the numeric channel ID (0-31).
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use goud_engine::ecs::components::AudioChannel;
111    ///
112    /// assert_eq!(AudioChannel::Music.id(), 0);
113    /// assert_eq!(AudioChannel::SFX.id(), 1);
114    /// assert_eq!(AudioChannel::Custom(10).id(), 10);
115    /// ```
116    pub fn id(&self) -> u8 {
117        match self {
118            AudioChannel::Music => 0,
119            AudioChannel::SFX => 1,
120            AudioChannel::Voice => 2,
121            AudioChannel::Ambience => 3,
122            AudioChannel::UI => 4,
123            AudioChannel::Custom(id) => *id,
124        }
125    }
126
127    /// Returns the channel name for debugging.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// use goud_engine::ecs::components::AudioChannel;
133    ///
134    /// assert_eq!(AudioChannel::Music.name(), "Music");
135    /// assert_eq!(AudioChannel::SFX.name(), "SFX");
136    /// assert_eq!(AudioChannel::Custom(10).name(), "Custom(10)");
137    /// ```
138    pub fn name(&self) -> String {
139        match self {
140            AudioChannel::Music => "Music".to_string(),
141            AudioChannel::SFX => "SFX".to_string(),
142            AudioChannel::Voice => "Voice".to_string(),
143            AudioChannel::Ambience => "Ambience".to_string(),
144            AudioChannel::UI => "UI".to_string(),
145            AudioChannel::Custom(id) => format!("Custom({})", id),
146        }
147    }
148}
149
150impl Default for AudioChannel {
151    /// Returns `AudioChannel::SFX` as the default.
152    fn default() -> Self {
153        AudioChannel::SFX
154    }
155}
156
157impl std::fmt::Display for AudioChannel {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        write!(f, "{}", self.name())
160    }
161}
162
163/// Audio attenuation model for distance-based volume falloff.
164///
165/// Controls how audio volume decreases with distance from the listener.
166/// Different models provide different falloff curves for realistic or
167/// stylized audio behavior.
168///
169/// # Models
170///
171/// - **Linear**: Linear falloff (volume = 1 - distance/max_distance)
172/// - **InverseDistance**: Realistic inverse distance falloff (volume = 1 / (1 + distance))
173/// - **Exponential**: Exponential falloff (volume = (1 - distance/max_distance)^rolloff)
174/// - **None**: No attenuation (constant volume regardless of distance)
175///
176/// # Examples
177///
178/// ```
179/// use goud_engine::ecs::components::AttenuationModel;
180///
181/// let linear = AttenuationModel::Linear;
182/// let inverse = AttenuationModel::InverseDistance;
183/// let exponential = AttenuationModel::Exponential { rolloff: 2.0 };
184/// let none = AttenuationModel::None;
185///
186/// assert_eq!(linear.name(), "Linear");
187/// assert_eq!(inverse.name(), "InverseDistance");
188/// ```
189#[derive(Clone, Copy, Debug, PartialEq)]
190pub enum AttenuationModel {
191    /// Linear falloff: volume = max(0, 1 - distance/max_distance)
192    Linear,
193    /// Inverse distance falloff: volume = 1 / (1 + distance)
194    InverseDistance,
195    /// Exponential falloff: volume = max(0, (1 - distance/max_distance)^rolloff)
196    Exponential {
197        /// The exponent for the falloff curve.
198        rolloff: f32,
199    },
200    /// No attenuation (constant volume)
201    None,
202}
203
204impl AttenuationModel {
205    /// Returns the model name for debugging.
206    pub fn name(&self) -> &str {
207        match self {
208            AttenuationModel::Linear => "Linear",
209            AttenuationModel::InverseDistance => "InverseDistance",
210            AttenuationModel::Exponential { .. } => "Exponential",
211            AttenuationModel::None => "None",
212        }
213    }
214
215    /// Computes the attenuation factor (0.0-1.0) based on distance.
216    ///
217    /// # Arguments
218    ///
219    /// - `distance`: Distance from listener (must be >= 0)
220    /// - `max_distance`: Maximum distance for attenuation (must be > 0)
221    ///
222    /// # Returns
223    ///
224    /// Volume multiplier in range [0.0, 1.0]
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use goud_engine::ecs::components::AttenuationModel;
230    ///
231    /// let model = AttenuationModel::Linear;
232    /// assert_eq!(model.compute_attenuation(0.0, 100.0), 1.0);
233    /// assert_eq!(model.compute_attenuation(50.0, 100.0), 0.5);
234    /// assert_eq!(model.compute_attenuation(100.0, 100.0), 0.0);
235    /// assert_eq!(model.compute_attenuation(150.0, 100.0), 0.0); // Beyond max
236    /// ```
237    pub fn compute_attenuation(&self, distance: f32, max_distance: f32) -> f32 {
238        match self {
239            AttenuationModel::Linear => {
240                if distance >= max_distance {
241                    0.0
242                } else {
243                    (1.0 - distance / max_distance).max(0.0)
244                }
245            }
246            AttenuationModel::InverseDistance => 1.0 / (1.0 + distance),
247            AttenuationModel::Exponential { rolloff } => {
248                if distance >= max_distance {
249                    0.0
250                } else {
251                    ((1.0 - distance / max_distance).powf(*rolloff)).max(0.0)
252                }
253            }
254            AttenuationModel::None => 1.0,
255        }
256    }
257}
258
259impl Default for AttenuationModel {
260    /// Returns `AttenuationModel::InverseDistance` as the default (most realistic).
261    fn default() -> Self {
262        AttenuationModel::InverseDistance
263    }
264}
265
266impl std::fmt::Display for AttenuationModel {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        match self {
269            AttenuationModel::Exponential { rolloff } => {
270                write!(f, "Exponential(rolloff={})", rolloff)
271            }
272            other => write!(f, "{}", other.name()),
273        }
274    }
275}
276
277/// AudioSource component for spatial audio playback.
278///
279/// Attach this component to an entity to enable audio playback. The audio system
280/// will automatically handle playback, looping, volume, pitch, and spatial audio
281/// based on the component's configuration.
282///
283/// # Fields
284///
285/// - `audio`: Reference to the audio asset to play
286/// - `playing`: Whether the audio is currently playing
287/// - `looping`: Whether the audio should loop when it finishes
288/// - `volume`: Volume multiplier (0.0 = silent, 1.0 = full volume)
289/// - `pitch`: Pitch multiplier (0.5 = half speed, 2.0 = double speed)
290/// - `channel`: Audio channel for grouping and mixing
291/// - `auto_play`: Whether to start playing automatically when spawned
292/// - `spatial`: Whether to apply spatial audio (requires Transform)
293/// - `max_distance`: Maximum distance for spatial audio attenuation
294/// - `attenuation`: Distance-based volume falloff model
295/// - `sink_id`: Internal audio sink ID (managed by audio system)
296///
297/// # Examples
298///
299/// See module-level documentation for usage examples.
300#[derive(Clone, Debug)]
301pub struct AudioSource {
302    /// Reference to the audio asset to play
303    pub audio: AssetHandle<AudioAsset>,
304    /// Whether the audio is currently playing
305    pub playing: bool,
306    /// Whether the audio should loop when it finishes
307    pub looping: bool,
308    /// Volume multiplier (0.0 = silent, 1.0 = full volume)
309    pub volume: f32,
310    /// Pitch multiplier (0.5 = half speed, 2.0 = double speed)
311    pub pitch: f32,
312    /// Audio channel for grouping and mixing
313    pub channel: AudioChannel,
314    /// Whether to start playing automatically when spawned
315    pub auto_play: bool,
316    /// Whether to apply spatial audio (requires Transform)
317    pub spatial: bool,
318    /// Maximum distance for spatial audio attenuation
319    pub max_distance: f32,
320    /// Distance-based volume falloff model
321    pub attenuation: AttenuationModel,
322    /// Internal audio sink ID (managed by audio system)
323    pub(crate) sink_id: Option<u64>,
324}
325
326impl AudioSource {
327    /// Creates a new AudioSource with default settings.
328    ///
329    /// # Arguments
330    ///
331    /// - `audio`: Reference to the audio asset to play
332    ///
333    /// # Default Values
334    ///
335    /// - playing: false (stopped)
336    /// - looping: false (one-shot)
337    /// - volume: 1.0 (full volume)
338    /// - pitch: 1.0 (normal speed)
339    /// - channel: SFX
340    /// - auto_play: false
341    /// - spatial: false (non-spatial)
342    /// - max_distance: 100.0
343    /// - attenuation: InverseDistance
344    /// - sink_id: None
345    ///
346    /// # Examples
347    ///
348    /// ```
349    /// use goud_engine::ecs::components::AudioSource;
350    /// use goud_engine::assets::AssetHandle;
351    /// use goud_engine::assets::loaders::audio::AudioAsset;
352    ///
353    /// let audio_handle: AssetHandle<AudioAsset> = AssetHandle::default();
354    /// let source = AudioSource::new(audio_handle);
355    ///
356    /// assert_eq!(source.playing, false);
357    /// assert_eq!(source.volume, 1.0);
358    /// assert_eq!(source.pitch, 1.0);
359    /// ```
360    pub fn new(audio: AssetHandle<AudioAsset>) -> Self {
361        Self {
362            audio,
363            playing: false,
364            looping: false,
365            volume: 1.0,
366            pitch: 1.0,
367            channel: AudioChannel::default(),
368            auto_play: false,
369            spatial: false,
370            max_distance: 100.0,
371            attenuation: AttenuationModel::default(),
372            sink_id: None,
373        }
374    }
375
376    /// Sets the volume (0.0-1.0, clamped).
377    ///
378    /// # Examples
379    ///
380    /// ```
381    /// use goud_engine::ecs::components::AudioSource;
382    /// use goud_engine::assets::AssetHandle;
383    /// use goud_engine::assets::loaders::audio::AudioAsset;
384    ///
385    /// let audio_handle: AssetHandle<AudioAsset> = AssetHandle::default();
386    /// let source = AudioSource::new(audio_handle).with_volume(0.5);
387    /// assert_eq!(source.volume, 0.5);
388    /// ```
389    pub fn with_volume(mut self, volume: f32) -> Self {
390        self.volume = volume.clamp(0.0, 1.0);
391        self
392    }
393
394    /// Sets the pitch (0.5-2.0, clamped).
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use goud_engine::ecs::components::AudioSource;
400    /// use goud_engine::assets::AssetHandle;
401    /// use goud_engine::assets::loaders::audio::AudioAsset;
402    ///
403    /// let audio_handle: AssetHandle<AudioAsset> = AssetHandle::default();
404    /// let source = AudioSource::new(audio_handle).with_pitch(1.5);
405    /// assert_eq!(source.pitch, 1.5);
406    /// ```
407    pub fn with_pitch(mut self, pitch: f32) -> Self {
408        self.pitch = pitch.clamp(0.5, 2.0);
409        self
410    }
411
412    /// Sets whether the audio should loop.
413    pub fn with_looping(mut self, looping: bool) -> Self {
414        self.looping = looping;
415        self
416    }
417
418    /// Sets the audio channel.
419    pub fn with_channel(mut self, channel: AudioChannel) -> Self {
420        self.channel = channel;
421        self
422    }
423
424    /// Sets whether to start playing automatically when spawned.
425    pub fn with_auto_play(mut self, auto_play: bool) -> Self {
426        self.auto_play = auto_play;
427        self
428    }
429
430    /// Sets whether to apply spatial audio (requires Transform component).
431    pub fn with_spatial(mut self, spatial: bool) -> Self {
432        self.spatial = spatial;
433        self
434    }
435
436    /// Sets the maximum distance for spatial audio attenuation.
437    pub fn with_max_distance(mut self, max_distance: f32) -> Self {
438        self.max_distance = max_distance.max(0.1);
439        self
440    }
441
442    /// Sets the attenuation model for spatial audio.
443    pub fn with_attenuation(mut self, attenuation: AttenuationModel) -> Self {
444        self.attenuation = attenuation;
445        self
446    }
447
448    /// Starts playing the audio.
449    pub fn play(&mut self) {
450        self.playing = true;
451    }
452
453    /// Pauses the audio (retains playback position).
454    pub fn pause(&mut self) {
455        self.playing = false;
456    }
457
458    /// Stops the audio (resets playback position).
459    pub fn stop(&mut self) {
460        self.playing = false;
461        self.sink_id = None;
462    }
463
464    /// Returns whether the audio is currently playing.
465    pub fn is_playing(&self) -> bool {
466        self.playing
467    }
468
469    /// Returns whether the audio is spatial.
470    pub fn is_spatial(&self) -> bool {
471        self.spatial
472    }
473
474    /// Returns whether the audio has an active sink.
475    pub fn has_sink(&self) -> bool {
476        self.sink_id.is_some()
477    }
478
479    /// Sets the internal sink ID (managed by audio system).
480    #[allow(dead_code)]
481    pub(crate) fn set_sink_id(&mut self, id: Option<u64>) {
482        self.sink_id = id;
483    }
484
485    /// Returns the internal sink ID.
486    #[allow(dead_code)]
487    pub(crate) fn sink_id(&self) -> Option<u64> {
488        self.sink_id
489    }
490}
491
492impl Component for AudioSource {}
493
494impl Default for AudioSource {
495    fn default() -> Self {
496        Self::new(AssetHandle::default())
497    }
498}
499
500impl std::fmt::Display for AudioSource {
501    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502        write!(
503            f,
504            "AudioSource(playing={}, looping={}, volume={:.2}, pitch={:.2}, channel={}, spatial={})",
505            self.playing, self.looping, self.volume, self.pitch, self.channel, self.spatial
506        )
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    // AudioChannel tests
515    #[test]
516    fn test_audio_channel_id() {
517        assert_eq!(AudioChannel::Music.id(), 0);
518        assert_eq!(AudioChannel::SFX.id(), 1);
519        assert_eq!(AudioChannel::Voice.id(), 2);
520        assert_eq!(AudioChannel::Ambience.id(), 3);
521        assert_eq!(AudioChannel::UI.id(), 4);
522        assert_eq!(AudioChannel::Custom(10).id(), 10);
523    }
524
525    #[test]
526    fn test_audio_channel_name() {
527        assert_eq!(AudioChannel::Music.name(), "Music");
528        assert_eq!(AudioChannel::SFX.name(), "SFX");
529        assert_eq!(AudioChannel::Voice.name(), "Voice");
530        assert_eq!(AudioChannel::Ambience.name(), "Ambience");
531        assert_eq!(AudioChannel::UI.name(), "UI");
532        assert_eq!(AudioChannel::Custom(10).name(), "Custom(10)");
533    }
534
535    #[test]
536    fn test_audio_channel_default() {
537        assert_eq!(AudioChannel::default(), AudioChannel::SFX);
538    }
539
540    #[test]
541    fn test_audio_channel_display() {
542        assert_eq!(format!("{}", AudioChannel::Music), "Music");
543        assert_eq!(format!("{}", AudioChannel::Custom(5)), "Custom(5)");
544    }
545
546    #[test]
547    fn test_audio_channel_clone_copy() {
548        let channel = AudioChannel::Music;
549        let cloned = channel;
550        assert_eq!(channel, cloned);
551    }
552
553    #[test]
554    fn test_audio_channel_eq_hash() {
555        use std::collections::HashSet;
556        let mut set = HashSet::new();
557        set.insert(AudioChannel::Music);
558        set.insert(AudioChannel::SFX);
559        assert!(set.contains(&AudioChannel::Music));
560        assert!(!set.contains(&AudioChannel::Voice));
561    }
562
563    // AttenuationModel tests
564    #[test]
565    fn test_attenuation_model_name() {
566        assert_eq!(AttenuationModel::Linear.name(), "Linear");
567        assert_eq!(AttenuationModel::InverseDistance.name(), "InverseDistance");
568        assert_eq!(
569            AttenuationModel::Exponential { rolloff: 2.0 }.name(),
570            "Exponential"
571        );
572        assert_eq!(AttenuationModel::None.name(), "None");
573    }
574
575    #[test]
576    fn test_attenuation_linear() {
577        let model = AttenuationModel::Linear;
578        assert_eq!(model.compute_attenuation(0.0, 100.0), 1.0);
579        assert_eq!(model.compute_attenuation(50.0, 100.0), 0.5);
580        assert_eq!(model.compute_attenuation(100.0, 100.0), 0.0);
581        assert_eq!(model.compute_attenuation(150.0, 100.0), 0.0);
582    }
583
584    #[test]
585    fn test_attenuation_inverse_distance() {
586        let model = AttenuationModel::InverseDistance;
587        assert_eq!(model.compute_attenuation(0.0, 100.0), 1.0);
588        assert!((model.compute_attenuation(1.0, 100.0) - 0.5).abs() < 0.01);
589        assert!(model.compute_attenuation(50.0, 100.0) < 1.0);
590    }
591
592    #[test]
593    fn test_attenuation_exponential() {
594        let model = AttenuationModel::Exponential { rolloff: 2.0 };
595        assert_eq!(model.compute_attenuation(0.0, 100.0), 1.0);
596        assert_eq!(model.compute_attenuation(50.0, 100.0), 0.25); // (0.5)^2
597        assert_eq!(model.compute_attenuation(100.0, 100.0), 0.0);
598    }
599
600    #[test]
601    fn test_attenuation_none() {
602        let model = AttenuationModel::None;
603        assert_eq!(model.compute_attenuation(0.0, 100.0), 1.0);
604        assert_eq!(model.compute_attenuation(50.0, 100.0), 1.0);
605        assert_eq!(model.compute_attenuation(1000.0, 100.0), 1.0);
606    }
607
608    #[test]
609    fn test_attenuation_default() {
610        let model = AttenuationModel::default();
611        assert_eq!(model.name(), "InverseDistance");
612    }
613
614    #[test]
615    fn test_attenuation_display() {
616        assert_eq!(format!("{}", AttenuationModel::Linear), "Linear");
617        assert_eq!(
618            format!("{}", AttenuationModel::Exponential { rolloff: 3.0 }),
619            "Exponential(rolloff=3)"
620        );
621    }
622
623    // AudioSource tests
624    #[test]
625    fn test_audio_source_new() {
626        let handle = AssetHandle::default();
627        let source = AudioSource::new(handle);
628
629        assert_eq!(source.playing, false);
630        assert_eq!(source.looping, false);
631        assert_eq!(source.volume, 1.0);
632        assert_eq!(source.pitch, 1.0);
633        assert_eq!(source.channel, AudioChannel::SFX);
634        assert_eq!(source.auto_play, false);
635        assert_eq!(source.spatial, false);
636        assert_eq!(source.max_distance, 100.0);
637        assert_eq!(source.attenuation.name(), "InverseDistance");
638        assert_eq!(source.sink_id, None);
639    }
640
641    #[test]
642    fn test_audio_source_with_volume() {
643        let handle = AssetHandle::default();
644        let source = AudioSource::new(handle).with_volume(0.5);
645        assert_eq!(source.volume, 0.5);
646
647        // Test clamping
648        let source = AudioSource::new(handle).with_volume(-0.1);
649        assert_eq!(source.volume, 0.0);
650
651        let source = AudioSource::new(handle).with_volume(1.5);
652        assert_eq!(source.volume, 1.0);
653    }
654
655    #[test]
656    fn test_audio_source_with_pitch() {
657        let handle = AssetHandle::default();
658        let source = AudioSource::new(handle).with_pitch(1.5);
659        assert_eq!(source.pitch, 1.5);
660
661        // Test clamping
662        let source = AudioSource::new(handle).with_pitch(0.1);
663        assert_eq!(source.pitch, 0.5);
664
665        let source = AudioSource::new(handle).with_pitch(3.0);
666        assert_eq!(source.pitch, 2.0);
667    }
668
669    #[test]
670    fn test_audio_source_builder_pattern() {
671        let handle = AssetHandle::default();
672        let source = AudioSource::new(handle)
673            .with_volume(0.8)
674            .with_pitch(1.2)
675            .with_looping(true)
676            .with_channel(AudioChannel::Music)
677            .with_auto_play(true)
678            .with_spatial(true)
679            .with_max_distance(200.0)
680            .with_attenuation(AttenuationModel::Linear);
681
682        assert_eq!(source.volume, 0.8);
683        assert_eq!(source.pitch, 1.2);
684        assert_eq!(source.looping, true);
685        assert_eq!(source.channel, AudioChannel::Music);
686        assert_eq!(source.auto_play, true);
687        assert_eq!(source.spatial, true);
688        assert_eq!(source.max_distance, 200.0);
689        assert_eq!(source.attenuation.name(), "Linear");
690    }
691
692    #[test]
693    fn test_audio_source_play_pause_stop() {
694        let handle = AssetHandle::default();
695        let mut source = AudioSource::new(handle);
696
697        assert_eq!(source.is_playing(), false);
698
699        source.play();
700        assert_eq!(source.is_playing(), true);
701
702        source.pause();
703        assert_eq!(source.is_playing(), false);
704
705        source.play();
706        assert_eq!(source.is_playing(), true);
707
708        source.stop();
709        assert_eq!(source.is_playing(), false);
710        assert_eq!(source.sink_id, None);
711    }
712
713    #[test]
714    fn test_audio_source_is_spatial() {
715        let handle = AssetHandle::default();
716        let source = AudioSource::new(handle);
717        assert_eq!(source.is_spatial(), false);
718
719        let source = source.with_spatial(true);
720        assert_eq!(source.is_spatial(), true);
721    }
722
723    #[test]
724    fn test_audio_source_sink_id() {
725        let handle = AssetHandle::default();
726        let mut source = AudioSource::new(handle);
727
728        assert_eq!(source.has_sink(), false);
729        assert_eq!(source.sink_id(), None);
730
731        source.set_sink_id(Some(42));
732        assert_eq!(source.has_sink(), true);
733        assert_eq!(source.sink_id(), Some(42));
734
735        source.set_sink_id(None);
736        assert_eq!(source.has_sink(), false);
737    }
738
739    #[test]
740    fn test_audio_source_default() {
741        let source = AudioSource::default();
742        assert_eq!(source.playing, false);
743        assert_eq!(source.volume, 1.0);
744    }
745
746    #[test]
747    fn test_audio_source_display() {
748        let handle = AssetHandle::default();
749        let source = AudioSource::new(handle)
750            .with_volume(0.75)
751            .with_pitch(1.5)
752            .with_channel(AudioChannel::Music);
753
754        let display = format!("{}", source);
755        assert!(display.contains("playing=false"));
756        assert!(display.contains("volume=0.75"));
757        assert!(display.contains("pitch=1.50"));
758        assert!(display.contains("channel=Music"));
759    }
760
761    #[test]
762    fn test_audio_source_component() {
763        let handle = AssetHandle::default();
764        let _source: Box<dyn Component> = Box::new(AudioSource::new(handle));
765    }
766
767    #[test]
768    fn test_audio_source_clone() {
769        let handle = AssetHandle::default();
770        let source = AudioSource::new(handle).with_volume(0.5);
771        let cloned = source.clone();
772        assert_eq!(cloned.volume, 0.5);
773    }
774
775    #[test]
776    fn test_audio_source_debug() {
777        let handle = AssetHandle::default();
778        let source = AudioSource::new(handle);
779        let debug = format!("{:?}", source);
780        assert!(debug.contains("AudioSource"));
781    }
782}