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