using Barotrauma.Items.Components; using Barotrauma.Sounds; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Xml.Linq; namespace Barotrauma { public struct DamageSound { //the range of inflicted damage where the sound can be played //(10.0f, 30.0f) would be played when the inflicted damage is between 10 and 30 public readonly Vector2 damageRange; public readonly string damageType; public readonly Sound sound; public readonly string requiredTag; public DamageSound(Sound sound, Vector2 damageRange, string damageType, string requiredTag = "") { this.sound = sound; this.damageRange = damageRange; this.damageType = damageType; this.requiredTag = requiredTag; } } public class BackgroundMusic { public readonly string file; public readonly string type; public readonly Vector2 priorityRange; public BackgroundMusic(string file, string type, Vector2 priorityRange) { this.file = file; this.type = type; this.priorityRange = priorityRange; } } static class SoundPlayer { private static ILookup miscSounds; //music public static float MusicVolume = 1.0f; private const float MusicLerpSpeed = 1.0f; private const float UpdateMusicInterval = 5.0f; private static BackgroundMusic currentMusic; private static BackgroundMusic targetMusic; private static List musicClips; private static float currMusicVolume; private static float updateMusicTimer; //ambience private static List waterAmbiences = new List(); private static int[] waterAmbienceIndexes = new int[2]; private static float ambientSoundTimer; private static Vector2 ambientSoundInterval = new Vector2(20.0f, 40.0f); //x = min, y = max //misc public static List FlowSounds = new List(); public static List SplashSounds = new List(); private static List damageSounds; private static Sound startUpSound; public static bool Initialized; public static string OverrideMusicType { get; set; } public static float? OverrideMusicDuration; public static int SoundCount; public static IEnumerable Init() { OverrideMusicType = null; List soundFiles = GameMain.Config.SelectedContentPackage.GetFilesOfType(ContentType.Sounds); List soundElements = new List(); foreach (string soundFile in soundFiles) { XDocument doc = XMLExtensions.TryLoadXml(soundFile); if (doc != null && doc.Root != null) { soundElements.AddRange(doc.Root.Elements()); } } SoundCount = 1 + soundElements.Count(); var startUpSoundElement = soundElements.Find(e => e.Name.ToString().ToLowerInvariant() == "startupsound"); if (startUpSoundElement != null) { startUpSound = Sound.Load(startUpSoundElement, false); startUpSound.Play(); } yield return CoroutineStatus.Running; List> miscSoundList = new List>(); damageSounds = new List(); musicClips = new List(); foreach (XElement soundElement in soundElements) { yield return CoroutineStatus.Running; switch (soundElement.Name.ToString().ToLowerInvariant()) { case "music": string file = soundElement.GetAttributeString("file", ""); string type = soundElement.GetAttributeString("type", "").ToLowerInvariant(); Vector2 priority = soundElement.GetAttributeVector2("priorityrange", new Vector2(0.0f, 100.0f)); musicClips.Add(new BackgroundMusic(file, type, priority)); break; case "splash": SplashSounds.Add(Sound.Load(soundElement, false)); break; case "flow": FlowSounds.Add(Sound.Load(soundElement, false)); break; case "waterambience": waterAmbiences.Add(Sound.Load(soundElement, false)); break; case "damagesound": Sound damageSound = Sound.Load(soundElement.GetAttributeString("file", ""), false); if (damageSound == null) continue; string damageSoundType = soundElement.GetAttributeString("damagesoundtype", "None"); damageSounds.Add(new DamageSound( damageSound, soundElement.GetAttributeVector2("damagerange", new Vector2(0.0f, 100.0f)), damageSoundType, soundElement.GetAttributeString("requiredtag", ""))); break; default: Sound sound = Sound.Load(soundElement.GetAttributeString("file", ""), false); if (sound != null) { miscSoundList.Add(new KeyValuePair(soundElement.Name.ToString().ToLowerInvariant(), sound)); } break; } } miscSounds = miscSoundList.ToLookup(kvp => kvp.Key, kvp => kvp.Value); Initialized = true; yield return CoroutineStatus.Success; } public static void Update(float deltaTime) { UpdateMusic(deltaTime); if (startUpSound != null && !startUpSound.IsPlaying) { startUpSound.Remove(); startUpSound = null; } //stop submarine ambient sounds if no sub is loaded if (Submarine.MainSub == null) { for (int i = 0; i < waterAmbienceIndexes.Length; i++) { if (waterAmbienceIndexes[i] <= 0) continue; SoundManager.Stop(waterAmbienceIndexes[i]); waterAmbienceIndexes[i] = 0; } return; } float ambienceVolume = 0.8f; float lowpassHFGain = 1.0f; if (Character.Controlled != null) { AnimController animController = Character.Controlled.AnimController; if (animController.HeadInWater) { ambienceVolume = 1.0f; ambienceVolume += animController.Limbs[0].LinearVelocity.Length(); lowpassHFGain = 0.2f; } lowpassHFGain *= Character.Controlled.LowPassMultiplier; } //how fast the sub is moving, scaled to 0.0 -> 1.0 float movementSoundVolume = 0.0f; foreach (Submarine sub in Submarine.Loaded) { float movementFactor = (sub.Velocity == Vector2.Zero) ? 0.0f : sub.Velocity.Length() / 10.0f; movementFactor = MathHelper.Clamp(movementFactor, 0.0f, 1.0f); if (Character.Controlled==null || Character.Controlled.Submarine != sub) { float dist = Vector2.Distance(GameMain.GameScreen.Cam.WorldViewCenter, sub.WorldPosition); movementFactor = movementFactor / Math.Max(dist / 1000.0f, 1.0f); } movementSoundVolume = Math.Max(movementSoundVolume, movementFactor); } if (ambientSoundTimer > 0.0f) { ambientSoundTimer -= (float)Timing.Step; } else { PlaySound( "ambient", Rand.Range(0.5f, 1.0f), 1000.0f, new Vector2(Sound.CameraPos.X, Sound.CameraPos.Y) + Rand.Vector(100.0f)); ambientSoundTimer = Rand.Range(ambientSoundInterval.X, ambientSoundInterval.Y); } SoundManager.LowPassHFGain = lowpassHFGain; if (waterAmbiences.Count > 1) { waterAmbienceIndexes[0] = waterAmbiences[0].Loop(waterAmbienceIndexes[0], ambienceVolume * (1.0f - movementSoundVolume)); waterAmbienceIndexes[1] = waterAmbiences[1].Loop(waterAmbienceIndexes[1], ambienceVolume * movementSoundVolume); } } public static Sound GetSound(string soundTag) { var matchingSounds = miscSounds[soundTag].ToList(); if (matchingSounds.Count == 0) return null; return matchingSounds[Rand.Int(matchingSounds.Count)]; } public static void PlaySound(string soundTag, float volume = 1.0f) { var sound = GetSound(soundTag); if (sound != null) sound.Play(volume); } public static void PlaySound(string soundTag, float volume, float range, Vector2 position) { var sound = GetSound(soundTag); if (sound != null) sound.Play(volume, range, position); } private static void UpdateMusic(float deltaTime) { if (musicClips == null) return; if (OverrideMusicType != null && OverrideMusicDuration.HasValue) { OverrideMusicDuration -= deltaTime; if (OverrideMusicDuration <= 0.0f) { OverrideMusicType = null; OverrideMusicDuration = null; } } updateMusicTimer -= deltaTime; if (updateMusicTimer <= 0.0f) { List suitableMusic = GetSuitableMusicClips(); if (suitableMusic.Count == 0) { targetMusic = null; } else if (!suitableMusic.Contains(currentMusic)) { int index = Rand.Int(suitableMusic.Count); if (currentMusic == null || suitableMusic[index].file != currentMusic.file) { targetMusic = suitableMusic[index]; } } updateMusicTimer = UpdateMusicInterval; } if (targetMusic == null || currentMusic == null || targetMusic.file != currentMusic.file) { currMusicVolume = MathHelper.Lerp(currMusicVolume, 0.0f, MusicLerpSpeed * deltaTime); if (currentMusic != null) Sound.StreamVolume(currMusicVolume); if (currMusicVolume < 0.01f) { Sound.StopStream(); try { if (targetMusic != null) Sound.StartStream(targetMusic.file, currMusicVolume); } catch (FileNotFoundException e) { DebugConsole.ThrowError("Music clip " + targetMusic.file + " not found!", e); } currentMusic = targetMusic; } } else { currMusicVolume = MathHelper.Lerp(currMusicVolume, MusicVolume, MusicLerpSpeed * deltaTime); Sound.StreamVolume(currMusicVolume); } } public static void SwitchMusic() { var suitableMusic = GetSuitableMusicClips(); if (suitableMusic.Count > 1) { targetMusic = suitableMusic.Find(m => m != currentMusic); } } private static List GetSuitableMusicClips() { string musicType = GetCurrentMusicType(); return musicClips.Where(music => music != null && music.type == musicType).ToList(); } private static string GetCurrentMusicType() { if (OverrideMusicType != null) return OverrideMusicType; if (Character.Controlled != null && Level.Loaded != null && Level.Loaded.Ruins != null && Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition))) { return "ruins"; } Submarine targetSubmarine = Character.Controlled?.Submarine; if ((targetSubmarine != null && targetSubmarine.AtDamageDepth) || (Screen.Selected == GameMain.GameScreen && GameMain.GameScreen.Cam.Position.Y < SubmarineBody.DamageDepth)) { return "deep"; } if (targetSubmarine != null) { List reactors = new List(); foreach (Item item in Item.ItemList) { if (item.Submarine != targetSubmarine) continue; var reactor = item.GetComponent(); if (reactor != null) { reactors.Add(reactor); } } if (reactors.All(r => r.Temperature < 1.0f)) return "repair"; float floodedArea = 0.0f; float totalArea = 0.0f; foreach (Hull hull in Hull.hullList) { if (hull.Submarine != targetSubmarine) continue; floodedArea += hull.WaterVolume; totalArea += hull.Volume; } if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) return "repair"; } float enemyDistThreshold = 5000.0f; if (targetSubmarine != null) { enemyDistThreshold = Math.Max(enemyDistThreshold, Math.Max(targetSubmarine.Borders.Width, targetSubmarine.Borders.Height) * 2.0f); } foreach (Character character in Character.CharacterList) { EnemyAIController enemyAI = character.AIController as EnemyAIController; if (enemyAI == null || (enemyAI.AttackHumans < 0.0f && enemyAI.AttackRooms < 0.0f)) continue; if (targetSubmarine != null) { if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold) { return "monster"; } } else if (Character.Controlled != null) { if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold) { return "monster"; } } } return "default"; } public static void PlaySplashSound(Vector2 worldPosition, float strength) { int splashIndex = MathHelper.Clamp((int)(strength + Rand.Range(-2, 2)), 0, SplashSounds.Count - 1); SplashSounds[splashIndex].Play(1.0f, 800.0f, worldPosition); } public static void PlayDamageSound(string damageType, float damage, PhysicsBody body) { Vector2 bodyPosition = body.DrawPosition; PlayDamageSound(damageType, damage, bodyPosition, 800.0f); } public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, List tags = null) { damage = MathHelper.Clamp(damage+Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); var sounds = damageSounds.FindAll(s => s.damageRange == null || (damage >= s.damageRange.X && damage <= s.damageRange.Y) && s.damageType == damageType && (tags == null ? string.IsNullOrEmpty(s.requiredTag) : tags.Contains(s.requiredTag))); if (!sounds.Any()) return; int selectedSound = Rand.Int(sounds.Count); sounds[selectedSound].sound.Play(1.0f, range, position); Debug.WriteLine("playing: " + sounds[selectedSound].sound); } } }