using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Sounds; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Threading; namespace Barotrauma { static class SoundPlayer { //music private const float MusicLerpSpeed = 1.0f; private const float UpdateMusicInterval = 5.0f; public const float MuffleFilterFrequency = 600; const int MaxMusicChannels = 6; private readonly static BackgroundMusic[] currentMusic = new BackgroundMusic[MaxMusicChannels]; private readonly static SoundChannel[] musicChannel = new SoundChannel[MaxMusicChannels]; private readonly static BackgroundMusic[] targetMusic = new BackgroundMusic[MaxMusicChannels]; private static IEnumerable musicClips => BackgroundMusic.BackgroundMusicPrefabs; private static BackgroundMusic previousDefaultMusic; private static float updateMusicTimer; //ambience private static SoundPrefab waterAmbienceIn => SoundPrefab.WaterAmbienceIn.ActivePrefab; private static SoundPrefab waterAmbienceOut => SoundPrefab.WaterAmbienceOut.ActivePrefab; private static SoundPrefab waterAmbienceMoving => SoundPrefab.WaterAmbienceMoving.ActivePrefab; private static readonly HashSet waterAmbienceChannels = new HashSet(); private static float ambientSoundTimer; private static Vector2 ambientSoundInterval = new Vector2(20.0f, 40.0f); //x = min, y = max private static SoundChannel hullSoundChannel; private static Hull hullSoundSource; private static float hullSoundTimer; private static Vector2 hullSoundInterval = new Vector2(45.0f, 90.0f); //x = min, y = max //misc private static float[] targetFlowLeft, targetFlowRight; public static IReadOnlyList FlowSounds => SoundPrefab.FlowSounds; public static IReadOnlyList SplashSounds => SoundPrefab.SplashSounds; private static SoundChannel[] flowSoundChannels; private static float[] flowVolumeLeft; private static float[] flowVolumeRight; const float FlowSoundRange = 1500.0f; const float MaxFlowStrength = 400.0f; //the heaviest water sound effect is played when the water flow is this strong private static SoundChannel[] fireSoundChannels; private static float[] fireVolumeLeft; private static float[] fireVolumeRight; const float FireSoundRange = 1000.0f; const float FireSoundMediumLimit = 100.0f; const float FireSoundLargeLimit = 200.0f; //switch to large fire sound when the size of a firesource is above this const int fireSizes = 3; private static string[] fireSoundTags = new string[fireSizes] { "fire", "firemedium", "firelarge" }; // TODO: could use a dictionary to split up the list into smaller lists of same type? private static IEnumerable damageSounds => DamageSound.DamageSoundPrefabs; private static bool firstTimeInMainMenu = true; private static Sound startUpSound => SoundPrefab.StartupSound.ActivePrefab.Sound; public static Identifier OverrideMusicType { get; set; } public static float? OverrideMusicDuration; public static void Update(float deltaTime) { UpdateMusic(deltaTime); if (flowSoundChannels == null || flowSoundChannels.Length != FlowSounds.Count) { flowSoundChannels = new SoundChannel[FlowSounds.Count]; flowVolumeLeft = new float[FlowSounds.Count]; flowVolumeRight = new float[FlowSounds.Count]; targetFlowLeft = new float[FlowSounds.Count]; targetFlowRight = new float[FlowSounds.Count]; } if (fireSoundChannels == null || fireSoundChannels.Length != fireSizes) { fireSoundChannels = new SoundChannel[fireSizes]; fireVolumeLeft = new float[fireSizes]; fireVolumeRight = new float[fireSizes]; } //stop water sounds if no sub is loaded if (Submarine.MainSub == null || Screen.Selected != GameMain.GameScreen) { foreach (var chn in waterAmbienceChannels.Concat(flowSoundChannels).Concat(fireSoundChannels)) { chn?.FadeOutAndDispose(); } fireVolumeLeft[0] = 0.0f; fireVolumeLeft[1] = 0.0f; fireVolumeRight[0] = 0.0f; fireVolumeRight[1] = 0.0f; hullSoundChannel?.FadeOutAndDispose(); hullSoundSource = null; return; } float ambienceVolume = 0.8f; if (Character.Controlled != null && !Character.Controlled.Removed) { AnimController animController = Character.Controlled.AnimController; if (animController.HeadInWater) { ambienceVolume = 1.0f; float limbSpeed = animController.Limbs[0].LinearVelocity.Length(); if (MathUtils.IsValid(limbSpeed)) { ambienceVolume += limbSpeed; } } } UpdateWaterAmbience(ambienceVolume, deltaTime); UpdateWaterFlowSounds(deltaTime); UpdateRandomAmbience(deltaTime); UpdateHullSounds(deltaTime); UpdateFireSounds(deltaTime); } private static void UpdateWaterAmbience(float ambienceVolume, float deltaTime) { if (GameMain.SoundManager.Disabled || GameMain.GameScreen?.Cam == null) { return; } //how fast the sub is moving, scaled to 0.0 -> 1.0 float movementSoundVolume = 0.0f; float insideSubFactor = 0.0f; foreach (Submarine sub in Submarine.Loaded) { if (sub == null || sub.Removed) { continue; } 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 /= Math.Max(dist / 1000.0f, 1.0f); insideSubFactor = Math.Max(1.0f / Math.Max(dist / 1000.0f, 1.0f), insideSubFactor); } else { insideSubFactor = 1.0f; } if (Character.Controlled != null && Character.Controlled.PressureTimer > 0.0f && !Character.Controlled.IsDead) { //make the sound lerp to the "outside" sound when under pressure insideSubFactor -= Character.Controlled.PressureTimer / 100.0f; } movementSoundVolume = Math.Max(movementSoundVolume, movementFactor); if (!MathUtils.IsValid(movementSoundVolume)) { string errorMsg = "Failed to update water ambience volume - submarine's movement value invalid (" + movementSoundVolume + ", sub velocity: " + sub.Velocity + ")"; DebugConsole.Log(errorMsg); GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.UpdateWaterAmbience:InvalidVolume", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); movementSoundVolume = 0.0f; } if (!MathUtils.IsValid(insideSubFactor)) { string errorMsg = "Failed to update water ambience volume - inside sub value invalid (" + insideSubFactor + ")"; DebugConsole.Log(errorMsg); GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.UpdateWaterAmbience:InvalidVolume", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); insideSubFactor = 0.0f; } } void updateWaterAmbience(Sound sound, float volume) { SoundChannel chn = waterAmbienceChannels.FirstOrDefault(c => c.Sound == sound); if (Level.Loaded != null) { volume *= Level.Loaded.GenerationParams.WaterAmbienceVolume; } if (chn is null || !chn.IsPlaying) { if (volume < 0.01f) { return; } if (chn is not null) { waterAmbienceChannels.Remove(chn); } chn = sound.Play(volume, SoundManager.SoundCategoryWaterAmbience); if (chn != null) { chn.Looping = true; waterAmbienceChannels.Add(chn); } } else { chn.Gain += deltaTime * Math.Sign(volume - chn.Gain); if (chn.Gain < 0.01f) { chn.FadeOutAndDispose(); } if (Character.Controlled != null && Character.Controlled.PressureTimer > 0.0f && !Character.Controlled.IsDead) { //make the sound decrease in pitch when under pressure chn.FrequencyMultiplier = MathHelper.Clamp(Character.Controlled.PressureTimer / 200.0f, 0.75f, 1.0f); } else { chn.FrequencyMultiplier = Math.Min(chn.frequencyMultiplier + deltaTime, 1.0f); } } } updateWaterAmbience(waterAmbienceIn.Sound, ambienceVolume * (1.0f - movementSoundVolume) * insideSubFactor * waterAmbienceIn.Volume); updateWaterAmbience(waterAmbienceMoving.Sound, ambienceVolume * movementSoundVolume * insideSubFactor * waterAmbienceMoving.Volume); updateWaterAmbience(waterAmbienceOut.Sound, (1.0f - insideSubFactor) * waterAmbienceOut.Volume); } private static void UpdateWaterFlowSounds(float deltaTime) { if (FlowSounds.Count == 0) { return; } for (int i = 0; i < targetFlowLeft.Length; i++) { targetFlowLeft[i] = 0.0f; targetFlowRight[i] = 0.0f; } Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y); foreach (Gap gap in Gap.GapList) { Vector2 diff = gap.WorldPosition - listenerPos; if (Math.Abs(diff.X) < FlowSoundRange && Math.Abs(diff.Y) < FlowSoundRange) { if (gap.Open < 0.01f || gap.LerpedFlowForce.LengthSquared() < 100.0f) { continue; } float gapFlow = Math.Abs(gap.LerpedFlowForce.X) + Math.Abs(gap.LerpedFlowForce.Y) * 2.5f; if (!gap.IsRoomToRoom) { gapFlow *= 2.0f; } if (gapFlow < 10.0f) { continue; } if (gap.linkedTo.Count == 2 && gap.linkedTo[0] is Hull hull1 && gap.linkedTo[1] is Hull hull2) { //no flow sounds between linked hulls (= rooms consisting of multiple hulls) if (hull1.linkedTo.Contains(hull2)) { continue; } if (hull1.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { continue; } if (hull2.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { continue; } } int flowSoundIndex = (int)Math.Floor(MathHelper.Clamp(gapFlow / MaxFlowStrength, 0, FlowSounds.Count)); flowSoundIndex = Math.Min(flowSoundIndex, FlowSounds.Count - 1); float dist = diff.Length(); float distFallOff = dist / FlowSoundRange; if (distFallOff >= 0.99f) { continue; } float gain = MathHelper.Clamp(gapFlow / 100.0f, 0.0f, 1.0f); //flow at the left side if (diff.X < 0) { targetFlowLeft[flowSoundIndex] += gain - distFallOff; } else { targetFlowRight[flowSoundIndex] += gain - distFallOff; } } } if (Character.Controlled?.CharacterHealth?.GetAffliction("psychosis") is AfflictionPsychosis psychosis) { if (psychosis.CurrentFloodType == AfflictionPsychosis.FloodType.Minor) { targetFlowLeft[0] = Math.Max(targetFlowLeft[0], 1.0f); targetFlowRight[0] = Math.Max(targetFlowRight[0], 1.0f); } else if (psychosis.CurrentFloodType == AfflictionPsychosis.FloodType.Major) { targetFlowLeft[FlowSounds.Count - 1] = Math.Max(targetFlowLeft[FlowSounds.Count - 1], 1.0f); targetFlowRight[FlowSounds.Count - 1] = Math.Max(targetFlowRight[FlowSounds.Count - 1], 1.0f); } } for (int i = 0; i < FlowSounds.Count; i++) { flowVolumeLeft[i] = (targetFlowLeft[i] < flowVolumeLeft[i]) ? Math.Max(targetFlowLeft[i], flowVolumeLeft[i] - deltaTime) : Math.Min(targetFlowLeft[i], flowVolumeLeft[i] + deltaTime * 10.0f); flowVolumeRight[i] = (targetFlowRight[i] < flowVolumeRight[i]) ? Math.Max(targetFlowRight[i], flowVolumeRight[i] - deltaTime) : Math.Min(targetFlowRight[i], flowVolumeRight[i] + deltaTime * 10.0f); if (flowVolumeLeft[i] < 0.05f && flowVolumeRight[i] < 0.05f) { if (flowSoundChannels[i] != null) { flowSoundChannels[i].Dispose(); flowSoundChannels[i] = null; } } else { if (FlowSounds[i]?.Sound == null) { continue; } Vector2 soundPos = new Vector2(GameMain.SoundManager.ListenerPosition.X + (flowVolumeRight[i] - flowVolumeLeft[i]) * 100, GameMain.SoundManager.ListenerPosition.Y); if (flowSoundChannels[i] == null || !flowSoundChannels[i].IsPlaying) { flowSoundChannels[i] = FlowSounds[i].Sound.Play(1.0f, FlowSoundRange, soundPos); if (flowSoundChannels[i] == null) { continue; } flowSoundChannels[i].Looping = true; } flowSoundChannels[i].Gain = Math.Max(flowVolumeRight[i], flowVolumeLeft[i]); flowSoundChannels[i].Position = new Vector3(soundPos, 0.0f); } } } private static void UpdateFireSounds(float deltaTime) { for (int i = 0; i < fireVolumeLeft.Length; i++) { fireVolumeLeft[i] = 0.0f; fireVolumeRight[i] = 0.0f; } Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y); foreach (Hull hull in Hull.HullList) { foreach (FireSource fs in hull.FireSources) { AddFireVolume(fs); } foreach (FireSource fs in hull.FakeFireSources) { AddFireVolume(fs); } } for (int i = 0; i < fireVolumeLeft.Length; i++) { if (fireVolumeLeft[i] < 0.05f && fireVolumeRight[i] < 0.05f) { if (fireSoundChannels[i] != null) { fireSoundChannels[i].FadeOutAndDispose(); fireSoundChannels[i] = null; } } else { Vector2 soundPos = new Vector2(GameMain.SoundManager.ListenerPosition.X + (fireVolumeRight[i] - fireVolumeLeft[i]) * 100, GameMain.SoundManager.ListenerPosition.Y); if (fireSoundChannels[i] == null || !fireSoundChannels[i].IsPlaying) { fireSoundChannels[i] = GetSound(fireSoundTags[i])?.Play(1.0f, FlowSoundRange, soundPos); if (fireSoundChannels[i] == null) { continue; } fireSoundChannels[i].Looping = true; } fireSoundChannels[i].Gain = Math.Max(fireVolumeRight[i], fireVolumeLeft[i]); fireSoundChannels[i].Position = new Vector3(soundPos, 0.0f); } } void AddFireVolume(FireSource fs) { Vector2 diff = fs.WorldPosition + fs.Size / 2 - listenerPos; if (Math.Abs(diff.X) < FireSoundRange && Math.Abs(diff.Y) < FireSoundRange) { Vector2 diffLeft = (fs.WorldPosition + new Vector2(fs.Size.X, fs.Size.Y / 2)) - listenerPos; if (Math.Abs(diff.X) < fs.Size.X / 2.0f) { diffLeft.X = 0.0f; } if (diffLeft.X <= 0) { float distFallOffLeft = diffLeft.Length() / FireSoundRange; if (distFallOffLeft < 0.99f) { fireVolumeLeft[0] += (1.0f - distFallOffLeft); if (fs.Size.X > FireSoundLargeLimit) { fireVolumeLeft[2] += (1.0f - distFallOffLeft) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit); } else if (fs.Size.X > FireSoundMediumLimit) { fireVolumeLeft[1] += (1.0f - distFallOffLeft) * ((fs.Size.X - FireSoundMediumLimit) / FireSoundMediumLimit); } } } Vector2 diffRight = (fs.WorldPosition + new Vector2(0.0f, fs.Size.Y / 2)) - listenerPos; if (Math.Abs(diff.X) < fs.Size.X / 2.0f) { diffRight.X = 0.0f; } if (diffRight.X >= 0) { float distFallOffRight = diffRight.Length() / FireSoundRange; if (distFallOffRight < 0.99f) { fireVolumeRight[0] += 1.0f - distFallOffRight; if (fs.Size.X > FireSoundLargeLimit) { fireVolumeRight[2] += (1.0f - distFallOffRight) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit); } else if (fs.Size.X > FireSoundMediumLimit) { fireVolumeRight[1] += (1.0f - distFallOffRight) * ((fs.Size.X - FireSoundMediumLimit) / FireSoundMediumLimit); } } } } } } private static void UpdateRandomAmbience(float deltaTime) { if (ambientSoundTimer > 0.0f) { ambientSoundTimer -= deltaTime; } else { PlaySound( "ambient", new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y) + Rand.Vector(100.0f), Rand.Range(0.5f, 1.0f), 1000.0f); ambientSoundTimer = Rand.Range(ambientSoundInterval.X, ambientSoundInterval.Y); } } private static void UpdateHullSounds(float deltaTime) { if (hullSoundChannel != null && hullSoundChannel.IsPlaying && hullSoundSource != null) { hullSoundChannel.Position = new Vector3(hullSoundSource.WorldPosition, 0.0f); hullSoundChannel.Gain = GetHullSoundVolume(hullSoundSource.Submarine); } if (hullSoundTimer > 0.0f) { hullSoundTimer -= deltaTime; } else { if (!Level.IsLoadedFriendlyOutpost && Character.Controlled?.CurrentHull?.Submarine is Submarine sub && sub.Info != null && !sub.Info.IsOutpost) { hullSoundSource = Character.Controlled.CurrentHull; hullSoundChannel = PlaySound("hull", hullSoundSource.WorldPosition, volume: GetHullSoundVolume(sub), range: 1500.0f); hullSoundTimer = Rand.Range(hullSoundInterval.X, hullSoundInterval.Y); } else { hullSoundTimer = 5.0f; } } static float GetHullSoundVolume(Submarine sub) { var depth = Level.Loaded == null ? 0.0f : Math.Abs(sub.Position.Y - Level.Loaded.Size.Y) * Physics.DisplayToRealWorldRatio; return Math.Clamp((depth - 800.0f) / 1500.0f, 0.4f, 1.0f); } } public static Sound GetSound(string soundTag) { var matchingSounds = SoundPrefab.Prefabs.Where(p => p.ElementName == soundTag); if (!matchingSounds.Any()) return null; return matchingSounds.GetRandomUnsynced().Sound; } /// /// Play a sound defined in a sound xml file without any positional effects. /// public static SoundChannel PlaySound(string soundTag, float volume = 1.0f) { var sound = GetSound(soundTag); return sound?.Play(volume); } /// /// Play a sound defined in a sound xml file. If the volume or range parameters are omitted, the volume and range defined in the sound xml are used. /// public static SoundChannel PlaySound(string soundTag, Vector2 position, float? volume = null, float? range = null, Hull hullGuess = null) { var sound = GetSound(soundTag); if (sound == null) { return null; } return PlaySound(sound, position, volume ?? sound.BaseGain, range ?? sound.BaseFar, 1.0f, hullGuess); } public static SoundChannel PlaySound(RoundSound sound, Vector2 position, float? volume = null, Hull hullGuess = null) { return PlaySound( sound.Sound, position, volume ?? sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, hullGuess: hullGuess, freqMult: sound.GetRandomFrequencyMultiplier(), muteBackgroundMusic: sound.MuteBackgroundMusic); } public static SoundChannel PlaySound(Sound sound, Vector2 position, float? volume = null, float? range = null, float? freqMult = null, Hull hullGuess = null, bool ignoreMuffling = false, bool muteBackgroundMusic = false) { if (sound == null) { string errorMsg = "Error in SoundPlayer.PlaySound (sound was null)\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.PlaySound:SoundNull" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return null; } float far = range ?? sound.BaseFar; if (Vector2.DistanceSquared(new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y), position) > far * far) { return null; } bool muffle = !ignoreMuffling && ShouldMuffleSound(Character.Controlled, position, far, hullGuess); var channel = sound.Play(volume ?? sound.BaseGain, far, freqMult ?? 1.0f, position, muffle: muffle); if (channel != null) { channel.MuteBackgroundMusic = muteBackgroundMusic; } return channel; } public static void DisposeDisabledMusic() { bool musicDisposed = false; for (int i = 0; i < currentMusic.Length; i++) { var music = currentMusic[i]; if (music is null) { continue; } if (!SoundPrefab.Prefabs.Contains(music)) { musicChannel[i].Dispose(); musicDisposed = true; currentMusic[i] = null; } } for (int i = 0; i < targetMusic.Length; i++) { var music = targetMusic[i]; if (music is null) { continue; } if (!SoundPrefab.Prefabs.Contains(music)) { targetMusic[i] = null; } } if (musicDisposed) { Thread.Sleep(60); } } public static void ForceMusicUpdate() { updateMusicTimer = 0.0f; } private static void UpdateMusic(float deltaTime) { if (musicClips == null || (GameMain.SoundManager?.Disabled ?? true)) { return; } if (OverrideMusicType != null && OverrideMusicDuration.HasValue) { OverrideMusicDuration -= deltaTime; if (OverrideMusicDuration <= 0.0f) { OverrideMusicType = Identifier.Empty; OverrideMusicDuration = null; } } int noiseLoopIndex = 1; updateMusicTimer -= deltaTime; if (updateMusicTimer <= 0.0f) { //find appropriate music for the current situation Identifier currentMusicType = GetCurrentMusicType(); float currentIntensity = GameMain.GameSession?.EventManager != null ? GameMain.GameSession.EventManager.MusicIntensity * 100.0f : 0.0f; IEnumerable suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity); int mainTrackIndex = 0; if (suitableMusic.None()) { targetMusic[mainTrackIndex] = null; } //switch the music if nothing playing atm or the currently playing clip is not suitable anymore else if (targetMusic[mainTrackIndex] == null || currentMusic[mainTrackIndex] == null || !currentMusic[mainTrackIndex].IsPlaying() || !suitableMusic.Any(m => m == currentMusic[mainTrackIndex])) { if (currentMusicType == "default") { if (previousDefaultMusic == null) { targetMusic[mainTrackIndex] = previousDefaultMusic = suitableMusic.GetRandomUnsynced(); } else { targetMusic[mainTrackIndex] = previousDefaultMusic; } } else { targetMusic[mainTrackIndex] = suitableMusic.GetRandomUnsynced(); } } if (Level.Loaded != null && (Level.Loaded.Type == LevelData.LevelType.LocationConnection || Level.Loaded.GenerationParams.PlayNoiseLoopInOutpostLevel)) { Identifier biome = Level.Loaded.LevelData.Biome.Identifier; if (Level.Loaded.IsEndBiome && GameMain.GameSession?.Campaign is CampaignMode campaign) { //don't play end biome music in the path leading up to the end level(s) if (!campaign.Map.EndLocations.Contains(Level.Loaded.StartLocation)) { biome = Level.Loaded.StartLocation.Biome.Identifier; } } // Find background noise loop for the current biome IEnumerable suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ? GetSuitableMusicClips(biome, currentIntensity) : Enumerable.Empty(); if (suitableNoiseLoops.Count() == 0) { targetMusic[noiseLoopIndex] = null; } // Switch the noise loop if nothing playing atm or the currently playing clip is not suitable anymore else if (targetMusic[noiseLoopIndex] == null || currentMusic[noiseLoopIndex] == null || !suitableNoiseLoops.Any(m => m == currentMusic[noiseLoopIndex])) { targetMusic[noiseLoopIndex] = suitableNoiseLoops.GetRandomUnsynced(); } } else { targetMusic[noiseLoopIndex] = null; } IEnumerable suitableTypeAmbiences = GetSuitableMusicClips($"{currentMusicType}ambience".ToIdentifier(), currentIntensity); int typeAmbienceTrackIndex = 2; if (suitableTypeAmbiences.None()) { targetMusic[typeAmbienceTrackIndex] = null; } // Switch the type ambience if nothing playing atm or the currently playing clip is not suitable anymore else if (targetMusic[typeAmbienceTrackIndex] == null || currentMusic[typeAmbienceTrackIndex] == null || !currentMusic[typeAmbienceTrackIndex].IsPlaying() || suitableTypeAmbiences.None(m => m == currentMusic[typeAmbienceTrackIndex])) { targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced(); } IEnumerable suitableIntensityMusic = Enumerable.Empty(); BackgroundMusic mainTrack = targetMusic[mainTrackIndex]; if (mainTrack is not { MuteIntensityTracks: true } && Screen.Selected == GameMain.GameScreen) { float intensity = currentIntensity; if (mainTrack?.ForceIntensityTrack != null) { intensity = mainTrack.ForceIntensityTrack.Value; } suitableIntensityMusic = GetSuitableMusicClips("intensity".ToIdentifier(), intensity); } //get the appropriate intensity layers for current situation int intensityTrackStartIndex = 3; for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { //disable targetmusics that aren't suitable anymore if (targetMusic[i] != null && !suitableIntensityMusic.Any(m => m == targetMusic[i])) { targetMusic[i] = null; } } foreach (BackgroundMusic intensityMusic in suitableIntensityMusic) { //already playing, do nothing if (targetMusic.Any(m => m != null && m == intensityMusic)) { continue; } for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { if (targetMusic[i] == null) { targetMusic[i] = intensityMusic; break; } } } LogCurrentMusic(); updateMusicTimer = UpdateMusicInterval; if (mainTrack != null) { updateMusicTimer += mainTrack.MinimumPlayDuration; } } bool muteBackgroundMusic = false; for (int i = 0; i < SoundManager.SourceCount; i++) { SoundChannel playingSoundChannel = GameMain.SoundManager.GetSoundChannelFromIndex(SoundManager.SourcePoolIndex.Default, i); if (playingSoundChannel is { MuteBackgroundMusic: true, IsPlaying: true }) { muteBackgroundMusic = true; break; } } int activeTrackCount = targetMusic.Count(m => m != null); for (int i = 0; i < MaxMusicChannels; i++) { //nothing should be playing on this channel if (targetMusic[i] == null) { if (musicChannel[i] != null && musicChannel[i].IsPlaying) { //mute the channel musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, 0.0f, MusicLerpSpeed * deltaTime); if (musicChannel[i].Gain < 0.01f) { DisposeMusicChannel(i); } } } //something should be playing, but the targetMusic is invalid else if (!musicClips.Any(mc => mc == targetMusic[i])) { targetMusic[i] = GetSuitableMusicClips(targetMusic[i].Type, 0.0f).GetRandomUnsynced(); } //something should be playing, but the channel is playing nothing or an incorrect clip else if (currentMusic[i] == null || targetMusic[i] != currentMusic[i]) { //something playing -> mute it first if (musicChannel[i] != null && musicChannel[i].IsPlaying) { musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, 0.0f, MusicLerpSpeed * deltaTime); if (musicChannel[i].Gain < 0.01f) { DisposeMusicChannel(i); } } //channel free now, start playing the correct clip if (currentMusic[i] == null || (musicChannel[i] == null || !musicChannel[i].IsPlaying)) { DisposeMusicChannel(i); currentMusic[i] = targetMusic[i]; musicChannel[i] = currentMusic[i].Sound.Play(0.0f, i == noiseLoopIndex ? SoundManager.SoundCategoryDefault : SoundManager.SoundCategoryMusic); if (targetMusic[i].ContinueFromPreviousTime) { musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime; } else if (targetMusic[i].StartFromRandomTime) { musicChannel[i].StreamSeekPos = (int)(musicChannel[i].MaxStreamSeekPos * Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced)); } musicChannel[i].Looping = true; } } else { //playing something, lerp volume up if (musicChannel[i] == null || !musicChannel[i].IsPlaying) { musicChannel[i]?.Dispose(); musicChannel[i] = currentMusic[i].Sound.Play(0.0f, i == noiseLoopIndex ? SoundManager.SoundCategoryDefault : SoundManager.SoundCategoryMusic); musicChannel[i].Looping = true; } float targetGain = targetMusic[i].Volume; if (muteBackgroundMusic) { targetGain = 0.0f; } if (targetMusic[i].DuckVolume) { targetGain *= (float)Math.Sqrt(1.0f / activeTrackCount); } musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, targetGain, MusicLerpSpeed * deltaTime); } } } private static double lastMusicLogTime; const double MusicLogInterval = 60.0; private static void LogCurrentMusic() { if (Screen.Selected != GameMain.GameScreen) { return; } if (Timing.TotalTime < lastMusicLogTime + MusicLogInterval) { return; } for (int i = 0; i < musicChannel.Length; i++) { if (musicChannel[i] != null && musicChannel[i].IsPlaying && musicChannel[i].Sound?.Filename != null) { GameAnalyticsManager.AddDesignEvent( "BackgroundMusic:" + Path.GetFileNameWithoutExtension(musicChannel[i].Sound.Filename.Replace(":", string.Empty).Replace(" ", string.Empty))); } } lastMusicLogTime = Timing.TotalTime; } private static void DisposeMusicChannel(int index) { var clip = musicClips.FirstOrDefault(m => m.Sound == musicChannel[index]?.Sound); if (clip != null) { if (clip.ContinueFromPreviousTime) { clip.PreviousTime = musicChannel[index].StreamSeekPos; } } musicChannel[index]?.Dispose(); musicChannel[index] = null; currentMusic[index] = null; } private static IEnumerable GetSuitableMusicClips(Identifier musicType, float currentIntensity) { return musicClips.Where(music => IsSuitableMusicClip(music, musicType, currentIntensity)); } private static bool IsSuitableMusicClip(BackgroundMusic music, Identifier musicType, float currentIntensity) { return music != null && music.Type == musicType && currentIntensity >= music.IntensityRange.X && currentIntensity <= music.IntensityRange.Y; } private static Identifier GetCurrentMusicType() { if (OverrideMusicType != null) { return OverrideMusicType; } if (Screen.Selected == null) { return "menu".ToIdentifier(); } if (Screen.Selected is { IsEditor: true } || GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected == GameMain.NetLobbyScreen) { return "editor".ToIdentifier(); } if (Screen.Selected != GameMain.GameScreen) { previousDefaultMusic = null; return (firstTimeInMainMenu ? "menu" : "default").ToIdentifier(); } firstTimeInMainMenu = false; if (GameMain.GameSession != null) { foreach (var mission in GameMain.GameSession.Missions) { var missionMusic = mission.GetOverrideMusicType(); if (!missionMusic.IsEmpty) { return missionMusic; } } } if (Character.Controlled != null) { if (Level.Loaded != null && Level.Loaded.Ruins != null && Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition))) { return "ruins".ToIdentifier(); } if (Character.Controlled.Submarine?.Info?.IsWreck ?? false) { return "wreck".ToIdentifier(); } if (Level.IsLoadedOutpost) { // Only return music type for location types which have music tracks defined var locationType = Level.Loaded?.StartLocation?.Type?.Identifier; var backgroundMusicIdentifier = Level.Loaded?.StartLocation?.Type?.BackgroundMusicLocationType; if (MatchesTrack(backgroundMusicIdentifier, out Identifier id) || MatchesTrack(locationType, out id)) { return id; } bool MatchesTrack(Identifier? identifier, out Identifier idValue) { if (identifier.HasValue && identifier.Value != Identifier.Empty) { if (musicClips.Any(clip => clip.Type == identifier.Value)) { idValue = identifier.Value; return true; } } idValue = Identifier.Empty; return false; } } } if (Level.Loaded is { IsEndBiome: true }) { return "endlevel".ToIdentifier(); } Submarine targetSubmarine = Character.Controlled?.Submarine; float intensity = (GameMain.GameSession?.EventManager?.MusicIntensity ?? 0) * 100.0f; float enemyDistThreshold = 5000.0f; if (targetSubmarine != null) { enemyDistThreshold = Math.Max(enemyDistThreshold, Math.Max(targetSubmarine.Borders.Width, targetSubmarine.Borders.Height) * 2.0f); } List monsterMusicCharacters = new List(); foreach (Character character in Character.CharacterList) { if (character.IsDead || !character.Enabled) { continue; } if (character.AIController is not EnemyAIController { Enabled: true } enemyAI) { continue; } if (!enemyAI.AttackHumans && !enemyAI.AttackRooms) { continue; } bool specificMonsterMusicAvailable = musicClips.Any(m => IsSuitableMusicClip(m, character.Params.MusicType, intensity)); if (specificMonsterMusicAvailable) { float maxDistSqr = MathF.Pow(enemyDistThreshold * character.Params.MusicRangeMultiplier, 2); if (targetSubmarine != null) { if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < maxDistSqr) { monsterMusicCharacters.Add(character); } } else if (Character.Controlled != null) { if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < maxDistSqr) { monsterMusicCharacters.Add(character); } } } } if (monsterMusicCharacters.Any()) { Character chosenCharacter = monsterMusicCharacters.GetRandomByWeight(c => c.Params.MusicCommonness, Rand.RandSync.Unsynced); return chosenCharacter.Params.MusicType; } if (targetSubmarine != null && targetSubmarine.AtDamageDepth) { return "deep".ToIdentifier(); } if (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && Submarine.MainSub != null && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) > Submarine.MainSub.RealWorldCrushDepth) { return "deep".ToIdentifier(); } if (targetSubmarine != null) { 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 "flooded".ToIdentifier(); } } if (GameMain.GameSession != null) { if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { return "levelend".ToIdentifier(); } if (GameMain.GameSession.RoundDuration < 120.0 && Level.Loaded?.Type == LevelData.LevelType.LocationConnection) { return "start".ToIdentifier(); } } return "default".ToIdentifier(); } public static bool ShouldMuffleSound(Character listener, Vector2 soundWorldPos, float range, Hull hullGuess) { if (listener == null) return false; float lowpassHFGain = 1.0f; AnimController animController = listener.AnimController; if (animController.HeadInWater) { lowpassHFGain = 0.2f; } lowpassHFGain *= Character.Controlled.LowPassMultiplier; if (lowpassHFGain < 0.5f) return true; Hull targetHull = Hull.FindHull(soundWorldPos, hullGuess, true); if (listener.CurrentHull == null || targetHull == null) { return listener.CurrentHull != targetHull; } Vector2 soundPos = soundWorldPos; if (targetHull.Submarine != null) { soundPos += -targetHull.Submarine.WorldPosition + targetHull.Submarine.HiddenSubPosition; } return listener.CurrentHull.GetApproximateDistance(listener.Position, soundPos, targetHull, range) > range; } public static void PlaySplashSound(Vector2 worldPosition, float strength) { if (SplashSounds.Count == 0) { return; } int splashIndex = MathHelper.Clamp((int)(strength + Rand.Range(-2.0f, 2.0f)), 0, SplashSounds.Count - 1); float range = 800.0f; SplashSounds[splashIndex].Sound?.Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(Character.Controlled, worldPosition, range, null)); } 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, IEnumerable tags = null, float gain = 1.0f) { var suitableSounds = damageSounds.Where(s => s.DamageType == damageType && (s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag)))); //if the damage is too low for any sound, don't play anything if (suitableSounds.All(d => damage < d.DamageRange.X)) { return; } //allow the damage to differ by 10 from the configured damage range, //so the same amount of damage doesn't always play the same sound float randomizedDamage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); suitableSounds = suitableSounds.Where(s => s.DamageRange == Vector2.Zero || (randomizedDamage >= s.DamageRange.X && randomizedDamage <= s.DamageRange.Y)); var damageSound = suitableSounds.GetRandomUnsynced(); damageSound?.Sound?.Play(gain, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); } public static void PlayUISound(GUISoundType soundType) { GUISound.GUISoundPrefabs .Where(s => s.Type == soundType) .GetRandomUnsynced()?.Sound?.Play(null, SoundManager.SoundCategoryUi); } public static void PlayUISound(GUISoundType? soundType) { if (soundType.HasValue) { PlayUISound(soundType.Value); } } } }