using AlienAttack.MonoGame.Things.Items; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Media; using System; using System.Collections.Generic; namespace AlienAttack.MonoGame.Audio; public sealed class AudioManager { public float MasterVolume { get; set; } = 1f; public float SfxVolume { get; set; } = 0.8f; public float MusicVolume { get; set; } = 0.7f; private readonly Random _random = new(12345); // Pools private VariantSoundPool _playerGunPool = default!; private VariantSoundPool _enemyGunPool = default!; private VariantSoundPool _explosionPool = default!; private VariantSoundPool _impactPool = default!; private Dictionary _pickupPool = []; // Rate limiters (global caps per category/event) private RateLimiter _playerGunLimiter = new(0.06f); // max ~16 plays/sec private RateLimiter _enemyGunLimiter = new(0.12f); // max ~8 plays/sec (global for all enemies) private RateLimiter _impactLimiter = new(0.04f); // Music private Song? _currentSong; private float _musicDuck = 1f; // 1.0 = normal, <1 = ducked private float _musicDuckTarget = 1f; private float _musicDuckSpeed = 6f; // how fast it moves toward target (bigger = snappier) public void LoadContent(ContentManager content) { // Load SoundEffects once LoadPlayerGunPool(content); LoadEnemyGunPool(content); LoadImpactPool(content); LoadExplosionPool(content); LoadShieldPickupPool(content); LoadAmmoPickupPool(content); LoadRocketsPickupPool(content); } private void LoadPlayerGunPool(ContentManager content) { var playerGunVariants = new[] { content.Load("Sfx/GUNAuto_Assault Rifle A Fire_01_SFRMS_SCIWPNS"), content.Load("Sfx/GUNAuto_Assault Rifle A Fire_02_SFRMS_SCIWPNS"), content.Load("Sfx/GUNAuto_Assault Rifle A Fire_03_SFRMS_SCIWPNS"), content.Load("Sfx/GUNAuto_Assault Rifle A Fire_04_SFRMS_SCIWPNS"), content.Load("Sfx/GUNAuto_Assault Rifle A Fire_05_SFRMS_SCIWPNS"), content.Load("Sfx/GUNAuto_Assault Rifle A Fire_06_SFRMS_SCIWPNS") }; _playerGunPool = new VariantSoundPool(playerGunVariants, voicesPerVariant: 1, rng: _random); } private void LoadEnemyGunPool(ContentManager content) { var variants = new[] { content.Load("Sfx/Pistol/GUNPis_Pistol Fire_01_SFRMS_SCIWPNS"), content.Load("Sfx/Pistol/GUNPis_Pistol Fire_02_SFRMS_SCIWPNS"), content.Load("Sfx/Pistol/GUNPis_Pistol Fire_03_SFRMS_SCIWPNS"), content.Load("Sfx/Pistol/GUNPis_Pistol Fire_04_SFRMS_SCIWPNS"), content.Load("Sfx/Pistol/GUNPis_Pistol Fire_05_SFRMS_SCIWPNS"), content.Load("Sfx/Pistol/GUNPis_Pistol Fire_06_SFRMS_SCIWPNS"), }; _enemyGunPool = new VariantSoundPool(variants, voicesPerVariant: 1, rng: _random); } private void LoadImpactPool(ContentManager content) { var variants = new[] { content.Load("Sfx/Impact/BLLTImpt_Impact Object_01_SFRMS_SCIWPNS"), content.Load("Sfx/Impact/BLLTImpt_Impact Object_02_SFRMS_SCIWPNS"), content.Load("Sfx/Impact/BLLTImpt_Impact Object_03_SFRMS_SCIWPNS"), content.Load("Sfx/Impact/BLLTImpt_Impact Object_04_SFRMS_SCIWPNS"), content.Load("Sfx/Impact/BLLTImpt_Impact Object_05_SFRMS_SCIWPNS"), content.Load("Sfx/Impact/BLLTImpt_Impact Object_06_SFRMS_SCIWPNS"), }; _impactPool = new VariantSoundPool(variants, voicesPerVariant: 1, rng: _random); } private void LoadExplosionPool(ContentManager content) { var variants = new[] { content.Load("Sfx/Explosions/EXPLDsgn_Explosion Impact_01_SFRMS_SCIWPNS"), content.Load("Sfx/Explosions/EXPLDsgn_Explosion Impact_02_SFRMS_SCIWPNS"), content.Load("Sfx/Explosions/EXPLDsgn_Explosion Impact_03_SFRMS_SCIWPNS"), content.Load("Sfx/Explosions/EXPLDsgn_Explosion Impact_04_SFRMS_SCIWPNS"), content.Load("Sfx/Explosions/EXPLDsgn_Explosion Impact_05_SFRMS_SCIWPNS"), content.Load("Sfx/Explosions/EXPLDsgn_Explosion Impact_06_SFRMS_SCIWPNS") }; _explosionPool = new VariantSoundPool(variants, voicesPerVariant: 1, rng: _random); } private void LoadShieldPickupPool(ContentManager content) { var shieldVariants = new[] { content.Load("Sfx/Shield/SCIEnrg_Shield Activate_01_SFRMS_SCIWPNS"), content.Load("Sfx/Shield/SCIEnrg_Shield Activate_02_SFRMS_SCIWPNS"), content.Load("Sfx/Shield/SCIEnrg_Shield Activate_03_SFRMS_SCIWPNS"), content.Load("Sfx/Shield/SCIEnrg_Shield Activate_04_SFRMS_SCIWPNS"), content.Load("Sfx/Shield/SCIEnrg_Shield Activate_05_SFRMS_SCIWPNS") }; _pickupPool.Add(PickupKind.Shield, new VariantSoundPool(shieldVariants, voicesPerVariant: 1, rng: _random)); } private void LoadAmmoPickupPool(ContentManager content) { var variants = new[] { content.Load("Sfx/Reload/GUNMech_Insert Clip_01_SFRMS_SCIWPNS"), content.Load("Sfx/Reload/GUNMech_Insert Clip_02_SFRMS_SCIWPNS"), content.Load("Sfx/Reload/GUNMech_Insert Clip_03_SFRMS_SCIWPNS"), content.Load("Sfx/Reload/GUNMech_Insert Clip_04_SFRMS_SCIWPNS"), content.Load("Sfx/Reload/GUNMech_Insert Clip_05_SFRMS_SCIWPNS") }; _pickupPool.Add(PickupKind.Ammo, new VariantSoundPool(variants, voicesPerVariant: 1, rng: _random)); } private void LoadRocketsPickupPool(ContentManager content) { var variants = new[] { content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_01_SFRMS_SCIWPNS"), content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_02_SFRMS_SCIWPNS"), content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_03_SFRMS_SCIWPNS"), content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_04_SFRMS_SCIWPNS"), content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_05_SFRMS_SCIWPNS"), content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_06_SFRMS_SCIWPNS"), content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_07_SFRMS_SCIWPNS"), content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_08_SFRMS_SCIWPNS"), content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_09_SFRMS_SCIWPNS"), content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_10_SFRMS_SCIWPNS"), content.Load("Sfx/Rocket Launcher/GUNMech_Rocket Launcher Reload_11_SFRMS_SCIWPNS"), }; _pickupPool.Add(PickupKind.Rockets, new VariantSoundPool(variants, voicesPerVariant: 1, rng: _random)); } //GUNMech_Rocket Launcher Reload_01_SFRMS_SCIWPNS // Call this once per frame public void Update(GameTime gameTime) { float dt = (float)gameTime.ElapsedGameTime.TotalSeconds; _playerGunLimiter.Update(dt); _enemyGunLimiter.Update(dt); _impactLimiter.Update(dt); // Smoothly approach the duck target _musicDuck = Approach(_musicDuck, _musicDuckTarget, _musicDuckSpeed * dt); // Apply music volume MediaPlayer.Volume = Clamp01(MasterVolume * MusicVolume * _musicDuck); } // ----------------------- // Public SFX entry points // ----------------------- public void PlayPlayerFire() { if (!_playerGunLimiter.TryConsume()) return; // Bullet sounds should be short + quiet-ish float baseVol = 0.35f; float vol = baseVol * RandRange(0.92f, 1.05f); float pitch = RandRange(-0.05f, 0.05f); _playerGunPool.Play( volume: Clamp01(MasterVolume * SfxVolume * vol), pitch: pitch ); } public void PlayEnemyFire() { if (!_enemyGunLimiter.TryConsume()) return; float baseVol = 0.28f; float vol = baseVol * RandRange(0.90f, 1.05f); float pitch = RandRange(-0.07f, 0.07f); _enemyGunPool.Play( volume: Clamp01(MasterVolume * SfxVolume * vol), pitch: pitch ); } public void PlayImpact() { if (!_impactLimiter.TryConsume()) return; float baseVol = 0.25f; float vol = baseVol * RandRange(0.90f, 1.10f); float pitch = RandRange(-0.08f, 0.08f); _impactPool.Play( volume: Clamp01(MasterVolume * SfxVolume * vol), pitch: pitch ); } public void PlayExplosion(bool duckMusic = true) { float baseVol = 0.85f; float vol = baseVol * RandRange(0.95f, 1.05f); float pitch = RandRange(-0.04f, 0.04f); _explosionPool.Play( volume: Clamp01(MasterVolume * SfxVolume * vol), pitch: pitch ); if (duckMusic) { // Duck music quickly, then let it recover DuckMusic(amount: 0.55f, holdSeconds: 0.20f, releaseSeconds: 0.35f); } } public void PlayPickup(PickupKind kind) { if (_pickupPool.TryGetValue(kind, out VariantSoundPool pool) == false) return; float baseVol = 0.85f; float vol = baseVol * RandRange(0.95f, 1.05f); float pitch = RandRange(-0.04f, 0.04f); pool.Play( volume: Clamp01(MasterVolume * SfxVolume * vol), pitch: pitch ); } // ----------------------- // Music // ----------------------- public void PlayMusic(ContentManager content, string songAssetName, bool loop = true) { // Example asset name: "Audio/Music/level1" _currentSong = content.Load(songAssetName); MediaPlayer.IsRepeating = loop; MediaPlayer.Volume = Clamp01(MasterVolume * MusicVolume * _musicDuck); MediaPlayer.Play(_currentSong); } public void StopMusic() => MediaPlayer.Stop(); public void PauseMusic() => MediaPlayer.Pause(); public void ResumeMusic() => MediaPlayer.Resume(); // Duck helper: amount < 1.0 means quieter music (e.g., 0.55) public void DuckMusic(float amount, float holdSeconds, float releaseSeconds) { amount = Math.Clamp(amount, 0.05f, 1f); _musicDuckTarget = amount; // Use a tiny internal timer without introducing a scheduler: // We'll set a one-shot "return to 1" using a lightweight countdown. _duckReturnTimer = holdSeconds; _duckReleaseSeconds = Math.Max(0.01f, releaseSeconds); } private float _duckReturnTimer = 0f; private float _duckReleaseSeconds = 0.35f; // Call from Update to manage duck release private void HandleDuckRelease(float dt) { if (_musicDuckTarget < 1f) { _duckReturnTimer -= dt; if (_duckReturnTimer <= 0f) { // start releasing _musicDuckTarget = 1f; _musicDuckSpeed = 1f / _duckReleaseSeconds; // approx // after it reaches 1, you can restore default speed if you want } } else { // restore default “snappiness” once fully normal if (Math.Abs(_musicDuck - 1f) < 0.001f) _musicDuckSpeed = 6f; } } // Update override to include duck release handling public void UpdateWithDuck(GameTime gameTime) { float dt = (float)gameTime.ElapsedGameTime.TotalSeconds; _playerGunLimiter.Update(dt); _enemyGunLimiter.Update(dt); _impactLimiter.Update(dt); HandleDuckRelease(dt); _musicDuck = Approach(_musicDuck, _musicDuckTarget, _musicDuckSpeed * dt); MediaPlayer.Volume = Clamp01(MasterVolume * MusicVolume * _musicDuck); } // ----------------------- // Utilities // ----------------------- private float RandRange(float min, float max) => (float)(min + _random.NextDouble() * (max - min)); private static float Clamp01(float v) => Math.Clamp(v, 0f, 1f); private static float Approach(float current, float target, float maxDelta) { if (current < target) return Math.Min(current + maxDelta, target); if (current > target) return Math.Max(current - maxDelta, target); return current; } }