4 using Microsoft.Xna.Framework;
6 using System.Collections.Generic;
8 using System.Threading;
12 static class SoundPlayer
15 private const float MusicLerpSpeed = 1.0f;
16 private const float UpdateMusicInterval = 5.0f;
18 public const float MuffleFilterFrequency = 600;
20 const int MaxMusicChannels = 6;
22 private readonly
static BackgroundMusic[] currentMusic =
new BackgroundMusic[MaxMusicChannels];
24 private readonly
static BackgroundMusic[] targetMusic =
new BackgroundMusic[MaxMusicChannels];
25 private static IEnumerable<BackgroundMusic> musicClips => BackgroundMusic.BackgroundMusicPrefabs;
27 private static BackgroundMusic previousDefaultMusic;
29 private static float updateMusicTimer;
32 private static SoundPrefab waterAmbienceIn => SoundPrefab.WaterAmbienceIn.ActivePrefab;
33 private static SoundPrefab waterAmbienceOut => SoundPrefab.WaterAmbienceOut.ActivePrefab;
34 private static SoundPrefab waterAmbienceMoving => SoundPrefab.WaterAmbienceMoving.ActivePrefab;
35 private static readonly HashSet<SoundChannel> waterAmbienceChannels =
new HashSet<SoundChannel>();
37 private static float ambientSoundTimer;
38 private static Vector2 ambientSoundInterval =
new Vector2(20.0f, 40.0f);
41 private static Hull hullSoundSource;
42 private static float hullSoundTimer;
43 private static Vector2 hullSoundInterval =
new Vector2(45.0f, 90.0f);
46 private static float[] targetFlowLeft, targetFlowRight;
47 public static IReadOnlyList<SoundPrefab> FlowSounds => SoundPrefab.FlowSounds;
48 public static IReadOnlyList<SoundPrefab> SplashSounds => SoundPrefab.SplashSounds;
50 private static float[] flowVolumeLeft;
51 private static float[] flowVolumeRight;
53 const float FlowSoundRange = 1500.0f;
54 const float MaxFlowStrength = 400.0f;
57 private static float[] fireVolumeLeft;
58 private static float[] fireVolumeRight;
60 const float FireSoundRange = 1000.0f;
61 const float FireSoundMediumLimit = 100.0f;
62 const float FireSoundLargeLimit = 200.0f;
63 const int fireSizes = 3;
64 private static string[] fireSoundTags =
new string[fireSizes] {
"fire",
"firemedium",
"firelarge" };
67 private static IEnumerable<DamageSound> damageSounds => DamageSound.DamageSoundPrefabs;
69 private static bool firstTimeInMainMenu =
true;
71 private static Sound startUpSound => SoundPrefab.StartupSound.ActivePrefab.
Sound;
73 public static Identifier OverrideMusicType
79 public static float? OverrideMusicDuration;
81 public static void Update(
float deltaTime)
83 UpdateMusic(deltaTime);
84 if (flowSoundChannels ==
null || flowSoundChannels.Length != FlowSounds.Count)
87 flowVolumeLeft =
new float[FlowSounds.Count];
88 flowVolumeRight =
new float[FlowSounds.Count];
89 targetFlowLeft =
new float[FlowSounds.Count];
90 targetFlowRight =
new float[FlowSounds.Count];
92 if (fireSoundChannels ==
null || fireSoundChannels.Length != fireSizes)
95 fireVolumeLeft =
new float[fireSizes];
96 fireVolumeRight =
new float[fireSizes];
100 if (
Submarine.MainSub ==
null || Screen.Selected != GameMain.GameScreen)
102 foreach (var chn
in waterAmbienceChannels.Concat(flowSoundChannels).Concat(fireSoundChannels))
106 fireVolumeLeft[0] = 0.0f; fireVolumeLeft[1] = 0.0f;
107 fireVolumeRight[0] = 0.0f; fireVolumeRight[1] = 0.0f;
109 hullSoundSource =
null;
113 float ambienceVolume = 0.8f;
116 AnimController animController =
Character.Controlled.AnimController;
117 if (animController.HeadInWater)
119 ambienceVolume = 1.0f;
120 float limbSpeed = animController.Limbs[0].LinearVelocity.Length();
121 if (MathUtils.IsValid(limbSpeed))
123 ambienceVolume += limbSpeed;
128 UpdateWaterAmbience(ambienceVolume, deltaTime);
129 UpdateWaterFlowSounds(deltaTime);
130 UpdateRandomAmbience(deltaTime);
131 UpdateHullSounds(deltaTime);
132 UpdateFireSounds(deltaTime);
135 private static void UpdateWaterAmbience(
float ambienceVolume,
float deltaTime)
137 if (GameMain.SoundManager.Disabled || GameMain.GameScreen?.Cam ==
null) {
return; }
140 float movementSoundVolume = 0.0f;
142 float insideSubFactor = 0.0f;
143 foreach (Submarine sub
in Submarine.Loaded)
145 if (sub ==
null || sub.Removed) {
continue; }
146 float movementFactor = (sub.Velocity == Vector2.Zero) ? 0.0f : sub.Velocity.Length() / 10.0f;
147 movementFactor = MathHelper.Clamp(movementFactor, 0.0f, 1.0f);
151 float dist = Vector2.Distance(GameMain.GameScreen.Cam.WorldViewCenter, sub.WorldPosition);
152 movementFactor /= Math.Max(dist / 1000.0f, 1.0f);
153 insideSubFactor = Math.Max(1.0f / Math.Max(dist / 1000.0f, 1.0f), insideSubFactor);
157 insideSubFactor = 1.0f;
163 insideSubFactor -=
Character.Controlled.PressureTimer / 100.0f;
166 movementSoundVolume = Math.Max(movementSoundVolume, movementFactor);
167 if (!MathUtils.IsValid(movementSoundVolume))
169 string errorMsg =
"Failed to update water ambience volume - submarine's movement value invalid (" + movementSoundVolume +
", sub velocity: " + sub.Velocity +
")";
170 DebugConsole.Log(errorMsg);
171 GameAnalyticsManager.AddErrorEventOnce(
"SoundPlayer.UpdateWaterAmbience:InvalidVolume", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
172 movementSoundVolume = 0.0f;
174 if (!MathUtils.IsValid(insideSubFactor))
176 string errorMsg =
"Failed to update water ambience volume - inside sub value invalid (" + insideSubFactor +
")";
177 DebugConsole.Log(errorMsg);
178 GameAnalyticsManager.AddErrorEventOnce(
"SoundPlayer.UpdateWaterAmbience:InvalidVolume", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
179 insideSubFactor = 0.0f;
183 void updateWaterAmbience(
Sound sound,
float volume)
185 SoundChannel chn = waterAmbienceChannels.FirstOrDefault(c => c.Sound == sound);
186 if (Level.Loaded !=
null)
188 volume *= Level.Loaded.GenerationParams.WaterAmbienceVolume;
192 if (volume < 0.01f) {
return; }
193 if (chn is not
null) { waterAmbienceChannels.Remove(chn); }
194 chn = sound.
Play(volume,
"waterambience");
196 waterAmbienceChannels.Add(chn);
200 chn.
Gain += deltaTime * Math.Sign(volume - chn.
Gain);
201 if (chn.
Gain < 0.01f)
217 updateWaterAmbience(waterAmbienceIn.Sound, ambienceVolume * (1.0f - movementSoundVolume) * insideSubFactor * waterAmbienceIn.Volume);
218 updateWaterAmbience(waterAmbienceMoving.Sound, ambienceVolume * movementSoundVolume * insideSubFactor * waterAmbienceMoving.Volume);
219 updateWaterAmbience(waterAmbienceOut.Sound, (1.0f - insideSubFactor) * waterAmbienceOut.Volume);
222 private static void UpdateWaterFlowSounds(
float deltaTime)
224 if (FlowSounds.Count == 0) {
return; }
226 for (
int i = 0; i < targetFlowLeft.Length; i++)
228 targetFlowLeft[i] = 0.0f;
229 targetFlowRight[i] = 0.0f;
232 Vector2 listenerPos =
new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y);
233 foreach (Gap gap
in Gap.GapList)
235 Vector2 diff = gap.WorldPosition - listenerPos;
236 if (Math.Abs(diff.X) < FlowSoundRange && Math.Abs(diff.Y) < FlowSoundRange)
238 if (gap.Open < 0.01f || gap.LerpedFlowForce.LengthSquared() < 100.0f) {
continue; }
239 float gapFlow = Math.Abs(gap.LerpedFlowForce.X) + Math.Abs(gap.LerpedFlowForce.Y) * 2.5f;
240 if (!gap.IsRoomToRoom) { gapFlow *= 2.0f; }
241 if (gapFlow < 10.0f) {
continue; }
243 if (gap.linkedTo.Count == 2 && gap.linkedTo[0] is Hull hull1 && gap.linkedTo[1] is Hull hull2)
246 if (hull1.linkedTo.Contains(hull2)) {
continue; }
247 if (hull1.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) {
continue; }
248 if (hull2.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) {
continue; }
251 int flowSoundIndex = (int)Math.Floor(MathHelper.Clamp(gapFlow / MaxFlowStrength, 0, FlowSounds.Count));
252 flowSoundIndex = Math.Min(flowSoundIndex, FlowSounds.Count - 1);
254 float dist = diff.Length();
255 float distFallOff = dist / FlowSoundRange;
256 if (distFallOff >= 0.99f) {
continue; }
258 float gain = MathHelper.Clamp(gapFlow / 100.0f, 0.0f, 1.0f);
263 targetFlowLeft[flowSoundIndex] += gain - distFallOff;
267 targetFlowRight[flowSoundIndex] += gain - distFallOff;
272 if (
Character.Controlled?.CharacterHealth?.GetAffliction(
"psychosis") is AfflictionPsychosis psychosis)
274 if (psychosis.CurrentFloodType == AfflictionPsychosis.FloodType.Minor)
276 targetFlowLeft[0] = Math.Max(targetFlowLeft[0], 1.0f);
277 targetFlowRight[0] = Math.Max(targetFlowRight[0], 1.0f);
279 else if (psychosis.CurrentFloodType == AfflictionPsychosis.FloodType.Major)
281 targetFlowLeft[FlowSounds.Count - 1] = Math.Max(targetFlowLeft[FlowSounds.Count - 1], 1.0f);
282 targetFlowRight[FlowSounds.Count - 1] = Math.Max(targetFlowRight[FlowSounds.Count - 1], 1.0f);
286 for (
int i = 0; i < FlowSounds.Count; i++)
288 flowVolumeLeft[i] = (targetFlowLeft[i] < flowVolumeLeft[i]) ?
289 Math.Max(targetFlowLeft[i], flowVolumeLeft[i] - deltaTime) :
290 Math.Min(targetFlowLeft[i], flowVolumeLeft[i] + deltaTime * 10.0f);
291 flowVolumeRight[i] = (targetFlowRight[i] < flowVolumeRight[i]) ?
292 Math.Max(targetFlowRight[i], flowVolumeRight[i] - deltaTime) :
293 Math.Min(targetFlowRight[i], flowVolumeRight[i] + deltaTime * 10.0f);
295 if (flowVolumeLeft[i] < 0.05f && flowVolumeRight[i] < 0.05f)
297 if (flowSoundChannels[i] !=
null)
299 flowSoundChannels[i].
Dispose();
300 flowSoundChannels[i] =
null;
305 if (FlowSounds[i]?.
Sound ==
null) {
continue; }
306 Vector2 soundPos =
new Vector2(GameMain.SoundManager.ListenerPosition.X + (flowVolumeRight[i] - flowVolumeLeft[i]) * 100, GameMain.SoundManager.ListenerPosition.Y);
307 if (flowSoundChannels[i] ==
null || !flowSoundChannels[i].IsPlaying)
309 flowSoundChannels[i] = FlowSounds[i].
Sound.
Play(1.0f, FlowSoundRange, soundPos);
310 flowSoundChannels[i].
Looping =
true;
312 flowSoundChannels[i].
Gain = Math.Max(flowVolumeRight[i], flowVolumeLeft[i]);
313 flowSoundChannels[i].
Position =
new Vector3(soundPos, 0.0f);
318 private static void UpdateFireSounds(
float deltaTime)
320 for (
int i = 0; i < fireVolumeLeft.Length; i++)
322 fireVolumeLeft[i] = 0.0f;
323 fireVolumeRight[i] = 0.0f;
326 Vector2 listenerPos =
new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y);
327 foreach (Hull hull
in Hull.HullList)
329 foreach (FireSource fs
in hull.FireSources)
333 foreach (FireSource fs
in hull.FakeFireSources)
339 for (
int i = 0; i < fireVolumeLeft.Length; i++)
341 if (fireVolumeLeft[i] < 0.05f && fireVolumeRight[i] < 0.05f)
343 if (fireSoundChannels[i] !=
null)
346 fireSoundChannels[i] =
null;
351 Vector2 soundPos =
new Vector2(GameMain.SoundManager.ListenerPosition.X + (fireVolumeRight[i] - fireVolumeLeft[i]) * 100, GameMain.SoundManager.ListenerPosition.Y);
352 if (fireSoundChannels[i] ==
null || !fireSoundChannels[i].IsPlaying)
354 fireSoundChannels[i] = GetSound(fireSoundTags[i])?.Play(1.0f, FlowSoundRange, soundPos);
355 if (fireSoundChannels[i] ==
null) {
continue; }
356 fireSoundChannels[i].
Looping =
true;
358 fireSoundChannels[i].
Gain = Math.Max(fireVolumeRight[i], fireVolumeLeft[i]);
359 fireSoundChannels[i].
Position =
new Vector3(soundPos, 0.0f);
363 void AddFireVolume(FireSource fs)
365 Vector2 diff = fs.WorldPosition + fs.Size / 2 - listenerPos;
366 if (Math.Abs(diff.X) < FireSoundRange && Math.Abs(diff.Y) < FireSoundRange)
368 Vector2 diffLeft = (fs.WorldPosition +
new Vector2(fs.Size.X, fs.Size.Y / 2)) - listenerPos;
369 if (Math.Abs(diff.X) < fs.Size.X / 2.0f) { diffLeft.X = 0.0f; }
372 float distFallOffLeft = diffLeft.Length() / FireSoundRange;
373 if (distFallOffLeft < 0.99f)
375 fireVolumeLeft[0] += (1.0f - distFallOffLeft);
376 if (fs.Size.X > FireSoundLargeLimit)
378 fireVolumeLeft[2] += (1.0f - distFallOffLeft) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit);
380 else if (fs.Size.X > FireSoundMediumLimit)
382 fireVolumeLeft[1] += (1.0f - distFallOffLeft) * ((fs.Size.X - FireSoundMediumLimit) / FireSoundMediumLimit);
387 Vector2 diffRight = (fs.WorldPosition +
new Vector2(0.0f, fs.Size.Y / 2)) - listenerPos;
388 if (Math.Abs(diff.X) < fs.Size.X / 2.0f) { diffRight.X = 0.0f; }
389 if (diffRight.X >= 0)
391 float distFallOffRight = diffRight.Length() / FireSoundRange;
392 if (distFallOffRight < 0.99f)
394 fireVolumeRight[0] += 1.0f - distFallOffRight;
395 if (fs.Size.X > FireSoundLargeLimit)
397 fireVolumeRight[2] += (1.0f - distFallOffRight) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit);
399 else if (fs.Size.X > FireSoundMediumLimit)
401 fireVolumeRight[1] += (1.0f - distFallOffRight) * ((fs.Size.X - FireSoundMediumLimit) / FireSoundMediumLimit);
409 private static void UpdateRandomAmbience(
float deltaTime)
411 if (ambientSoundTimer > 0.0f)
413 ambientSoundTimer -= deltaTime;
419 new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y) + Rand.Vector(100.0f),
420 Rand.Range(0.5f, 1.0f),
423 ambientSoundTimer = Rand.Range(ambientSoundInterval.X, ambientSoundInterval.Y);
427 private static void UpdateHullSounds(
float deltaTime)
429 if (hullSoundChannel !=
null && hullSoundChannel.
IsPlaying && hullSoundSource !=
null)
431 hullSoundChannel.
Position =
new Vector3(hullSoundSource.WorldPosition, 0.0f);
432 hullSoundChannel.
Gain = GetHullSoundVolume(hullSoundSource.Submarine);
435 if (hullSoundTimer > 0.0f)
437 hullSoundTimer -= deltaTime;
441 if (!Level.IsLoadedFriendlyOutpost &&
Character.Controlled?.CurrentHull?.Submarine is Submarine sub &&
442 sub.Info !=
null && !sub.Info.IsOutpost)
444 hullSoundSource =
Character.Controlled.CurrentHull;
445 hullSoundChannel = PlaySound(
"hull", hullSoundSource.WorldPosition, volume: GetHullSoundVolume(sub), range: 1500.0f);
446 hullSoundTimer = Rand.Range(hullSoundInterval.X, hullSoundInterval.Y);
450 hullSoundTimer = 5.0f;
454 static float GetHullSoundVolume(Submarine sub)
456 var depth = Level.Loaded ==
null ? 0.0f : Math.Abs(sub.Position.Y - Level.Loaded.Size.Y) * Physics.DisplayToRealWorldRatio;
457 return Math.Clamp((depth - 800.0f) / 1500.0f, 0.4f, 1.0f);
461 public static Sound GetSound(
string soundTag)
463 var matchingSounds = SoundPrefab.Prefabs.Where(p => p.ElementName == soundTag);
464 if (!matchingSounds.Any())
return null;
466 return matchingSounds.GetRandomUnsynced().
Sound;
472 public static SoundChannel PlaySound(
string soundTag,
float volume = 1.0f)
474 var sound = GetSound(soundTag);
475 return sound?.Play(volume);
481 public static SoundChannel PlaySound(
string soundTag, Vector2 position,
float? volume =
null,
float? range =
null, Hull hullGuess =
null)
483 var sound = GetSound(soundTag);
484 if (sound ==
null) {
return null; }
485 return PlaySound(sound, position, volume ?? sound.BaseGain, range ?? sound.BaseFar, 1.0f, hullGuess);
488 public static SoundChannel PlaySound(
Sound sound, Vector2 position,
float? volume =
null,
float? range =
null,
float? freqMult =
null, Hull hullGuess =
null,
bool ignoreMuffling =
false)
492 string errorMsg =
"Error in SoundPlayer.PlaySound (sound was null)\n" + Environment.StackTrace.CleanupStackTrace();
493 GameAnalyticsManager.AddErrorEventOnce(
"SoundPlayer.PlaySound:SoundNull" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
497 float far = range ?? sound.
BaseFar;
499 if (Vector2.DistanceSquared(
new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y), position) > far * far)
503 bool muffle = !ignoreMuffling && ShouldMuffleSound(
Character.Controlled, position, far, hullGuess);
504 return sound.
Play(volume ?? sound.
BaseGain, far, freqMult ?? 1.0f, position, muffle: muffle);
507 public static void DisposeDisabledMusic()
509 bool musicDisposed =
false;
510 for (
int i = 0; i < currentMusic.Length; i++)
512 var music = currentMusic[i];
513 if (music is
null) {
continue; }
515 if (!SoundPrefab.Prefabs.Contains(music))
518 musicDisposed =
true;
519 currentMusic[i] =
null;
523 for (
int i = 0; i < targetMusic.Length; i++)
525 var music = targetMusic[i];
526 if (music is
null) {
continue; }
528 if (!SoundPrefab.Prefabs.Contains(music))
530 targetMusic[i] =
null;
534 if (musicDisposed) { Thread.Sleep(60); }
537 public static void ForceMusicUpdate()
539 updateMusicTimer = 0.0f;
542 private static void UpdateMusic(
float deltaTime)
544 if (musicClips ==
null || (GameMain.SoundManager?.Disabled ??
true)) {
return; }
546 if (OverrideMusicType !=
null && OverrideMusicDuration.HasValue)
548 OverrideMusicDuration -= deltaTime;
549 if (OverrideMusicDuration <= 0.0f)
551 OverrideMusicType = Identifier.Empty;
552 OverrideMusicDuration =
null;
556 int noiseLoopIndex = 1;
558 updateMusicTimer -= deltaTime;
559 if (updateMusicTimer <= 0.0f)
562 Identifier currentMusicType = GetCurrentMusicType();
563 float currentIntensity = GameMain.GameSession?.EventManager !=
null ?
564 GameMain.GameSession.EventManager.MusicIntensity * 100.0f : 0.0f;
566 IEnumerable<BackgroundMusic> suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity);
567 int mainTrackIndex = 0;
568 if (suitableMusic.None())
570 targetMusic[mainTrackIndex] =
null;
573 else if (targetMusic[mainTrackIndex] ==
null || currentMusic[mainTrackIndex] ==
null || !currentMusic[mainTrackIndex].IsPlaying() || !suitableMusic.Any(m => m == currentMusic[mainTrackIndex]))
575 if (currentMusicType ==
"default")
577 if (previousDefaultMusic ==
null)
579 targetMusic[mainTrackIndex] = previousDefaultMusic = suitableMusic.GetRandomUnsynced();
583 targetMusic[mainTrackIndex] = previousDefaultMusic;
588 targetMusic[mainTrackIndex] = suitableMusic.GetRandomUnsynced();
592 if (Level.Loaded !=
null && (Level.Loaded.Type == LevelData.LevelType.LocationConnection || Level.Loaded.GenerationParams.PlayNoiseLoopInOutpostLevel))
594 Identifier biome = Level.Loaded.LevelData.Biome.Identifier;
595 if (Level.Loaded.IsEndBiome && GameMain.GameSession?.Campaign is CampaignMode campaign)
598 if (!campaign.Map.EndLocations.Contains(Level.Loaded.StartLocation))
600 biome = Level.Loaded.StartLocation.Biome.Identifier;
605 IEnumerable<BackgroundMusic> suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ?
606 GetSuitableMusicClips(biome, currentIntensity) :
607 Enumerable.
Empty<BackgroundMusic>();
608 if (suitableNoiseLoops.Count() == 0)
610 targetMusic[noiseLoopIndex] =
null;
613 else if (targetMusic[noiseLoopIndex] ==
null || currentMusic[noiseLoopIndex] ==
null || !suitableNoiseLoops.Any(m => m == currentMusic[noiseLoopIndex]))
615 targetMusic[noiseLoopIndex] = suitableNoiseLoops.GetRandomUnsynced();
620 targetMusic[noiseLoopIndex] =
null;
623 IEnumerable<BackgroundMusic> suitableTypeAmbiences = GetSuitableMusicClips($
"{currentMusicType}ambience".ToIdentifier(), currentIntensity);
624 int typeAmbienceTrackIndex = 2;
625 if (suitableTypeAmbiences.None())
627 targetMusic[typeAmbienceTrackIndex] =
null;
630 else if (targetMusic[typeAmbienceTrackIndex] ==
null || currentMusic[typeAmbienceTrackIndex] ==
null || !currentMusic[typeAmbienceTrackIndex].IsPlaying() || suitableTypeAmbiences.None(m => m == currentMusic[typeAmbienceTrackIndex]))
632 targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced();
635 IEnumerable<BackgroundMusic> suitableIntensityMusic = Enumerable.Empty<BackgroundMusic>();
636 BackgroundMusic mainTrack = targetMusic[mainTrackIndex];
637 if (mainTrack is not { MuteIntensityTracks:
true } && Screen.Selected == GameMain.GameScreen)
639 float intensity = currentIntensity;
640 if (mainTrack?.ForceIntensityTrack !=
null)
642 intensity = mainTrack.ForceIntensityTrack.Value;
644 suitableIntensityMusic = GetSuitableMusicClips(
"intensity".ToIdentifier(), intensity);
647 int intensityTrackStartIndex = 3;
648 for (
int i = intensityTrackStartIndex; i < MaxMusicChannels; i++)
651 if (targetMusic[i] !=
null && !suitableIntensityMusic.Any(m => m == targetMusic[i]))
653 targetMusic[i] =
null;
657 foreach (BackgroundMusic intensityMusic
in suitableIntensityMusic)
660 if (targetMusic.Any(m => m !=
null && m == intensityMusic)) {
continue; }
662 for (
int i = intensityTrackStartIndex; i < MaxMusicChannels; i++)
664 if (targetMusic[i] ==
null)
666 targetMusic[i] = intensityMusic;
673 updateMusicTimer = UpdateMusicInterval;
676 int activeTrackCount = targetMusic.Count(m => m !=
null);
677 for (
int i = 0; i < MaxMusicChannels; i++)
680 if (targetMusic[i] ==
null)
682 if (musicChannel[i] !=
null && musicChannel[i].IsPlaying)
685 musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, 0.0f, MusicLerpSpeed * deltaTime);
686 if (musicChannel[i].Gain < 0.01f) { DisposeMusicChannel(i); }
690 else if (!musicClips.Any(mc => mc == targetMusic[i]))
692 targetMusic[i] = GetSuitableMusicClips(targetMusic[i].Type, 0.0f).GetRandomUnsynced();
695 else if (currentMusic[i] ==
null || targetMusic[i] != currentMusic[i])
698 if (musicChannel[i] !=
null && musicChannel[i].IsPlaying)
700 musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, 0.0f, MusicLerpSpeed * deltaTime);
701 if (musicChannel[i].Gain < 0.01f) { DisposeMusicChannel(i); }
704 if (currentMusic[i] ==
null || (musicChannel[i] ==
null || !musicChannel[i].IsPlaying))
706 DisposeMusicChannel(i);
708 currentMusic[i] = targetMusic[i];
709 musicChannel[i] = currentMusic[i].Sound.Play(0.0f, i == noiseLoopIndex ?
"default" :
"music");
710 if (targetMusic[i].ContinueFromPreviousTime)
712 musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime;
714 else if (targetMusic[i].StartFromRandomTime)
716 musicChannel[i].StreamSeekPos =
717 (int)(musicChannel[i].MaxStreamSeekPos * Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced));
719 musicChannel[i].Looping =
true;
725 if (musicChannel[i] ==
null || !musicChannel[i].IsPlaying)
727 musicChannel[i]?.Dispose();
728 musicChannel[i] = currentMusic[i].Sound.Play(0.0f, i == noiseLoopIndex ?
"default" :
"music");
729 musicChannel[i].Looping =
true;
731 float targetGain = targetMusic[i].Volume;
732 if (targetMusic[i].DuckVolume)
734 targetGain *= (float)Math.Sqrt(1.0f / activeTrackCount);
736 musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, targetGain, MusicLerpSpeed * deltaTime);
741 private static double lastMusicLogTime;
742 const double MusicLogInterval = 60.0;
743 private static void LogCurrentMusic()
745 if (Screen.Selected != GameMain.GameScreen) {
return; }
746 if (Timing.TotalTime < lastMusicLogTime + MusicLogInterval) {
return; }
747 for (
int i = 0; i < musicChannel.Length; i++)
749 if (musicChannel[i] !=
null &&
750 musicChannel[i].IsPlaying &&
751 musicChannel[i].
Sound?.Filename !=
null)
753 GameAnalyticsManager.AddDesignEvent(
755 Path.GetFileNameWithoutExtension(musicChannel[i].Sound.Filename.Replace(
":",
string.Empty).Replace(
" ",
string.Empty)));
758 lastMusicLogTime = Timing.TotalTime;
761 private static void DisposeMusicChannel(
int index)
763 var clip = musicClips.FirstOrDefault(m => m.Sound == musicChannel[index]?.Sound);
766 if (clip.ContinueFromPreviousTime) { clip.PreviousTime = musicChannel[index].StreamSeekPos; }
769 musicChannel[index]?.Dispose(); musicChannel[index] =
null;
770 currentMusic[index] =
null;
773 private static IEnumerable<BackgroundMusic> GetSuitableMusicClips(Identifier musicType,
float currentIntensity)
775 return musicClips.Where(music => IsSuitableMusicClip(music, musicType, currentIntensity));
778 private static bool IsSuitableMusicClip(BackgroundMusic music, Identifier musicType,
float currentIntensity)
782 music.Type == musicType &&
783 currentIntensity >= music.IntensityRange.X &&
784 currentIntensity <= music.IntensityRange.Y;
787 private static Identifier GetCurrentMusicType()
789 if (OverrideMusicType !=
null) {
return OverrideMusicType; }
791 if (Screen.Selected ==
null) {
return "menu".ToIdentifier(); }
793 if (Screen.Selected is { IsEditor: true } || GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected == GameMain.NetLobbyScreen)
795 return "editor".ToIdentifier();
798 if (Screen.Selected != GameMain.GameScreen)
800 previousDefaultMusic =
null;
801 return (firstTimeInMainMenu ?
"menu" :
"default").ToIdentifier();
804 firstTimeInMainMenu =
false;
806 if (GameMain.GameSession !=
null)
808 foreach (var mission
in GameMain.GameSession.Missions)
810 var missionMusic = mission.GetOverrideMusicType();
811 if (!missionMusic.IsEmpty) {
return missionMusic; }
817 if (Level.Loaded !=
null && Level.Loaded.Ruins !=
null &&
818 Level.Loaded.Ruins.Any(r => r.Area.Contains(
Character.Controlled.WorldPosition)))
820 return "ruins".ToIdentifier();
823 if (
Character.Controlled.Submarine?.Info?.IsWreck ??
false)
825 return "wreck".ToIdentifier();
828 if (Level.IsLoadedOutpost)
831 var locationType = Level.Loaded.StartLocation?.Type?.Identifier;
832 if (locationType.HasValue && locationType != Identifier.Empty && musicClips.Any(c => c.Type == locationType))
834 return locationType.Value;
839 if (Level.Loaded is { IsEndBiome: true })
841 return "endlevel".ToIdentifier();
845 if (targetSubmarine !=
null && targetSubmarine.AtDamageDepth)
847 return "deep".ToIdentifier();
849 if (GameMain.GameScreen !=
null && Screen.Selected == GameMain.GameScreen &&
Submarine.MainSub !=
null &&
850 Level.Loaded !=
null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) >
Submarine.MainSub.RealWorldCrushDepth)
852 return "deep".ToIdentifier();
855 if (targetSubmarine !=
null)
857 float floodedArea = 0.0f;
858 float totalArea = 0.0f;
859 foreach (Hull hull
in Hull.HullList)
861 if (hull.Submarine != targetSubmarine) {
continue; }
862 floodedArea += hull.WaterVolume;
863 totalArea += hull.Volume;
866 if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) {
return "flooded".ToIdentifier(); }
869 float intensity = (GameMain.GameSession?.EventManager?.MusicIntensity ?? 0) * 100.0f;
870 bool anyMonsterMusicAvailable =
871 musicClips.Any(m => IsSuitableMusicClip(m,
"monster".ToIdentifier(), intensity) || IsSuitableMusicClip(m,
"monsterambience".ToIdentifier(), intensity));
873 if (anyMonsterMusicAvailable)
875 float enemyDistThreshold = 5000.0f;
876 if (targetSubmarine !=
null)
878 enemyDistThreshold = Math.Max(enemyDistThreshold, Math.Max(targetSubmarine.Borders.Width, targetSubmarine.Borders.Height) * 2.0f);
880 foreach (Character character
in Character.CharacterList)
882 if (character.IsDead || !character.Enabled) {
continue; }
883 if (character.AIController is not EnemyAIController { Enabled:
true } enemyAI) {
continue; }
884 if (!enemyAI.AttackHumans && !enemyAI.AttackRooms) {
continue; }
886 if (targetSubmarine !=
null)
888 if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold)
890 return "monster".ToIdentifier();
895 if (Vector2.DistanceSquared(character.WorldPosition,
Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold)
897 return "monster".ToIdentifier();
904 if (GameMain.GameSession !=
null)
908 return "levelend".ToIdentifier();
910 if (GameMain.GameSession.RoundDuration < 120.0 &&
911 Level.Loaded?.Type == LevelData.LevelType.LocationConnection)
913 return "start".ToIdentifier();
917 return "default".ToIdentifier();
920 public static bool ShouldMuffleSound(Character listener, Vector2 soundWorldPos,
float range, Hull hullGuess)
922 if (listener ==
null)
return false;
924 float lowpassHFGain = 1.0f;
925 AnimController animController = listener.AnimController;
926 if (animController.HeadInWater)
928 lowpassHFGain = 0.2f;
930 lowpassHFGain *=
Character.Controlled.LowPassMultiplier;
931 if (lowpassHFGain < 0.5f)
return true;
933 Hull targetHull = Hull.FindHull(soundWorldPos, hullGuess,
true);
934 if (listener.CurrentHull ==
null || targetHull ==
null)
936 return listener.CurrentHull != targetHull;
938 Vector2 soundPos = soundWorldPos;
939 if (targetHull.Submarine !=
null)
941 soundPos += -targetHull.Submarine.WorldPosition + targetHull.Submarine.HiddenSubPosition;
943 return listener.CurrentHull.GetApproximateDistance(listener.Position, soundPos, targetHull, range) > range;
946 public static void PlaySplashSound(Vector2 worldPosition,
float strength)
948 if (SplashSounds.Count == 0) {
return; }
949 int splashIndex = MathHelper.Clamp((
int)(strength + Rand.Range(-2.0f, 2.0f)), 0, SplashSounds.Count - 1);
950 float range = 800.0f;
951 SplashSounds[splashIndex].Sound?.Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(
Character.Controlled, worldPosition, range,
null));
954 public static void PlayDamageSound(
string damageType,
float damage, PhysicsBody body)
956 Vector2 bodyPosition = body.DrawPosition;
957 PlayDamageSound(damageType, damage, bodyPosition, 800.0f);
960 public static void PlayDamageSound(
string damageType,
float damage, Vector2 position,
float range = 2000.0f, IEnumerable<Identifier> tags =
null,
float gain = 1.0f)
962 var suitableSounds = damageSounds.Where(s =>
963 s.DamageType == damageType &&
964 (s.RequiredTag.IsEmpty || (tags ==
null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag))));
967 if (suitableSounds.All(d => damage < d.DamageRange.X)) {
return; }
971 float randomizedDamage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f);
972 suitableSounds = suitableSounds.Where(s =>
973 s.DamageRange == Vector2.Zero || (randomizedDamage >= s.DamageRange.X && randomizedDamage <= s.DamageRange.Y));
975 var damageSound = suitableSounds.GetRandomUnsynced();
976 damageSound?.Sound?.Play(gain, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(
Character.Controlled, position, range,
null));
981 GUISound.GUISoundPrefabs
982 .Where(s => s.Type == soundType)
983 .GetRandomUnsynced()?.Sound?.Play(
null,
"ui");
988 if (soundType.HasValue)
990 PlayUISound(soundType.Value);
float FrequencyMultiplier
float frequencyMultiplier
Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, ContentXElement xElement=null, bool getFullPath=true)
virtual SoundChannel Play(float gain, float range, Vector2 position, bool muffle=false)
@ Character
Characters only