using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using FlaxEngine; using Console = Game.Console; using Object = FlaxEngine.Object; namespace Game; [Flags] public enum AudioFlags { None = 0, /// Avoid replacing the existing playing audio source in this channel. ContinuePlayingExistingSource = 1 } public static class AudioManager { private static readonly Random random = new Random(); private static readonly Dictionary cachedAudioInfos = new Dictionary(); private static readonly Dictionary actorAudioChannels = new Dictionary(); public static void PlaySound(string soundName, Actor actor, Float3 position, float volume = 1f) { PlaySoundInternal(soundName, actor, 0, AudioFlags.None, position, volume, Float2.One, Float2.Zero); } public static void PlaySound(string soundName, Actor actor, Float3 position, float volume, Float2 pitchRange) { PlaySoundInternal(soundName, actor, 0, AudioFlags.None, position, volume, pitchRange, Float2.Zero); } public static void PlaySound(string soundName, Actor actor, int channel, Float3 position, float volume = 1f) { PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, Float2.One, Float2.Zero); } public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position, float volume = 1f) { PlaySoundInternal(soundName, actor, channel, flags, position, volume, Float2.One, Float2.Zero); } public static void PlaySound(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position, float volume, Float2 pitchRange) { PlaySoundInternal(soundName, actor, channel, flags, position, volume, pitchRange, Float2.Zero); } public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel, Float3 position, float volume = 1f) { PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, Float2.One, delayRange); } public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel, Float3 position, float volume, Float2 pitchRange) { PlaySoundInternal(soundName, actor, channel, AudioFlags.None, position, volume, pitchRange, delayRange); } public static void PlaySoundDelayed(Float2 delayRange, string soundName, Actor actor, int channel, AudioFlags flags, Float3 position, float volume, Float2 pitchRange) { PlaySoundInternal(soundName, actor, channel, flags, position, volume, pitchRange, delayRange); } private static void PlaySoundInternal(string soundName, Actor actor, int channel, AudioFlags flags, Float3 position, float volume, Float2 pitchRange, Float2 delayRange) { AudioInfo audio = GetSound(soundName); if (audio.AudioClips.Length == 0) return; channel = Math.Max(channel, 0); ActorAudioChannels actorChannels = null; if (channel > 0) { if (!actorAudioChannels.TryGetValue(actor, out actorChannels)) { actorChannels = new ActorAudioChannels(); actorAudioChannels.Add(actor, actorChannels); } if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource)) actorChannels.channelSources.Add(channel, null); if (existingAudioSource && existingAudioSource != null && existingAudioSource.State == AudioSource.States.Playing) { if (flags.HasFlag(AudioFlags.ContinuePlayingExistingSource)) return; existingAudioSource.Stop(); Object.Destroy(existingAudioSource); actorChannels.channelSources[channel] = null; } } AudioClip audioClip; if (audio.AudioClips.Length > 1) { // Randomize selected clip while avoiding the last used clip int randomIndex = 0; for (int i = 0; i < 10; i++) { randomIndex = random.Next(audio.AudioClips.Length); if (randomIndex != audio.lastAudioPlayed) break; } audioClip = audio.AudioClips[randomIndex]; audio.lastAudioPlayed = randomIndex; } else { audioClip = audio.AudioClips[0]; } // Randomized pitch float pitch; if (pitchRange[0] < pitchRange[1]) pitch = (float)(pitchRange[0] + random.NextDouble() * (pitchRange[1] - pitchRange[0])); else pitch = pitchRange[0]; // Randomized start delay float randomDelay; if (delayRange[0] < delayRange[1]) randomDelay = (float)(delayRange[0] + random.NextDouble() * (delayRange[1] - delayRange[0])); else randomDelay = delayRange[0]; AudioSourceDelayed audioSource = new AudioSourceDelayed(); audioSource.Delay = randomDelay; audioSource.Clip = audioClip; audioSource.Position = position; audioSource.Parent = actor.Parent; audioSource.Pitch = pitch; audioSource.Name = Path.GetFileNameWithoutExtension(audioClip.Path); audioSource.Volume = volume; audioSource.PlayOnStart = randomDelay == 0; if (volume != 1f) audioSource.Name += ", vol: " + volume; if (pitch != 1f) audioSource.Name += ", pitch: " + pitch; if (pitch != 0f) audioSource.Name += ", delay: " + randomDelay; //Console.Print("playing sound " + audioSource.Name); if (channel > 0) actorChannels.channelSources[channel] = audioSource; } public static void StopSound(Actor actor, int channel) { if (channel <= 0) return; if (!actorAudioChannels.TryGetValue(actor, out ActorAudioChannels actorChannels)) return; if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource)) return; if (existingAudioSource && existingAudioSource != null && existingAudioSource.State == AudioSource.States.Playing) { existingAudioSource.Stop(); Object.Destroy(existingAudioSource); actorChannels.channelSources[channel] = null; } } public static bool IsSoundPlaying(Actor actor, int channel) { if (channel <= 0) return false; if (!actorAudioChannels.TryGetValue(actor, out ActorAudioChannels actorChannels)) return false; if (!actorChannels.channelSources.TryGetValue(channel, out AudioSource existingAudioSource)) return false; if (existingAudioSource && existingAudioSource != null && existingAudioSource.State == AudioSource.States.Playing) return true; return false; } private static AudioInfo GetSound(string soundName) { if (cachedAudioInfos.TryGetValue(soundName, out AudioInfo cachedAudio)) return cachedAudio; AudioInfo audio = new AudioInfo(); string audioBasePath = Path.Combine(AssetManager.ContentPath, "Audio"); AudioClip audioClip = Content.Load(Path.Combine(audioBasePath, soundName + ".flax")); if (audioClip != null) { audio.AudioClips = new[] { audioClip }; } else { // Check if this audio has multiple variations var audioClips = new List(); for (int i = 1; i < 50; i++) { // TODO: make this more efficient, maybe get a list of assets and filter by name? AudioClip audioClipVariation = Content.Load(Path.Combine(audioBasePath, soundName + "_var" + i + ".flax")); if (audioClipVariation == null) break; audioClips.Add(audioClipVariation); } if (audioClips.Count > 0) audio.AudioClips = audioClips.ToArray(); else Console.PrintError("AudioClip '" + soundName + "' not found"); } audio.lastAudioPlayed = audio.AudioClips.Length + 1; cachedAudioInfos.Add(soundName, audio); return audio; } private class AudioInfo { public AudioClip[] AudioClips; public int lastAudioPlayed; } private class ActorAudioChannels { public readonly Dictionary channelSources; public ActorAudioChannels() { channelSources = new Dictionary(); } } }