1#![allow(clippy::missing_const_for_fn)]
18
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub enum AudioEvent {
24 PaddleHit {
26 frequency: f32,
28 duration: f32,
30 volume: f32,
32 },
33 WallBounce {
35 frequency: f32,
37 duration: f32,
39 volume: f32,
41 },
42 Goal {
44 player_scored: bool,
46 volume: f32,
48 },
49 GameStart {
51 volume: f32,
53 },
54 RallyMilestone {
56 rally_count: u32,
58 frequency: f32,
60 volume: f32,
62 },
63 SoundToggle {
65 enabled: bool,
67 volume: f32,
69 },
70}
71
72#[derive(Debug, Clone)]
93pub struct ProceduralAudio {
94 master_volume: f32,
96 enabled: bool,
98 events: Vec<AudioEvent>,
100}
101
102impl Default for ProceduralAudio {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl ProceduralAudio {
109 #[must_use]
111 pub fn new() -> Self {
112 Self {
113 master_volume: 0.7,
114 enabled: true,
115 events: Vec::with_capacity(4),
116 }
117 }
118
119 pub fn set_volume(&mut self, volume: f32) {
121 self.master_volume = volume.clamp(0.0, 1.0);
122 }
123
124 #[must_use]
126 pub const fn volume(&self) -> f32 {
127 self.master_volume
128 }
129
130 pub fn set_enabled(&mut self, enabled: bool) {
132 self.enabled = enabled;
133 }
134
135 #[must_use]
137 pub const fn is_enabled(&self) -> bool {
138 self.enabled
139 }
140
141 pub fn on_paddle_hit(&mut self, hit_y: f32, paddle_y: f32, paddle_height: f32) {
149 if !self.enabled {
150 return;
151 }
152
153 let half_height = paddle_height / 2.0;
155 let relative_y = (hit_y - paddle_y) / half_height;
156 let normalized = relative_y.clamp(-1.0, 1.0);
157
158 let frequency = 440.0 + normalized * 220.0;
160
161 self.events.push(AudioEvent::PaddleHit {
162 frequency,
163 duration: 0.08,
164 volume: self.master_volume,
165 });
166 }
167
168 pub fn on_wall_bounce(&mut self) {
170 if !self.enabled {
171 return;
172 }
173
174 let variation = (self.events.len() % 5) as f32 * 10.0;
177 let frequency = 180.0 + variation;
178
179 self.events.push(AudioEvent::WallBounce {
180 frequency,
181 duration: 0.05,
182 volume: self.master_volume * 0.5, });
184 }
185
186 pub fn on_wall_bounce_with_velocity(&mut self, ball_speed: f32, base_speed: f32) {
193 if !self.enabled {
194 return;
195 }
196
197 let speed_ratio = (ball_speed / base_speed).clamp(0.5, 2.0);
199 let frequency = 150.0 + speed_ratio * 75.0;
200
201 self.events.push(AudioEvent::WallBounce {
202 frequency,
203 duration: 0.05,
204 volume: self.master_volume * 0.5,
205 });
206 }
207
208 pub fn on_goal(&mut self, player_scored: bool) {
214 if !self.enabled {
215 return;
216 }
217
218 self.events.push(AudioEvent::Goal {
219 player_scored,
220 volume: self.master_volume,
221 });
222 }
223
224 pub fn on_game_start(&mut self) {
226 if !self.enabled {
227 return;
228 }
229
230 self.events.push(AudioEvent::GameStart {
231 volume: self.master_volume,
232 });
233 }
234
235 pub fn on_rally_milestone(&mut self, rally_count: u32) {
243 if !self.enabled {
244 return;
245 }
246
247 let base_freq = 300.0;
249 let freq_increase = (rally_count as f32 / 5.0) * 50.0;
250 let frequency = (base_freq + freq_increase).min(800.0);
251
252 self.events.push(AudioEvent::RallyMilestone {
253 rally_count,
254 frequency,
255 volume: self.master_volume,
256 });
257 }
258
259 pub fn on_sound_toggle(&mut self, enabled: bool) {
264 if enabled {
267 self.events.push(AudioEvent::SoundToggle {
268 enabled,
269 volume: self.master_volume,
270 });
271 }
272 }
273
274 pub fn take_events(&mut self) -> Vec<AudioEvent> {
280 core::mem::take(&mut self.events)
281 }
282
283 #[must_use]
285 pub fn peek_events(&self) -> &[AudioEvent] {
286 &self.events
287 }
288
289 pub fn clear_events(&mut self) {
291 self.events.clear();
292 }
293
294 #[must_use]
296 pub fn event_count(&self) -> usize {
297 self.events.len()
298 }
299}
300
301#[cfg(test)]
302#[allow(clippy::unwrap_used, clippy::float_cmp, clippy::panic)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_procedural_audio_new() {
308 let audio = ProceduralAudio::new();
309 assert!(audio.is_enabled());
310 assert!((audio.volume() - 0.7).abs() < 0.001);
311 assert_eq!(audio.event_count(), 0);
312 }
313
314 #[test]
315 fn test_procedural_audio_default() {
316 let audio = ProceduralAudio::default();
317 assert!((audio.volume() - 0.7).abs() < 0.001);
318 }
319
320 #[test]
321 fn test_set_volume() {
322 let mut audio = ProceduralAudio::new();
323
324 audio.set_volume(0.5);
325 assert!((audio.volume() - 0.5).abs() < 0.001);
326
327 audio.set_volume(1.5);
329 assert!((audio.volume() - 1.0).abs() < 0.001);
330
331 audio.set_volume(-0.5);
332 assert!(audio.volume().abs() < 0.001);
333 }
334
335 #[test]
336 fn test_set_enabled() {
337 let mut audio = ProceduralAudio::new();
338
339 audio.set_enabled(false);
340 assert!(!audio.is_enabled());
341
342 audio.set_enabled(true);
343 assert!(audio.is_enabled());
344 }
345
346 #[test]
347 fn test_on_paddle_hit() {
348 let mut audio = ProceduralAudio::new();
349
350 audio.on_paddle_hit(300.0, 300.0, 100.0);
352
353 assert_eq!(audio.event_count(), 1);
354 let events = audio.take_events();
355 match &events[0] {
356 AudioEvent::PaddleHit {
357 frequency,
358 duration,
359 volume,
360 } => {
361 assert!((*frequency - 440.0).abs() < 1.0); assert!((*duration - 0.08).abs() < 0.01);
363 assert!((*volume - 0.7).abs() < 0.01);
364 }
365 _ => panic!("Expected PaddleHit event"),
366 }
367 }
368
369 #[test]
370 fn test_on_paddle_hit_top() {
371 let mut audio = ProceduralAudio::new();
372
373 audio.on_paddle_hit(250.0, 300.0, 100.0);
375
376 let events = audio.take_events();
377 match &events[0] {
378 AudioEvent::PaddleHit { frequency, .. } => {
379 assert!(*frequency < 440.0); }
381 _ => panic!("Expected PaddleHit event"),
382 }
383 }
384
385 #[test]
386 fn test_on_paddle_hit_bottom() {
387 let mut audio = ProceduralAudio::new();
388
389 audio.on_paddle_hit(350.0, 300.0, 100.0);
391
392 let events = audio.take_events();
393 match &events[0] {
394 AudioEvent::PaddleHit { frequency, .. } => {
395 assert!(*frequency > 440.0); }
397 _ => panic!("Expected PaddleHit event"),
398 }
399 }
400
401 #[test]
402 fn test_on_wall_bounce() {
403 let mut audio = ProceduralAudio::new();
404
405 audio.on_wall_bounce();
406
407 assert_eq!(audio.event_count(), 1);
408 let events = audio.take_events();
409 match &events[0] {
410 AudioEvent::WallBounce {
411 frequency,
412 duration,
413 volume,
414 } => {
415 assert!(*frequency >= 180.0 && *frequency <= 220.0);
417 assert!((*duration - 0.05).abs() < 0.01);
418 assert!(*volume < 0.7); }
420 _ => panic!("Expected WallBounce event"),
421 }
422 }
423
424 #[test]
425 fn test_on_goal_player() {
426 let mut audio = ProceduralAudio::new();
427
428 audio.on_goal(true);
429
430 assert_eq!(audio.event_count(), 1);
431 let events = audio.take_events();
432 match &events[0] {
433 AudioEvent::Goal {
434 player_scored,
435 volume,
436 } => {
437 assert!(*player_scored);
438 assert!((*volume - 0.7).abs() < 0.01);
439 }
440 _ => panic!("Expected Goal event"),
441 }
442 }
443
444 #[test]
445 fn test_on_goal_ai() {
446 let mut audio = ProceduralAudio::new();
447
448 audio.on_goal(false);
449
450 let events = audio.take_events();
451 match &events[0] {
452 AudioEvent::Goal { player_scored, .. } => {
453 assert!(!*player_scored);
454 }
455 _ => panic!("Expected Goal event"),
456 }
457 }
458
459 #[test]
460 fn test_on_game_start() {
461 let mut audio = ProceduralAudio::new();
462
463 audio.on_game_start();
464
465 assert_eq!(audio.event_count(), 1);
466 let events = audio.take_events();
467 matches!(&events[0], AudioEvent::GameStart { .. });
468 }
469
470 #[test]
471 fn test_disabled_audio_no_events() {
472 let mut audio = ProceduralAudio::new();
473 audio.set_enabled(false);
474
475 audio.on_paddle_hit(300.0, 300.0, 100.0);
476 audio.on_wall_bounce();
477 audio.on_goal(true);
478 audio.on_game_start();
479
480 assert_eq!(audio.event_count(), 0);
481 }
482
483 #[test]
484 fn test_take_events_clears() {
485 let mut audio = ProceduralAudio::new();
486
487 audio.on_wall_bounce();
488 audio.on_wall_bounce();
489
490 assert_eq!(audio.event_count(), 2);
491 let events = audio.take_events();
492 assert_eq!(events.len(), 2);
493 assert_eq!(audio.event_count(), 0);
494 }
495
496 #[test]
497 fn test_peek_events_does_not_clear() {
498 let mut audio = ProceduralAudio::new();
499
500 audio.on_wall_bounce();
501
502 let events = audio.peek_events();
503 assert_eq!(events.len(), 1);
504 assert_eq!(audio.event_count(), 1); }
506
507 #[test]
508 fn test_clear_events() {
509 let mut audio = ProceduralAudio::new();
510
511 audio.on_wall_bounce();
512 audio.on_wall_bounce();
513 audio.clear_events();
514
515 assert_eq!(audio.event_count(), 0);
516 }
517
518 #[test]
519 fn test_multiple_events() {
520 let mut audio = ProceduralAudio::new();
521
522 audio.on_paddle_hit(300.0, 300.0, 100.0);
523 audio.on_wall_bounce();
524 audio.on_goal(true);
525
526 assert_eq!(audio.event_count(), 3);
527
528 let events = audio.take_events();
529 assert!(matches!(events[0], AudioEvent::PaddleHit { .. }));
530 assert!(matches!(events[1], AudioEvent::WallBounce { .. }));
531 assert!(matches!(events[2], AudioEvent::Goal { .. }));
532 }
533
534 #[test]
535 fn test_audio_event_serialization() {
536 let event = AudioEvent::PaddleHit {
537 frequency: 440.0,
538 duration: 0.08,
539 volume: 0.7,
540 };
541
542 let json = serde_json::to_string(&event).unwrap();
543 assert!(json.contains("PaddleHit"));
544 assert!(json.contains("440"));
545
546 let deserialized: AudioEvent = serde_json::from_str(&json).unwrap();
547 assert_eq!(event, deserialized);
548 }
549
550 #[test]
551 fn test_wall_bounce_serialization() {
552 let event = AudioEvent::WallBounce {
553 frequency: 200.0,
554 duration: 0.05,
555 volume: 0.35,
556 };
557
558 let json = serde_json::to_string(&event).unwrap();
559 assert!(json.contains("WallBounce"));
560 }
561
562 #[test]
563 fn test_goal_serialization() {
564 let event = AudioEvent::Goal {
565 player_scored: true,
566 volume: 0.7,
567 };
568
569 let json = serde_json::to_string(&event).unwrap();
570 assert!(json.contains("Goal"));
571 assert!(json.contains("player_scored"));
572 }
573
574 #[test]
575 fn test_game_start_serialization() {
576 let event = AudioEvent::GameStart { volume: 0.7 };
577
578 let json = serde_json::to_string(&event).unwrap();
579 assert!(json.contains("GameStart"));
580 }
581
582 #[test]
583 fn test_wall_bounce_with_velocity_low_speed() {
584 let mut audio = ProceduralAudio::new();
585 audio.on_wall_bounce_with_velocity(125.0, 250.0); let events = audio.take_events();
588 match &events[0] {
589 AudioEvent::WallBounce { frequency, .. } => {
590 assert!(*frequency >= 150.0 && *frequency <= 200.0);
592 }
593 _ => panic!("Expected WallBounce event"),
594 }
595 }
596
597 #[test]
598 fn test_wall_bounce_with_velocity_high_speed() {
599 let mut audio = ProceduralAudio::new();
600 audio.on_wall_bounce_with_velocity(500.0, 250.0); let events = audio.take_events();
603 match &events[0] {
604 AudioEvent::WallBounce { frequency, .. } => {
605 assert!(*frequency >= 250.0 && *frequency <= 350.0);
607 }
608 _ => panic!("Expected WallBounce event"),
609 }
610 }
611
612 #[test]
613 fn test_rally_milestone() {
614 let mut audio = ProceduralAudio::new();
615 audio.on_rally_milestone(5);
616
617 assert_eq!(audio.event_count(), 1);
618 let events = audio.take_events();
619 match &events[0] {
620 AudioEvent::RallyMilestone {
621 rally_count,
622 frequency,
623 volume,
624 } => {
625 assert_eq!(*rally_count, 5);
626 assert!(*frequency >= 300.0);
627 assert!((*volume - 0.7).abs() < 0.01);
628 }
629 _ => panic!("Expected RallyMilestone event"),
630 }
631 }
632
633 #[test]
634 fn test_rally_milestone_increasing_frequency() {
635 let mut audio = ProceduralAudio::new();
636
637 audio.on_rally_milestone(5);
638 audio.on_rally_milestone(15);
639
640 let events = audio.take_events();
641
642 let freq_5 = match &events[0] {
643 AudioEvent::RallyMilestone { frequency, .. } => *frequency,
644 _ => panic!("Expected RallyMilestone"),
645 };
646
647 let freq_15 = match &events[1] {
648 AudioEvent::RallyMilestone { frequency, .. } => *frequency,
649 _ => panic!("Expected RallyMilestone"),
650 };
651
652 assert!(freq_15 > freq_5);
654 }
655
656 #[test]
657 fn test_rally_milestone_serialization() {
658 let event = AudioEvent::RallyMilestone {
659 rally_count: 10,
660 frequency: 400.0,
661 volume: 0.7,
662 };
663
664 let json = serde_json::to_string(&event).unwrap();
665 assert!(json.contains("RallyMilestone"));
666 assert!(json.contains("rally_count"));
667 assert!(json.contains("10"));
668 }
669
670 #[test]
671 fn test_wall_bounce_pitch_variation() {
672 let mut audio = ProceduralAudio::new();
673
674 audio.on_wall_bounce();
676 audio.on_wall_bounce();
677 audio.on_wall_bounce();
678
679 let events = audio.take_events();
680
681 let freqs: Vec<f32> = events
683 .iter()
684 .map(|e| match e {
685 AudioEvent::WallBounce { frequency, .. } => *frequency,
686 _ => 0.0,
687 })
688 .collect();
689
690 for freq in &freqs {
692 assert!(*freq >= 180.0 && *freq <= 220.0);
693 }
694 }
695}