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}