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}