Skip to main content

goud_engine/ecs/components/audiosource/
source.rs

1//! AudioSource component for spatial audio playback.
2
3use crate::assets::loaders::audio::AudioAsset;
4use crate::assets::AssetHandle;
5use crate::ecs::Component;
6
7use super::attenuation::AttenuationModel;
8use super::channel::AudioChannel;
9
10/// AudioSource component for spatial audio playback.
11///
12/// Attach this component to an entity to enable audio playback. The audio system
13/// will automatically handle playback, looping, volume, pitch, and spatial audio
14/// based on the component's configuration.
15///
16/// # Fields
17///
18/// - `audio`: Reference to the audio asset to play
19/// - `playing`: Whether the audio is currently playing
20/// - `looping`: Whether the audio should loop when it finishes
21/// - `volume`: Volume multiplier (0.0 = silent, 1.0 = full volume)
22/// - `pitch`: Pitch multiplier (0.5 = half speed, 2.0 = double speed)
23/// - `channel`: Audio channel for grouping and mixing
24/// - `auto_play`: Whether to start playing automatically when spawned
25/// - `spatial`: Whether to apply spatial audio (requires Transform)
26/// - `max_distance`: Maximum distance for spatial audio attenuation
27/// - `attenuation`: Distance-based volume falloff model
28/// - `sink_id`: Internal audio sink ID (managed by audio system)
29///
30/// # Examples
31///
32/// See module-level documentation for usage examples.
33#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
34pub struct AudioSource {
35    /// Reference to the audio asset to play
36    // TODO(#219): Resolve audio_path back to a handle on deserialization
37    #[serde(skip)]
38    pub audio: AssetHandle<AudioAsset>,
39    /// Optional path to the audio asset for serialization.
40    ///
41    /// The handle cannot survive serialization, but this path string
42    /// can. A higher-level system resolves it back to an
43    /// [`AssetHandle`] after deserialization.
44    #[serde(default)]
45    pub audio_path: Option<String>,
46    /// Whether the audio is currently playing
47    pub playing: bool,
48    /// Whether the audio should loop when it finishes
49    pub looping: bool,
50    /// Volume multiplier (0.0 = silent, 1.0 = full volume)
51    pub volume: f32,
52    /// Pitch multiplier (0.5 = half speed, 2.0 = double speed)
53    pub pitch: f32,
54    /// Audio channel for grouping and mixing
55    pub channel: AudioChannel,
56    /// Whether to start playing automatically when spawned
57    pub auto_play: bool,
58    /// Whether to apply spatial audio (requires Transform)
59    pub spatial: bool,
60    /// Maximum distance for spatial audio attenuation
61    pub max_distance: f32,
62    /// Distance-based volume falloff model
63    pub attenuation: AttenuationModel,
64    /// Internal audio sink ID (managed by audio system)
65    #[serde(skip)]
66    pub(crate) sink_id: Option<u64>,
67}
68
69impl AudioSource {
70    /// Creates a new AudioSource with default settings.
71    ///
72    /// # Arguments
73    ///
74    /// - `audio`: Reference to the audio asset to play
75    ///
76    /// # Default Values
77    ///
78    /// - playing: false (stopped)
79    /// - looping: false (one-shot)
80    /// - volume: 1.0 (full volume)
81    /// - pitch: 1.0 (normal speed)
82    /// - channel: SFX
83    /// - auto_play: false
84    /// - spatial: false (non-spatial)
85    /// - max_distance: 100.0
86    /// - attenuation: InverseDistance
87    /// - sink_id: None
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use goud_engine::ecs::components::AudioSource;
93    /// use goud_engine::assets::AssetHandle;
94    /// use goud_engine::assets::loaders::audio::AudioAsset;
95    ///
96    /// let audio_handle: AssetHandle<AudioAsset> = AssetHandle::default();
97    /// let source = AudioSource::new(audio_handle);
98    ///
99    /// assert_eq!(source.playing, false);
100    /// assert_eq!(source.volume, 1.0);
101    /// assert_eq!(source.pitch, 1.0);
102    /// ```
103    pub fn new(audio: AssetHandle<AudioAsset>) -> Self {
104        Self {
105            audio,
106            audio_path: None,
107            playing: false,
108            looping: false,
109            volume: 1.0,
110            pitch: 1.0,
111            channel: AudioChannel::default(),
112            auto_play: false,
113            spatial: false,
114            max_distance: 100.0,
115            attenuation: AttenuationModel::default(),
116            sink_id: None,
117        }
118    }
119
120    /// Sets the volume (0.0-1.0, clamped).
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use goud_engine::ecs::components::AudioSource;
126    /// use goud_engine::assets::AssetHandle;
127    /// use goud_engine::assets::loaders::audio::AudioAsset;
128    ///
129    /// let audio_handle: AssetHandle<AudioAsset> = AssetHandle::default();
130    /// let source = AudioSource::new(audio_handle).with_volume(0.5);
131    /// assert_eq!(source.volume, 0.5);
132    /// ```
133    pub fn with_volume(mut self, volume: f32) -> Self {
134        self.volume = volume.clamp(0.0, 1.0);
135        self
136    }
137
138    /// Sets the pitch (0.5-2.0, clamped).
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use goud_engine::ecs::components::AudioSource;
144    /// use goud_engine::assets::AssetHandle;
145    /// use goud_engine::assets::loaders::audio::AudioAsset;
146    ///
147    /// let audio_handle: AssetHandle<AudioAsset> = AssetHandle::default();
148    /// let source = AudioSource::new(audio_handle).with_pitch(1.5);
149    /// assert_eq!(source.pitch, 1.5);
150    /// ```
151    pub fn with_pitch(mut self, pitch: f32) -> Self {
152        self.pitch = pitch.clamp(0.5, 2.0);
153        self
154    }
155
156    /// Sets whether the audio should loop.
157    pub fn with_looping(mut self, looping: bool) -> Self {
158        self.looping = looping;
159        self
160    }
161
162    /// Sets the audio channel.
163    pub fn with_channel(mut self, channel: AudioChannel) -> Self {
164        self.channel = channel;
165        self
166    }
167
168    /// Sets whether to start playing automatically when spawned.
169    pub fn with_auto_play(mut self, auto_play: bool) -> Self {
170        self.auto_play = auto_play;
171        self
172    }
173
174    /// Sets the audio asset path for serialization.
175    ///
176    /// The path is stored alongside the source so it survives
177    /// serialization. A higher-level system resolves it back to an
178    /// [`AssetHandle`] after deserialization.
179    pub fn with_audio_path(mut self, path: impl Into<String>) -> Self {
180        self.audio_path = Some(path.into());
181        self
182    }
183
184    /// Sets whether to apply spatial audio (requires Transform component).
185    pub fn with_spatial(mut self, spatial: bool) -> Self {
186        self.spatial = spatial;
187        self
188    }
189
190    /// Sets the maximum distance for spatial audio attenuation.
191    pub fn with_max_distance(mut self, max_distance: f32) -> Self {
192        self.max_distance = max_distance.max(0.1);
193        self
194    }
195
196    /// Sets the attenuation model for spatial audio.
197    pub fn with_attenuation(mut self, attenuation: AttenuationModel) -> Self {
198        self.attenuation = attenuation;
199        self
200    }
201
202    /// Starts playing the audio.
203    pub fn play(&mut self) {
204        self.playing = true;
205    }
206
207    /// Pauses the audio (retains playback position).
208    pub fn pause(&mut self) {
209        self.playing = false;
210    }
211
212    /// Stops the audio (resets playback position).
213    pub fn stop(&mut self) {
214        self.playing = false;
215        self.sink_id = None;
216    }
217
218    /// Returns whether the audio is currently playing.
219    pub fn is_playing(&self) -> bool {
220        self.playing
221    }
222
223    /// Returns whether the audio is spatial.
224    pub fn is_spatial(&self) -> bool {
225        self.spatial
226    }
227
228    /// Returns whether the audio has an active sink.
229    pub fn has_sink(&self) -> bool {
230        self.sink_id.is_some()
231    }
232
233    /// Sets the internal sink ID (managed by audio system).
234    #[cfg(test)]
235    pub(crate) fn set_sink_id(&mut self, id: Option<u64>) {
236        self.sink_id = id;
237    }
238
239    /// Returns the internal sink ID.
240    #[cfg(test)]
241    pub(crate) fn sink_id(&self) -> Option<u64> {
242        self.sink_id
243    }
244}
245
246impl Component for AudioSource {}
247
248impl Default for AudioSource {
249    fn default() -> Self {
250        Self::new(AssetHandle::default())
251    }
252}
253
254impl std::fmt::Display for AudioSource {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        write!(
257            f,
258            "AudioSource(playing={}, looping={}, volume={:.2}, pitch={:.2}, channel={}, spatial={})",
259            self.playing, self.looping, self.volume, self.pitch, self.channel, self.spatial
260        )
261    }
262}