Added more sound effeects. Added audio manager.
This commit is contained in:
284
AlientAttack.MonoGame/Audio/AudioManager.cs
Normal file
284
AlientAttack.MonoGame/Audio/AudioManager.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Audio;
|
||||
using Microsoft.Xna.Framework.Content;
|
||||
using Microsoft.Xna.Framework.Media;
|
||||
using System;
|
||||
|
||||
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!;
|
||||
|
||||
// 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);
|
||||
|
||||
//var playerGun = content.Load<SoundEffect>("Audio/Sfx/player_fire");
|
||||
//var enemyGun = content.Load<SoundEffect>("Audio/Sfx/enemy_fire");
|
||||
//var explosion = content.Load<SoundEffect>("Audio/Sfx/explosion");
|
||||
//var impact = content.Load<SoundEffect>("Audio/Sfx/impact");
|
||||
|
||||
// Create pools with sane voice caps
|
||||
//_playerGunPool = new SoundPool(playerGun, maxVoices: 2);
|
||||
//_enemyGunPool = new SoundPool(enemyGun, maxVoices: 3);
|
||||
//_explosionPool = new SoundPool(explosion, maxVoices: 6);
|
||||
//_impactPool = new SoundPool(impact, maxVoices: 4);
|
||||
}
|
||||
|
||||
private void LoadPlayerGunPool(ContentManager content)
|
||||
{
|
||||
var playerGunVariants = new[]
|
||||
{
|
||||
content.Load<SoundEffect>("Sfx/GUNAuto_Assault Rifle A Fire_01_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/GUNAuto_Assault Rifle A Fire_02_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/GUNAuto_Assault Rifle A Fire_03_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/GUNAuto_Assault Rifle A Fire_04_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/GUNAuto_Assault Rifle A Fire_05_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("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<SoundEffect>("Sfx/Pistol/GUNPis_Pistol Fire_01_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Pistol/GUNPis_Pistol Fire_02_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Pistol/GUNPis_Pistol Fire_03_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Pistol/GUNPis_Pistol Fire_04_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Pistol/GUNPis_Pistol Fire_05_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("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<SoundEffect>("Sfx/Impact/BLLTImpt_Impact Object_01_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Impact/BLLTImpt_Impact Object_02_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Impact/BLLTImpt_Impact Object_03_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Impact/BLLTImpt_Impact Object_04_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Impact/BLLTImpt_Impact Object_05_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("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<SoundEffect>("Sfx/Explosions/EXPLDsgn_Explosion Impact_01_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Explosions/EXPLDsgn_Explosion Impact_02_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Explosions/EXPLDsgn_Explosion Impact_03_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Explosions/EXPLDsgn_Explosion Impact_04_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Explosions/EXPLDsgn_Explosion Impact_05_SFRMS_SCIWPNS"),
|
||||
content.Load<SoundEffect>("Sfx/Explosions/EXPLDsgn_Explosion Impact_06_SFRMS_SCIWPNS")
|
||||
};
|
||||
|
||||
_explosionPool = new VariantSoundPool(variants, voicesPerVariant: 1, rng: _random);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Music
|
||||
// -----------------------
|
||||
|
||||
public void PlayMusic(ContentManager content, string songAssetName, bool loop = true)
|
||||
{
|
||||
// Example asset name: "Audio/Music/level1"
|
||||
_currentSong = content.Load<Song>(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;
|
||||
}
|
||||
}
|
||||
25
AlientAttack.MonoGame/Audio/RateLimiter.cs
Normal file
25
AlientAttack.MonoGame/Audio/RateLimiter.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace AlienAttack.MonoGame.Audio;
|
||||
|
||||
public sealed class RateLimiter(float cooldownSeconds)
|
||||
{
|
||||
private readonly float _cooldownSeconds = cooldownSeconds <= 0 ? 0.0001f : cooldownSeconds;
|
||||
private float _timer = 0f;
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
_timer -= deltaTime;
|
||||
|
||||
if (_timer < 0f)
|
||||
_timer = 0f;
|
||||
}
|
||||
|
||||
public bool TryConsume()
|
||||
{
|
||||
if (_timer > 0f)
|
||||
return false;
|
||||
|
||||
_timer = _cooldownSeconds;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
37
AlientAttack.MonoGame/Audio/SoundPool.cs
Normal file
37
AlientAttack.MonoGame/Audio/SoundPool.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Microsoft.Xna.Framework.Audio;
|
||||
using System;
|
||||
|
||||
namespace AlienAttack.MonoGame.Audio;
|
||||
|
||||
public sealed class SoundPool
|
||||
{
|
||||
private readonly SoundEffectInstance[] _voices;
|
||||
private int _cursor;
|
||||
|
||||
public SoundPool(SoundEffect effect, int maxVoices)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maxVoices, 1);
|
||||
|
||||
_voices = new SoundEffectInstance[maxVoices];
|
||||
|
||||
for (int i = 0; i < maxVoices; i++)
|
||||
_voices[i] = effect.CreateInstance();
|
||||
}
|
||||
|
||||
public void Play(float volume = 1f, float pitch = 0f, float pan = 0f)
|
||||
{
|
||||
SoundEffectInstance instance = _voices[_cursor];
|
||||
|
||||
_cursor = (_cursor + 1) % _voices.Length;
|
||||
|
||||
// "Voice stealing": stop if currently playing, then reuse immediately
|
||||
if (instance.State == SoundState.Playing)
|
||||
instance.Stop();
|
||||
|
||||
instance.Volume = Math.Clamp(volume, 0f, 1f);
|
||||
instance.Pitch = Math.Clamp(pitch, -1f, 1f);
|
||||
instance.Pan = Math.Clamp(pan, -1f, 1f);
|
||||
|
||||
instance.Play();
|
||||
}
|
||||
}
|
||||
48
AlientAttack.MonoGame/Audio/VariantSoundPool.cs
Normal file
48
AlientAttack.MonoGame/Audio/VariantSoundPool.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Microsoft.Xna.Framework.Audio;
|
||||
using System;
|
||||
|
||||
namespace AlienAttack.MonoGame.Audio;
|
||||
|
||||
public sealed class VariantSoundPool
|
||||
{
|
||||
private readonly SoundPool[] _variantPools;
|
||||
private readonly Random _rng;
|
||||
private int _lastIndex = -1;
|
||||
|
||||
public VariantSoundPool(SoundEffect[] variants, int voicesPerVariant, Random rng)
|
||||
{
|
||||
if (variants == null || variants.Length == 0)
|
||||
throw new ArgumentException("At least one variant is required.", nameof(variants));
|
||||
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(voicesPerVariant, 1);
|
||||
|
||||
_rng = rng ?? throw new ArgumentNullException(nameof(rng));
|
||||
|
||||
_variantPools = new SoundPool[variants.Length];
|
||||
|
||||
for (int i = 0; i < variants.Length; i++)
|
||||
_variantPools[i] = new SoundPool(variants[i], voicesPerVariant);
|
||||
}
|
||||
|
||||
public void Play(float volume = 1f, float pitch = 0f, float pan = 0f, bool avoidImmediateRepeat = true)
|
||||
{
|
||||
int idx = PickVariantIndex(avoidImmediateRepeat);
|
||||
_variantPools[idx].Play(volume, pitch, pan);
|
||||
}
|
||||
|
||||
private int PickVariantIndex(bool avoidImmediateRepeat)
|
||||
{
|
||||
if (_variantPools.Length == 1)
|
||||
return 0;
|
||||
|
||||
int idx = _rng.Next(_variantPools.Length);
|
||||
if (!avoidImmediateRepeat) { _lastIndex = idx; return idx; }
|
||||
|
||||
// Avoid playing the exact same sample twice in a row (nice polish)
|
||||
if (idx == _lastIndex)
|
||||
idx = (idx + 1 + _rng.Next(_variantPools.Length - 1)) % _variantPools.Length;
|
||||
|
||||
_lastIndex = idx;
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user