1use crate::components::Component;
16use crate::reflect::{AssetKind, Field};
17use glam::Vec3;
18use serde::{Deserialize, Serialize};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub struct SoundId(pub u64);
23
24#[derive(Clone, Debug)]
26pub enum AudioCmd {
27 Play {
29 id: SoundId,
31 clip: String,
33 volume: f32,
35 pitch: f32,
37 pan: f32,
39 looped: bool,
41 position: Option<Vec3>,
43 min_distance: f32,
45 max_distance: f32,
47 },
48 Stop(SoundId),
50 StopAll,
52 SetVolume(SoundId, f32),
54}
55
56#[derive(Clone, Debug)]
58pub struct AudioBus {
59 pub master_volume: f32,
61 pub muted: bool,
63 pub reverb: f32,
65 pub reverb_room_ms: f32,
68 pub music_volume: f32,
70 pub effects_volume: f32,
72 next: u64,
73 commands: Vec<AudioCmd>,
74}
75
76impl Default for AudioBus {
77 fn default() -> Self {
78 Self { master_volume: 1.0, muted: false, reverb: 0.0, reverb_room_ms: 45.0, music_volume: 1.0, effects_volume: 1.0, next: 1, commands: Vec::new() }
79 }
80}
81
82impl AudioBus {
83 fn push_play(&mut self, clip: &str, volume: f32, pitch: f32, pan: f32, looped: bool, position: Option<Vec3>) -> SoundId {
84 let id = SoundId(self.next);
85 self.next += 1;
86 self.commands.push(AudioCmd::Play {
87 id,
88 clip: clip.to_string(),
89 volume: volume.max(0.0),
90 pitch: if pitch.is_finite() && pitch > 0.0 { pitch } else { 1.0 },
91 pan: pan.clamp(-1.0, 1.0),
92 looped,
93 position,
94 min_distance: 2.0,
95 max_distance: 40.0,
96 });
97 id
98 }
99
100 pub fn play_with(&mut self, clip: &str, volume: f32, pitch: f32, pan: f32, looped: bool) -> SoundId {
102 self.push_play(clip, volume, pitch, pan, looped, None)
103 }
104
105 pub fn play(&mut self, clip: &str, volume: f32) -> SoundId {
107 self.push_play(clip, volume, 1.0, 0.0, false, None)
108 }
109
110 pub fn play_loop(&mut self, clip: &str, volume: f32) -> SoundId {
112 self.push_play(clip, volume, 1.0, 0.0, true, None)
113 }
114
115 pub fn play_panned(&mut self, clip: &str, volume: f32, pan: f32) -> SoundId {
117 self.push_play(clip, volume, 1.0, pan, false, None)
118 }
119
120 pub fn play_pitched(&mut self, clip: &str, volume: f32, pitch: f32) -> SoundId {
122 self.push_play(clip, volume, pitch, 0.0, false, None)
123 }
124
125 pub fn play_at(&mut self, clip: &str, volume: f32, position: Vec3) -> SoundId {
127 self.push_play(clip, volume, 1.0, 0.0, false, Some(position))
128 }
129
130 pub fn stop(&mut self, id: SoundId) {
132 self.commands.push(AudioCmd::Stop(id));
133 }
134
135 pub fn stop_all(&mut self) {
137 self.commands.push(AudioCmd::StopAll);
138 }
139
140 pub fn set_volume(&mut self, id: SoundId, volume: f32) {
142 self.commands.push(AudioCmd::SetVolume(id, volume.max(0.0)));
143 }
144
145 pub fn drain(&mut self) -> Vec<AudioCmd> {
147 std::mem::take(&mut self.commands)
148 }
149}
150
151#[derive(Clone, Debug, Serialize, Deserialize)]
156#[serde(default)]
157pub struct AudioSource {
158 pub clip: String,
160 pub volume: f32,
162 pub pitch: f32,
164 pub looping: bool,
166 pub play_on_start: bool,
168 pub spatial: bool,
170 pub min_distance: f32,
172 pub max_distance: f32,
174 pub pan: f32,
176 pub trigger: bool,
178 pub cone_angle: f32,
181}
182
183impl Default for AudioSource {
184 fn default() -> Self {
185 Self {
186 clip: String::new(),
187 volume: 1.0,
188 pitch: 1.0,
189 looping: true,
190 play_on_start: true,
191 spatial: true,
192 min_distance: 2.0,
193 max_distance: 40.0,
194 pan: 0.0,
195 trigger: false,
196 cone_angle: 360.0,
197 }
198 }
199}
200
201impl Component for AudioSource {
202 fn fields(&mut self) -> Vec<Field<'_>> {
203 vec![
204 Field::asset("clip", &mut self.clip, AssetKind::Audio),
205 Field::f32("volume", &mut self.volume),
206 Field::f32("pitch", &mut self.pitch),
207 Field::bool("looping", &mut self.looping),
208 Field::bool("play_on_start", &mut self.play_on_start),
209 Field::bool("spatial", &mut self.spatial),
210 Field::f32("min_distance", &mut self.min_distance),
211 Field::f32("max_distance", &mut self.max_distance),
212 Field::f32("pan", &mut self.pan),
213 Field::f32("cone_angle", &mut self.cone_angle),
214 Field::bool("trigger", &mut self.trigger),
215 ]
216 }
217}
218
219pub fn distance_gain(distance: f32, min: f32, max: f32) -> f32 {
221 if distance <= min {
222 1.0
223 } else if distance >= max || max <= min {
224 0.0
225 } else {
226 let t = (distance - min) / (max - min);
227 (1.0 - t) * (1.0 - t)
228 }
229}
230
231pub fn cone_gain(forward: Vec3, to_listener: Vec3, cone_degrees: f32) -> f32 {
234 if cone_degrees >= 359.0 {
235 return 1.0;
236 }
237 let (f, t) = (forward.normalize_or_zero(), to_listener.normalize_or_zero());
238 if f == Vec3::ZERO || t == Vec3::ZERO {
239 return 1.0;
240 }
241 let angle = f.dot(t).clamp(-1.0, 1.0).acos().to_degrees();
242 let half = (cone_degrees * 0.5).max(1.0);
243 let inner = half * 0.6;
244 if angle <= inner {
245 1.0
246 } else if angle >= half {
247 0.1
248 } else {
249 let k = (angle - inner) / (half - inner);
250 1.0 - 0.9 * k * k * (3.0 - 2.0 * k)
251 }
252}
253
254pub fn stereo_pan(listener: Vec3, listener_right: Vec3, source: Vec3) -> f32 {
256 let to = source - listener;
257 let d = to.length();
258 if d < 1e-3 {
259 return 0.0;
260 }
261 (to.dot(listener_right) / d).clamp(-1.0, 1.0)
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn falloff_is_one_inside_and_zero_outside() {
270 assert_eq!(distance_gain(1.0, 2.0, 10.0), 1.0);
271 assert_eq!(distance_gain(10.0, 2.0, 10.0), 0.0);
272 assert!(distance_gain(6.0, 2.0, 10.0) < 0.5);
273 }
274
275 #[test]
276 fn cone_is_loud_in_front_and_quiet_behind() {
277 assert_eq!(cone_gain(Vec3::NEG_Z, Vec3::NEG_Z, 90.0), 1.0);
278 assert_eq!(cone_gain(Vec3::NEG_Z, Vec3::Z, 90.0), 0.1);
279 assert_eq!(cone_gain(Vec3::NEG_Z, Vec3::Z, 360.0), 1.0);
280 let mid = cone_gain(Vec3::NEG_Z, Vec3::new(0.6, 0.0, -0.8), 90.0);
281 assert!(mid < 1.0 && mid > 0.1);
282 }
283
284 #[test]
285 fn pan_follows_side() {
286 assert!(stereo_pan(Vec3::ZERO, Vec3::X, Vec3::new(5.0, 0.0, 0.0)) > 0.99);
287 assert!(stereo_pan(Vec3::ZERO, Vec3::X, Vec3::new(-5.0, 0.0, 0.0)) < -0.99);
288 }
289}