Client LuaCsForBarotrauma
SoundPlayer.cs
2 using Barotrauma.IO;
3 using Barotrauma.Sounds;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Threading;
9 
10 namespace Barotrauma
11 {
12  static class SoundPlayer
13  {
14  //music
15  private const float MusicLerpSpeed = 1.0f;
16  private const float UpdateMusicInterval = 5.0f;
17 
18  public const float MuffleFilterFrequency = 600;
19 
20  const int MaxMusicChannels = 6;
21 
22  private readonly static BackgroundMusic[] currentMusic = new BackgroundMusic[MaxMusicChannels];
23  private readonly static SoundChannel[] musicChannel = new SoundChannel[MaxMusicChannels];
24  private readonly static BackgroundMusic[] targetMusic = new BackgroundMusic[MaxMusicChannels];
25  private static IEnumerable<BackgroundMusic> musicClips => BackgroundMusic.BackgroundMusicPrefabs;
26 
27  private static BackgroundMusic previousDefaultMusic;
28 
29  private static float updateMusicTimer;
30 
31  //ambience
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>();
36 
37  private static float ambientSoundTimer;
38  private static Vector2 ambientSoundInterval = new Vector2(20.0f, 40.0f); //x = min, y = max
39 
40  private static SoundChannel hullSoundChannel;
41  private static Hull hullSoundSource;
42  private static float hullSoundTimer;
43  private static Vector2 hullSoundInterval = new Vector2(45.0f, 90.0f); //x = min, y = max
44 
45  //misc
46  private static float[] targetFlowLeft, targetFlowRight;
47  public static IReadOnlyList<SoundPrefab> FlowSounds => SoundPrefab.FlowSounds;
48  public static IReadOnlyList<SoundPrefab> SplashSounds => SoundPrefab.SplashSounds;
49  private static SoundChannel[] flowSoundChannels;
50  private static float[] flowVolumeLeft;
51  private static float[] flowVolumeRight;
52 
53  const float FlowSoundRange = 1500.0f;
54  const float MaxFlowStrength = 400.0f; //the heaviest water sound effect is played when the water flow is this strong
55 
56  private static SoundChannel[] fireSoundChannels;
57  private static float[] fireVolumeLeft;
58  private static float[] fireVolumeRight;
59 
60  const float FireSoundRange = 1000.0f;
61  const float FireSoundMediumLimit = 100.0f;
62  const float FireSoundLargeLimit = 200.0f; //switch to large fire sound when the size of a firesource is above this
63  const int fireSizes = 3;
64  private static string[] fireSoundTags = new string[fireSizes] { "fire", "firemedium", "firelarge" };
65 
66  // TODO: could use a dictionary to split up the list into smaller lists of same type?
67  private static IEnumerable<DamageSound> damageSounds => DamageSound.DamageSoundPrefabs;
68 
69  private static bool firstTimeInMainMenu = true;
70 
71  private static Sound startUpSound => SoundPrefab.StartupSound.ActivePrefab.Sound;
72 
73  public static Identifier OverrideMusicType
74  {
75  get;
76  set;
77  }
78 
79  public static float? OverrideMusicDuration;
80 
81  public static void Update(float deltaTime)
82  {
83  UpdateMusic(deltaTime);
84  if (flowSoundChannels == null || flowSoundChannels.Length != FlowSounds.Count)
85  {
86  flowSoundChannels = new SoundChannel[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];
91  }
92  if (fireSoundChannels == null || fireSoundChannels.Length != fireSizes)
93  {
94  fireSoundChannels = new SoundChannel[fireSizes];
95  fireVolumeLeft = new float[fireSizes];
96  fireVolumeRight = new float[fireSizes];
97  }
98 
99  //stop water sounds if no sub is loaded
100  if (Submarine.MainSub == null || Screen.Selected != GameMain.GameScreen)
101  {
102  foreach (var chn in waterAmbienceChannels.Concat(flowSoundChannels).Concat(fireSoundChannels))
103  {
104  chn?.FadeOutAndDispose();
105  }
106  fireVolumeLeft[0] = 0.0f; fireVolumeLeft[1] = 0.0f;
107  fireVolumeRight[0] = 0.0f; fireVolumeRight[1] = 0.0f;
108  hullSoundChannel?.FadeOutAndDispose();
109  hullSoundSource = null;
110  return;
111  }
112 
113  float ambienceVolume = 0.8f;
114  if (Character.Controlled != null && !Character.Controlled.Removed)
115  {
116  AnimController animController = Character.Controlled.AnimController;
117  if (animController.HeadInWater)
118  {
119  ambienceVolume = 1.0f;
120  float limbSpeed = animController.Limbs[0].LinearVelocity.Length();
121  if (MathUtils.IsValid(limbSpeed))
122  {
123  ambienceVolume += limbSpeed;
124  }
125  }
126  }
127 
128  UpdateWaterAmbience(ambienceVolume, deltaTime);
129  UpdateWaterFlowSounds(deltaTime);
130  UpdateRandomAmbience(deltaTime);
131  UpdateHullSounds(deltaTime);
132  UpdateFireSounds(deltaTime);
133  }
134 
135  private static void UpdateWaterAmbience(float ambienceVolume, float deltaTime)
136  {
137  if (GameMain.SoundManager.Disabled || GameMain.GameScreen?.Cam == null) { return; }
138 
139  //how fast the sub is moving, scaled to 0.0 -> 1.0
140  float movementSoundVolume = 0.0f;
141 
142  float insideSubFactor = 0.0f;
143  foreach (Submarine sub in Submarine.Loaded)
144  {
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);
148 
149  if (Character.Controlled == null || Character.Controlled.Submarine != sub)
150  {
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);
154  }
155  else
156  {
157  insideSubFactor = 1.0f;
158  }
159 
160  if (Character.Controlled != null && Character.Controlled.PressureTimer > 0.0f && !Character.Controlled.IsDead)
161  {
162  //make the sound lerp to the "outside" sound when under pressure
163  insideSubFactor -= Character.Controlled.PressureTimer / 100.0f;
164  }
165 
166  movementSoundVolume = Math.Max(movementSoundVolume, movementFactor);
167  if (!MathUtils.IsValid(movementSoundVolume))
168  {
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;
173  }
174  if (!MathUtils.IsValid(insideSubFactor))
175  {
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;
180  }
181  }
182 
183  void updateWaterAmbience(Sound sound, float volume)
184  {
185  SoundChannel chn = waterAmbienceChannels.FirstOrDefault(c => c.Sound == sound);
186  if (Level.Loaded != null)
187  {
188  volume *= Level.Loaded.GenerationParams.WaterAmbienceVolume;
189  }
190  if (chn is null || !chn.IsPlaying)
191  {
192  if (volume < 0.01f) { return; }
193  if (chn is not null) { waterAmbienceChannels.Remove(chn); }
194  chn = sound.Play(volume, "waterambience");
195  chn.Looping = true;
196  waterAmbienceChannels.Add(chn);
197  }
198  else
199  {
200  chn.Gain += deltaTime * Math.Sign(volume - chn.Gain);
201  if (chn.Gain < 0.01f)
202  {
203  chn.FadeOutAndDispose();
204  }
205  if (Character.Controlled != null && Character.Controlled.PressureTimer > 0.0f && !Character.Controlled.IsDead)
206  {
207  //make the sound decrease in pitch when under pressure
208  chn.FrequencyMultiplier = MathHelper.Clamp(Character.Controlled.PressureTimer / 200.0f, 0.75f, 1.0f);
209  }
210  else
211  {
212  chn.FrequencyMultiplier = Math.Min(chn.frequencyMultiplier + deltaTime, 1.0f);
213  }
214  }
215  }
216 
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);
220  }
221 
222  private static void UpdateWaterFlowSounds(float deltaTime)
223  {
224  if (FlowSounds.Count == 0) { return; }
225 
226  for (int i = 0; i < targetFlowLeft.Length; i++)
227  {
228  targetFlowLeft[i] = 0.0f;
229  targetFlowRight[i] = 0.0f;
230  }
231 
232  Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y);
233  foreach (Gap gap in Gap.GapList)
234  {
235  Vector2 diff = gap.WorldPosition - listenerPos;
236  if (Math.Abs(diff.X) < FlowSoundRange && Math.Abs(diff.Y) < FlowSoundRange)
237  {
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; }
242 
243  if (gap.linkedTo.Count == 2 && gap.linkedTo[0] is Hull hull1 && gap.linkedTo[1] is Hull hull2)
244  {
245  //no flow sounds between linked hulls (= rooms consisting of multiple hulls)
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; }
249  }
250 
251  int flowSoundIndex = (int)Math.Floor(MathHelper.Clamp(gapFlow / MaxFlowStrength, 0, FlowSounds.Count));
252  flowSoundIndex = Math.Min(flowSoundIndex, FlowSounds.Count - 1);
253 
254  float dist = diff.Length();
255  float distFallOff = dist / FlowSoundRange;
256  if (distFallOff >= 0.99f) { continue; }
257 
258  float gain = MathHelper.Clamp(gapFlow / 100.0f, 0.0f, 1.0f);
259 
260  //flow at the left side
261  if (diff.X < 0)
262  {
263  targetFlowLeft[flowSoundIndex] += gain - distFallOff;
264  }
265  else
266  {
267  targetFlowRight[flowSoundIndex] += gain - distFallOff;
268  }
269  }
270  }
271 
272  if (Character.Controlled?.CharacterHealth?.GetAffliction("psychosis") is AfflictionPsychosis psychosis)
273  {
274  if (psychosis.CurrentFloodType == AfflictionPsychosis.FloodType.Minor)
275  {
276  targetFlowLeft[0] = Math.Max(targetFlowLeft[0], 1.0f);
277  targetFlowRight[0] = Math.Max(targetFlowRight[0], 1.0f);
278  }
279  else if (psychosis.CurrentFloodType == AfflictionPsychosis.FloodType.Major)
280  {
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);
283  }
284  }
285 
286  for (int i = 0; i < FlowSounds.Count; i++)
287  {
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);
294 
295  if (flowVolumeLeft[i] < 0.05f && flowVolumeRight[i] < 0.05f)
296  {
297  if (flowSoundChannels[i] != null)
298  {
299  flowSoundChannels[i].Dispose();
300  flowSoundChannels[i] = null;
301  }
302  }
303  else
304  {
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)
308  {
309  flowSoundChannels[i] = FlowSounds[i].Sound.Play(1.0f, FlowSoundRange, soundPos);
310  flowSoundChannels[i].Looping = true;
311  }
312  flowSoundChannels[i].Gain = Math.Max(flowVolumeRight[i], flowVolumeLeft[i]);
313  flowSoundChannels[i].Position = new Vector3(soundPos, 0.0f);
314  }
315  }
316  }
317 
318  private static void UpdateFireSounds(float deltaTime)
319  {
320  for (int i = 0; i < fireVolumeLeft.Length; i++)
321  {
322  fireVolumeLeft[i] = 0.0f;
323  fireVolumeRight[i] = 0.0f;
324  }
325 
326  Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y);
327  foreach (Hull hull in Hull.HullList)
328  {
329  foreach (FireSource fs in hull.FireSources)
330  {
331  AddFireVolume(fs);
332  }
333  foreach (FireSource fs in hull.FakeFireSources)
334  {
335  AddFireVolume(fs);
336  }
337  }
338 
339  for (int i = 0; i < fireVolumeLeft.Length; i++)
340  {
341  if (fireVolumeLeft[i] < 0.05f && fireVolumeRight[i] < 0.05f)
342  {
343  if (fireSoundChannels[i] != null)
344  {
345  fireSoundChannels[i].FadeOutAndDispose();
346  fireSoundChannels[i] = null;
347  }
348  }
349  else
350  {
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)
353  {
354  fireSoundChannels[i] = GetSound(fireSoundTags[i])?.Play(1.0f, FlowSoundRange, soundPos);
355  if (fireSoundChannels[i] == null) { continue; }
356  fireSoundChannels[i].Looping = true;
357  }
358  fireSoundChannels[i].Gain = Math.Max(fireVolumeRight[i], fireVolumeLeft[i]);
359  fireSoundChannels[i].Position = new Vector3(soundPos, 0.0f);
360  }
361  }
362 
363  void AddFireVolume(FireSource fs)
364  {
365  Vector2 diff = fs.WorldPosition + fs.Size / 2 - listenerPos;
366  if (Math.Abs(diff.X) < FireSoundRange && Math.Abs(diff.Y) < FireSoundRange)
367  {
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; }
370  if (diffLeft.X <= 0)
371  {
372  float distFallOffLeft = diffLeft.Length() / FireSoundRange;
373  if (distFallOffLeft < 0.99f)
374  {
375  fireVolumeLeft[0] += (1.0f - distFallOffLeft);
376  if (fs.Size.X > FireSoundLargeLimit)
377  {
378  fireVolumeLeft[2] += (1.0f - distFallOffLeft) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit);
379  }
380  else if (fs.Size.X > FireSoundMediumLimit)
381  {
382  fireVolumeLeft[1] += (1.0f - distFallOffLeft) * ((fs.Size.X - FireSoundMediumLimit) / FireSoundMediumLimit);
383  }
384  }
385  }
386 
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)
390  {
391  float distFallOffRight = diffRight.Length() / FireSoundRange;
392  if (distFallOffRight < 0.99f)
393  {
394  fireVolumeRight[0] += 1.0f - distFallOffRight;
395  if (fs.Size.X > FireSoundLargeLimit)
396  {
397  fireVolumeRight[2] += (1.0f - distFallOffRight) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit);
398  }
399  else if (fs.Size.X > FireSoundMediumLimit)
400  {
401  fireVolumeRight[1] += (1.0f - distFallOffRight) * ((fs.Size.X - FireSoundMediumLimit) / FireSoundMediumLimit);
402  }
403  }
404  }
405  }
406  }
407  }
408 
409  private static void UpdateRandomAmbience(float deltaTime)
410  {
411  if (ambientSoundTimer > 0.0f)
412  {
413  ambientSoundTimer -= deltaTime;
414  }
415  else
416  {
417  PlaySound(
418  "ambient",
419  new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y) + Rand.Vector(100.0f),
420  Rand.Range(0.5f, 1.0f),
421  1000.0f);
422 
423  ambientSoundTimer = Rand.Range(ambientSoundInterval.X, ambientSoundInterval.Y);
424  }
425  }
426 
427  private static void UpdateHullSounds(float deltaTime)
428  {
429  if (hullSoundChannel != null && hullSoundChannel.IsPlaying && hullSoundSource != null)
430  {
431  hullSoundChannel.Position = new Vector3(hullSoundSource.WorldPosition, 0.0f);
432  hullSoundChannel.Gain = GetHullSoundVolume(hullSoundSource.Submarine);
433  }
434 
435  if (hullSoundTimer > 0.0f)
436  {
437  hullSoundTimer -= deltaTime;
438  }
439  else
440  {
441  if (!Level.IsLoadedFriendlyOutpost && Character.Controlled?.CurrentHull?.Submarine is Submarine sub &&
442  sub.Info != null && !sub.Info.IsOutpost)
443  {
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);
447  }
448  else
449  {
450  hullSoundTimer = 5.0f;
451  }
452  }
453 
454  static float GetHullSoundVolume(Submarine sub)
455  {
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);
458  }
459  }
460 
461  public static Sound GetSound(string soundTag)
462  {
463  var matchingSounds = SoundPrefab.Prefabs.Where(p => p.ElementName == soundTag);
464  if (!matchingSounds.Any()) return null;
465 
466  return matchingSounds.GetRandomUnsynced().Sound;
467  }
468 
472  public static SoundChannel PlaySound(string soundTag, float volume = 1.0f)
473  {
474  var sound = GetSound(soundTag);
475  return sound?.Play(volume);
476  }
477 
481  public static SoundChannel PlaySound(string soundTag, Vector2 position, float? volume = null, float? range = null, Hull hullGuess = null)
482  {
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);
486  }
487 
488  public static SoundChannel PlaySound(Sound sound, Vector2 position, float? volume = null, float? range = null, float? freqMult = null, Hull hullGuess = null, bool ignoreMuffling = false)
489  {
490  if (sound == null)
491  {
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);
494  return null;
495  }
496 
497  float far = range ?? sound.BaseFar;
498 
499  if (Vector2.DistanceSquared(new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y), position) > far * far)
500  {
501  return null;
502  }
503  bool muffle = !ignoreMuffling && ShouldMuffleSound(Character.Controlled, position, far, hullGuess);
504  return sound.Play(volume ?? sound.BaseGain, far, freqMult ?? 1.0f, position, muffle: muffle);
505  }
506 
507  public static void DisposeDisabledMusic()
508  {
509  bool musicDisposed = false;
510  for (int i = 0; i < currentMusic.Length; i++)
511  {
512  var music = currentMusic[i];
513  if (music is null) { continue; }
514 
515  if (!SoundPrefab.Prefabs.Contains(music))
516  {
517  musicChannel[i].Dispose();
518  musicDisposed = true;
519  currentMusic[i] = null;
520  }
521  }
522 
523  for (int i = 0; i < targetMusic.Length; i++)
524  {
525  var music = targetMusic[i];
526  if (music is null) { continue; }
527 
528  if (!SoundPrefab.Prefabs.Contains(music))
529  {
530  targetMusic[i] = null;
531  }
532  }
533 
534  if (musicDisposed) { Thread.Sleep(60); }
535  }
536 
537  public static void ForceMusicUpdate()
538  {
539  updateMusicTimer = 0.0f;
540  }
541 
542  private static void UpdateMusic(float deltaTime)
543  {
544  if (musicClips == null || (GameMain.SoundManager?.Disabled ?? true)) { return; }
545 
546  if (OverrideMusicType != null && OverrideMusicDuration.HasValue)
547  {
548  OverrideMusicDuration -= deltaTime;
549  if (OverrideMusicDuration <= 0.0f)
550  {
551  OverrideMusicType = Identifier.Empty;
552  OverrideMusicDuration = null;
553  }
554  }
555 
556  int noiseLoopIndex = 1;
557 
558  updateMusicTimer -= deltaTime;
559  if (updateMusicTimer <= 0.0f)
560  {
561  //find appropriate music for the current situation
562  Identifier currentMusicType = GetCurrentMusicType();
563  float currentIntensity = GameMain.GameSession?.EventManager != null ?
564  GameMain.GameSession.EventManager.MusicIntensity * 100.0f : 0.0f;
565 
566  IEnumerable<BackgroundMusic> suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity);
567  int mainTrackIndex = 0;
568  if (suitableMusic.None())
569  {
570  targetMusic[mainTrackIndex] = null;
571  }
572  //switch the music if nothing playing atm or the currently playing clip is not suitable anymore
573  else if (targetMusic[mainTrackIndex] == null || currentMusic[mainTrackIndex] == null || !currentMusic[mainTrackIndex].IsPlaying() || !suitableMusic.Any(m => m == currentMusic[mainTrackIndex]))
574  {
575  if (currentMusicType == "default")
576  {
577  if (previousDefaultMusic == null)
578  {
579  targetMusic[mainTrackIndex] = previousDefaultMusic = suitableMusic.GetRandomUnsynced();
580  }
581  else
582  {
583  targetMusic[mainTrackIndex] = previousDefaultMusic;
584  }
585  }
586  else
587  {
588  targetMusic[mainTrackIndex] = suitableMusic.GetRandomUnsynced();
589  }
590  }
591 
592  if (Level.Loaded != null && (Level.Loaded.Type == LevelData.LevelType.LocationConnection || Level.Loaded.GenerationParams.PlayNoiseLoopInOutpostLevel))
593  {
594  Identifier biome = Level.Loaded.LevelData.Biome.Identifier;
595  if (Level.Loaded.IsEndBiome && GameMain.GameSession?.Campaign is CampaignMode campaign)
596  {
597  //don't play end biome music in the path leading up to the end level(s)
598  if (!campaign.Map.EndLocations.Contains(Level.Loaded.StartLocation))
599  {
600  biome = Level.Loaded.StartLocation.Biome.Identifier;
601  }
602  }
603 
604  // Find background noise loop for the current biome
605  IEnumerable<BackgroundMusic> suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ?
606  GetSuitableMusicClips(biome, currentIntensity) :
607  Enumerable.Empty<BackgroundMusic>();
608  if (suitableNoiseLoops.Count() == 0)
609  {
610  targetMusic[noiseLoopIndex] = null;
611  }
612  // Switch the noise loop if nothing playing atm or the currently playing clip is not suitable anymore
613  else if (targetMusic[noiseLoopIndex] == null || currentMusic[noiseLoopIndex] == null || !suitableNoiseLoops.Any(m => m == currentMusic[noiseLoopIndex]))
614  {
615  targetMusic[noiseLoopIndex] = suitableNoiseLoops.GetRandomUnsynced();
616  }
617  }
618  else
619  {
620  targetMusic[noiseLoopIndex] = null;
621  }
622 
623  IEnumerable<BackgroundMusic> suitableTypeAmbiences = GetSuitableMusicClips($"{currentMusicType}ambience".ToIdentifier(), currentIntensity);
624  int typeAmbienceTrackIndex = 2;
625  if (suitableTypeAmbiences.None())
626  {
627  targetMusic[typeAmbienceTrackIndex] = null;
628  }
629  // Switch the type ambience if nothing playing atm or the currently playing clip is not suitable anymore
630  else if (targetMusic[typeAmbienceTrackIndex] == null || currentMusic[typeAmbienceTrackIndex] == null || !currentMusic[typeAmbienceTrackIndex].IsPlaying() || suitableTypeAmbiences.None(m => m == currentMusic[typeAmbienceTrackIndex]))
631  {
632  targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced();
633  }
634 
635  IEnumerable<BackgroundMusic> suitableIntensityMusic = Enumerable.Empty<BackgroundMusic>();
636  BackgroundMusic mainTrack = targetMusic[mainTrackIndex];
637  if (mainTrack is not { MuteIntensityTracks: true } && Screen.Selected == GameMain.GameScreen)
638  {
639  float intensity = currentIntensity;
640  if (mainTrack?.ForceIntensityTrack != null)
641  {
642  intensity = mainTrack.ForceIntensityTrack.Value;
643  }
644  suitableIntensityMusic = GetSuitableMusicClips("intensity".ToIdentifier(), intensity);
645  }
646  //get the appropriate intensity layers for current situation
647  int intensityTrackStartIndex = 3;
648  for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++)
649  {
650  //disable targetmusics that aren't suitable anymore
651  if (targetMusic[i] != null && !suitableIntensityMusic.Any(m => m == targetMusic[i]))
652  {
653  targetMusic[i] = null;
654  }
655  }
656 
657  foreach (BackgroundMusic intensityMusic in suitableIntensityMusic)
658  {
659  //already playing, do nothing
660  if (targetMusic.Any(m => m != null && m == intensityMusic)) { continue; }
661 
662  for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++)
663  {
664  if (targetMusic[i] == null)
665  {
666  targetMusic[i] = intensityMusic;
667  break;
668  }
669  }
670  }
671 
672  LogCurrentMusic();
673  updateMusicTimer = UpdateMusicInterval;
674  }
675 
676  int activeTrackCount = targetMusic.Count(m => m != null);
677  for (int i = 0; i < MaxMusicChannels; i++)
678  {
679  //nothing should be playing on this channel
680  if (targetMusic[i] == null)
681  {
682  if (musicChannel[i] != null && musicChannel[i].IsPlaying)
683  {
684  //mute the channel
685  musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, 0.0f, MusicLerpSpeed * deltaTime);
686  if (musicChannel[i].Gain < 0.01f) { DisposeMusicChannel(i); }
687  }
688  }
689  //something should be playing, but the targetMusic is invalid
690  else if (!musicClips.Any(mc => mc == targetMusic[i]))
691  {
692  targetMusic[i] = GetSuitableMusicClips(targetMusic[i].Type, 0.0f).GetRandomUnsynced();
693  }
694  //something should be playing, but the channel is playing nothing or an incorrect clip
695  else if (currentMusic[i] == null || targetMusic[i] != currentMusic[i])
696  {
697  //something playing -> mute it first
698  if (musicChannel[i] != null && musicChannel[i].IsPlaying)
699  {
700  musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, 0.0f, MusicLerpSpeed * deltaTime);
701  if (musicChannel[i].Gain < 0.01f) { DisposeMusicChannel(i); }
702  }
703  //channel free now, start playing the correct clip
704  if (currentMusic[i] == null || (musicChannel[i] == null || !musicChannel[i].IsPlaying))
705  {
706  DisposeMusicChannel(i);
707 
708  currentMusic[i] = targetMusic[i];
709  musicChannel[i] = currentMusic[i].Sound.Play(0.0f, i == noiseLoopIndex ? "default" : "music");
710  if (targetMusic[i].ContinueFromPreviousTime)
711  {
712  musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime;
713  }
714  else if (targetMusic[i].StartFromRandomTime)
715  {
716  musicChannel[i].StreamSeekPos =
717  (int)(musicChannel[i].MaxStreamSeekPos * Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced));
718  }
719  musicChannel[i].Looping = true;
720  }
721  }
722  else
723  {
724  //playing something, lerp volume up
725  if (musicChannel[i] == null || !musicChannel[i].IsPlaying)
726  {
727  musicChannel[i]?.Dispose();
728  musicChannel[i] = currentMusic[i].Sound.Play(0.0f, i == noiseLoopIndex ? "default" : "music");
729  musicChannel[i].Looping = true;
730  }
731  float targetGain = targetMusic[i].Volume;
732  if (targetMusic[i].DuckVolume)
733  {
734  targetGain *= (float)Math.Sqrt(1.0f / activeTrackCount);
735  }
736  musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, targetGain, MusicLerpSpeed * deltaTime);
737  }
738  }
739  }
740 
741  private static double lastMusicLogTime;
742  const double MusicLogInterval = 60.0;
743  private static void LogCurrentMusic()
744  {
745  if (Screen.Selected != GameMain.GameScreen) { return; }
746  if (Timing.TotalTime < lastMusicLogTime + MusicLogInterval) { return; }
747  for (int i = 0; i < musicChannel.Length; i++)
748  {
749  if (musicChannel[i] != null &&
750  musicChannel[i].IsPlaying &&
751  musicChannel[i].Sound?.Filename != null)
752  {
753  GameAnalyticsManager.AddDesignEvent(
754  "BackgroundMusic:" +
755  Path.GetFileNameWithoutExtension(musicChannel[i].Sound.Filename.Replace(":", string.Empty).Replace(" ", string.Empty)));
756  }
757  }
758  lastMusicLogTime = Timing.TotalTime;
759  }
760 
761  private static void DisposeMusicChannel(int index)
762  {
763  var clip = musicClips.FirstOrDefault(m => m.Sound == musicChannel[index]?.Sound);
764  if (clip != null)
765  {
766  if (clip.ContinueFromPreviousTime) { clip.PreviousTime = musicChannel[index].StreamSeekPos; }
767  }
768 
769  musicChannel[index]?.Dispose(); musicChannel[index] = null;
770  currentMusic[index] = null;
771  }
772 
773  private static IEnumerable<BackgroundMusic> GetSuitableMusicClips(Identifier musicType, float currentIntensity)
774  {
775  return musicClips.Where(music => IsSuitableMusicClip(music, musicType, currentIntensity));
776  }
777 
778  private static bool IsSuitableMusicClip(BackgroundMusic music, Identifier musicType, float currentIntensity)
779  {
780  return
781  music != null &&
782  music.Type == musicType &&
783  currentIntensity >= music.IntensityRange.X &&
784  currentIntensity <= music.IntensityRange.Y;
785  }
786 
787  private static Identifier GetCurrentMusicType()
788  {
789  if (OverrideMusicType != null) { return OverrideMusicType; }
790 
791  if (Screen.Selected == null) { return "menu".ToIdentifier(); }
792 
793  if (Screen.Selected is { IsEditor: true } || GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected == GameMain.NetLobbyScreen)
794  {
795  return "editor".ToIdentifier();
796  }
797 
798  if (Screen.Selected != GameMain.GameScreen)
799  {
800  previousDefaultMusic = null;
801  return (firstTimeInMainMenu ? "menu" : "default").ToIdentifier();
802  }
803 
804  firstTimeInMainMenu = false;
805 
806  if (GameMain.GameSession != null)
807  {
808  foreach (var mission in GameMain.GameSession.Missions)
809  {
810  var missionMusic = mission.GetOverrideMusicType();
811  if (!missionMusic.IsEmpty) { return missionMusic; }
812  }
813  }
814 
815  if (Character.Controlled != null)
816  {
817  if (Level.Loaded != null && Level.Loaded.Ruins != null &&
818  Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition)))
819  {
820  return "ruins".ToIdentifier();
821  }
822 
823  if (Character.Controlled.Submarine?.Info?.IsWreck ?? false)
824  {
825  return "wreck".ToIdentifier();
826  }
827 
828  if (Level.IsLoadedOutpost)
829  {
830  // Only return music type for location types which have music tracks defined
831  var locationType = Level.Loaded.StartLocation?.Type?.Identifier;
832  if (locationType.HasValue && locationType != Identifier.Empty && musicClips.Any(c => c.Type == locationType))
833  {
834  return locationType.Value;
835  }
836  }
837  }
838 
839  if (Level.Loaded is { IsEndBiome: true })
840  {
841  return "endlevel".ToIdentifier();
842  }
843 
844  Submarine targetSubmarine = Character.Controlled?.Submarine;
845  if (targetSubmarine != null && targetSubmarine.AtDamageDepth)
846  {
847  return "deep".ToIdentifier();
848  }
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)
851  {
852  return "deep".ToIdentifier();
853  }
854 
855  if (targetSubmarine != null)
856  {
857  float floodedArea = 0.0f;
858  float totalArea = 0.0f;
859  foreach (Hull hull in Hull.HullList)
860  {
861  if (hull.Submarine != targetSubmarine) { continue; }
862  floodedArea += hull.WaterVolume;
863  totalArea += hull.Volume;
864  }
865 
866  if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return "flooded".ToIdentifier(); }
867  }
868 
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));
872 
873  if (anyMonsterMusicAvailable)
874  {
875  float enemyDistThreshold = 5000.0f;
876  if (targetSubmarine != null)
877  {
878  enemyDistThreshold = Math.Max(enemyDistThreshold, Math.Max(targetSubmarine.Borders.Width, targetSubmarine.Borders.Height) * 2.0f);
879  }
880  foreach (Character character in Character.CharacterList)
881  {
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; }
885 
886  if (targetSubmarine != null)
887  {
888  if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold)
889  {
890  return "monster".ToIdentifier();
891  }
892  }
893  else if (Character.Controlled != null)
894  {
895  if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold)
896  {
897  return "monster".ToIdentifier();
898  }
899  }
900  }
901  }
902 
903 
904  if (GameMain.GameSession != null)
905  {
906  if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
907  {
908  return "levelend".ToIdentifier();
909  }
910  if (GameMain.GameSession.RoundDuration < 120.0 &&
911  Level.Loaded?.Type == LevelData.LevelType.LocationConnection)
912  {
913  return "start".ToIdentifier();
914  }
915  }
916 
917  return "default".ToIdentifier();
918  }
919 
920  public static bool ShouldMuffleSound(Character listener, Vector2 soundWorldPos, float range, Hull hullGuess)
921  {
922  if (listener == null) return false;
923 
924  float lowpassHFGain = 1.0f;
925  AnimController animController = listener.AnimController;
926  if (animController.HeadInWater)
927  {
928  lowpassHFGain = 0.2f;
929  }
930  lowpassHFGain *= Character.Controlled.LowPassMultiplier;
931  if (lowpassHFGain < 0.5f) return true;
932 
933  Hull targetHull = Hull.FindHull(soundWorldPos, hullGuess, true);
934  if (listener.CurrentHull == null || targetHull == null)
935  {
936  return listener.CurrentHull != targetHull;
937  }
938  Vector2 soundPos = soundWorldPos;
939  if (targetHull.Submarine != null)
940  {
941  soundPos += -targetHull.Submarine.WorldPosition + targetHull.Submarine.HiddenSubPosition;
942  }
943  return listener.CurrentHull.GetApproximateDistance(listener.Position, soundPos, targetHull, range) > range;
944  }
945 
946  public static void PlaySplashSound(Vector2 worldPosition, float strength)
947  {
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));
952  }
953 
954  public static void PlayDamageSound(string damageType, float damage, PhysicsBody body)
955  {
956  Vector2 bodyPosition = body.DrawPosition;
957  PlayDamageSound(damageType, damage, bodyPosition, 800.0f);
958  }
959 
960  public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable<Identifier> tags = null, float gain = 1.0f)
961  {
962  var suitableSounds = damageSounds.Where(s =>
963  s.DamageType == damageType &&
964  (s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag))));
965 
966  //if the damage is too low for any sound, don't play anything
967  if (suitableSounds.All(d => damage < d.DamageRange.X)) { return; }
968 
969  //allow the damage to differ by 10 from the configured damage range,
970  //so the same amount of damage doesn't always play the same sound
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));
974 
975  var damageSound = suitableSounds.GetRandomUnsynced();
976  damageSound?.Sound?.Play(gain, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null));
977  }
978 
979  public static void PlayUISound(GUISoundType soundType)
980  {
981  GUISound.GUISoundPrefabs
982  .Where(s => s.Type == soundType)
983  .GetRandomUnsynced()?.Sound?.Play(null, "ui");
984  }
985 
986  public static void PlayUISound(GUISoundType? soundType)
987  {
988  if (soundType.HasValue)
989  {
990  PlayUISound(soundType.Value);
991  }
992  }
993  }
994 }
Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, ContentXElement xElement=null, bool getFullPath=true)
Definition: Sound.cs:65
virtual SoundChannel Play(float gain, float range, Vector2 position, bool muffle=false)
Definition: Sound.cs:102
GUISoundType
Definition: GUI.cs:21
@ Character
Characters only