2 using System.Threading;
3 using System.Collections.Generic;
6 using Microsoft.Xna.Framework;
27 private IntPtr alcDevice;
28 private IntPtr alcContext;
37 private readonly List<Sound> loadedSounds;
41 private readonly
object threadDeathMutex =
new object();
47 private Thread updateChannelsThread;
49 private Vector3 listenerPosition;
52 get {
return listenerPosition; }
56 listenerPosition = value;
61 throw new Exception(
"Failed to set listener position: " +
Al.
GetErrorString(alError));
66 private readonly
float[] listenerOrientation =
new float[6];
69 get {
return new Vector3(listenerOrientation[0], listenerOrientation[1], listenerOrientation[2]); }
73 listenerOrientation[0] = value.X; listenerOrientation[1] = value.Y; listenerOrientation[2] = value.Z;
78 throw new Exception(
"Failed to set listener target vector: " +
Al.
GetErrorString(alError));
84 get {
return new Vector3(listenerOrientation[3], listenerOrientation[4], listenerOrientation[5]); }
88 listenerOrientation[3] = value.X; listenerOrientation[4] = value.Y; listenerOrientation[5] = value.Z;
93 throw new Exception(
"Failed to set listener up vector: " +
Al.
GetErrorString(alError));
98 private float listenerGain;
101 get {
return listenerGain; }
105 if (Math.Abs(
ListenerGain - value) < 0.001f) {
return; }
106 listenerGain = value;
111 throw new Exception(
"Failed to set listener gain: " +
Al.
GetErrorString(alError));
121 float aggregateAmplitude = 0.0f;
126 for (
int i = 0; i < 2; i++)
128 foreach (
SoundChannel soundChannel
in playingChannels[i].Where(ch => ch !=
null))
131 amplitude *= soundChannel.
Gain;
133 if (dist > soundChannel.
Near)
135 amplitude *= 1.0f - Math.Min(1.0f, (dist - soundChannel.
Near) / (soundChannel.
Far - soundChannel.
Near));
137 aggregateAmplitude += amplitude;
140 return aggregateAmplitude;
146 private float voipAttenuatedGain;
147 private double lastAttenuationTime;
150 get {
return voipAttenuatedGain; }
153 lastAttenuationTime = Timing.TotalTime;
154 voipAttenuatedGain = value;
160 get {
return loadedSounds.Count; }
164 get {
return loadedSounds.Select(s => s.Filename).Distinct().Count(); }
167 private class CategoryModifier
169 public float[] GainMultipliers;
172 public CategoryModifier(
int gainMultiplierIndex,
float gain,
bool muffle)
175 GainMultipliers =
new float[gainMultiplierIndex + 1];
176 for (
int i = 0; i < GainMultipliers.Length; i++)
178 if (i == gainMultiplierIndex)
180 GainMultipliers[i] = gain;
184 GainMultipliers[i] = 1.0f;
189 public void SetGainMultiplier(
int index,
float gain)
191 if (GainMultipliers.Length < index + 1)
193 int oldLength = GainMultipliers.Length;
194 Array.Resize(ref GainMultipliers, index + 1);
195 for (
int i = oldLength; i < GainMultipliers.Length; i++)
197 GainMultipliers[i] = 1.0f;
200 GainMultipliers[index] = gain;
204 private readonly Dictionary<string, CategoryModifier> categoryModifiers =
new Dictionary<string, CategoryModifier>();
208 loadedSounds =
new List<Sound>();
209 updateChannelsThread =
null;
215 string deviceName = GameSettings.CurrentConfig.Audio.AudioOutputDevice;
217 if (
string.IsNullOrEmpty(deviceName))
224 if (audioDeviceNames.Any() && !audioDeviceNames.Any(n => n.Equals(deviceName, StringComparison.OrdinalIgnoreCase)))
226 deviceName = audioDeviceNames[0];
229 if (GameSettings.CurrentConfig.Audio.AudioOutputDevice != deviceName)
231 SetAudioOutputDevice(deviceName);
243 private static void SetAudioOutputDevice(
string deviceName)
245 var config = GameSettings.CurrentConfig;
246 config.Audio.AudioOutputDevice = deviceName;
247 GameSettings.SetCurrentConfig(config);
252 ReleaseResources(
true);
254 DebugConsole.NewMessage($
"Attempting to open ALC device \"{deviceName}\"");
256 alcDevice = IntPtr.Zero;
258 for (
int i = 0; i < 3; i++)
260 alcDevice =
Alc.OpenDevice(deviceName);
261 if (alcDevice == IntPtr.Zero)
264 DebugConsole.NewMessage($
"ALC device initialization attempt #{i + 1} failed: device is null (error code {Alc.GetErrorString(alcError)})");
265 if (!
string.IsNullOrEmpty(deviceName))
268 DebugConsole.NewMessage($
"Switching to default device...");
276 DebugConsole.NewMessage($
"ALC device initialization attempt #{i + 1} failed: error code {Alc.GetErrorString(alcError)}");
280 DebugConsole.NewMessage($
"Failed to close ALC device");
282 alcDevice = IntPtr.Zero;
290 if (alcDevice == IntPtr.Zero)
292 DebugConsole.ThrowError(
"ALC device creation failed too many times!");
301 DebugConsole.ThrowError(
"Error determining if disconnect can be detected: " + alcError.ToString() +
". Disabling audio playback...");
308 int[] alcContextAttrs =
new int[] { };
310 if (alcContext ==
null)
312 DebugConsole.ThrowError(
"Failed to create an ALC context! (error code: " +
Alc.
GetError(alcDevice).ToString() +
"). Disabling audio playback...");
319 DebugConsole.ThrowError(
"Failed to assign the current ALC context! (error code: " +
Alc.
GetError(alcDevice).ToString() +
"). Disabling audio playback...");
327 DebugConsole.ThrowError(
"Error after assigning ALC context: " +
Alc.
GetErrorString(alcError) +
". Disabling audio playback...");
337 DebugConsole.ThrowError(
"Error setting distance model: " +
Al.
GetErrorString(alError) +
". Disabling audio playback...");
356 if (!File.Exists(filename))
358 throw new System.IO.FileNotFoundException(
"Sound file \"" + filename +
"\" doesn't exist!");
362 System.Diagnostics.Stopwatch sw =
new System.Diagnostics.Stopwatch();
368 loadedSounds.Add(newSound);
372 System.Diagnostics.Debug.WriteLine($
"Loaded sound \"{filename}\" ({sw.ElapsedMilliseconds} ms).");
382 if (!File.Exists(filePath))
384 throw new System.IO.FileNotFoundException($
"Sound file \"{filePath}\" doesn't exist! Content package \"{(element.ContentPackage?.Name ?? "Unknown")}\".");
387 var newSound =
new OggSound(
this, filePath, stream, xElement: element)
389 BaseGain = element.GetAttributeFloat(
"volume", 1.0f)
392 newSound.BaseNear = range * 0.4f;
393 newSound.BaseFar = range;
397 loadedSounds.Add(newSound);
404 if (
Disabled || ind < 0 || ind >= playingChannels[(
int)poolIndex].Length)
return null;
405 return playingChannels[(int)poolIndex][ind];
410 if (
Disabled || srcInd < 0 || srcInd >= sourcePools[(
int)poolIndex].ALSources.Length)
return 0;
412 if (!
Al.
IsSource(sourcePools[(
int)poolIndex].ALSources[srcInd]))
414 throw new Exception(
"alSources[" + srcInd.ToString() +
"] is invalid!");
417 return sourcePools[(int)poolIndex].ALSources[srcInd];
428 lock (playingChannels[poolIndex])
430 for (
int i = 0; i < playingChannels[poolIndex].Length; i++)
432 if (playingChannels[poolIndex][i] ==
null || !playingChannels[poolIndex][i].
IsPlaying)
434 if (playingChannels[poolIndex][i] !=
null) { playingChannels[poolIndex][i].
Dispose(); }
435 playingChannels[poolIndex][i] = newChannel;
446 public void DebugSource(
int ind)
448 for (
int i = 0; i < sourcePools[0].ALSources.Length; i++)
461 for (
int i = 0; i < playingChannels[(int)sound.
SourcePoolIndex].Length; i++)
466 if (playingChannels[(
int)sound.
SourcePoolIndex][i].IsPlaying)
return true;
479 for (
int i = 0; i < playingChannels[(int)sound.
SourcePoolIndex].Length; i++)
484 if (playingChannels[(
int)sound.
SourcePoolIndex][i].IsPlaying) { count++; };
496 for (
int i = 0; i < playingChannels[(int)sound.
SourcePoolIndex].Length; i++)
513 for (
int i = 0; i < playingChannels[(int)sound.
SourcePoolIndex].Length; i++)
528 for (
int i = 0; i < loadedSounds.Count; i++)
530 if (loadedSounds[i] == sound)
532 loadedSounds.RemoveAt(i);
543 int index = loadedSounds.IndexOf(sound);
546 loadedSounds.SiftElement(index, pos);
554 category = category.ToLower();
555 lock (categoryModifiers)
557 if (!categoryModifiers.ContainsKey(category))
559 categoryModifiers.Add(category,
new CategoryModifier(index, gain,
false));
563 categoryModifiers[category].SetGainMultiplier(index, gain);
567 for (
int i = 0; i < playingChannels.Length; i++)
569 lock (playingChannels[i])
571 for (
int j = 0; j < playingChannels[i].Length; j++)
573 if (playingChannels[i][j] !=
null && playingChannels[i][j].
IsPlaying)
575 playingChannels[i][j].Gain = playingChannels[i][j].Gain;
585 category = category.ToLower();
586 lock (categoryModifiers)
588 if (categoryModifiers ==
null || !categoryModifiers.TryGetValue(category, out CategoryModifier categoryModifier)) {
return 1.0f; }
591 float accumulatedMultipliers = 1.0f;
592 for (
int i = 0; i < categoryModifier.GainMultipliers.Length; i++)
594 accumulatedMultipliers *= categoryModifier.GainMultipliers[i];
596 return accumulatedMultipliers;
600 return categoryModifier.GainMultipliers[index];
609 category = category.ToLower();
610 lock (categoryModifiers)
612 if (!categoryModifiers.ContainsKey(category))
614 categoryModifiers.Add(category,
new CategoryModifier(0, 1.0f, muffle));
618 categoryModifiers[category].Muffle = muffle;
622 for (
int i = 0; i < playingChannels.Length; i++)
624 lock (playingChannels[i])
626 for (
int j = 0; j < playingChannels[i].Length; j++)
628 if (playingChannels[i][j] !=
null && playingChannels[i][j].
IsPlaying)
630 if (playingChannels[i][j]?.Category.ToLower() == category) { playingChannels[i][j].Muffled = muffle; }
641 category = category.ToLower();
642 lock (categoryModifiers)
644 if (categoryModifiers ==
null || !categoryModifiers.TryGetValue(category, out CategoryModifier categoryModifier)) {
return false; }
645 return categoryModifier.Muffle;
659 throw new Exception(
"Failed to determine if device is connected: " + alcError.ToString());
662 if (isConnected == 0)
670 DebugConsole.ThrowError(
"Playback device has been disconnected. You can select another available device in the settings.");
671 SetAudioOutputDevice(
"<disconnected>");
677 if (
GameMain.
Client !=
null && GameSettings.CurrentConfig.Audio.VoipAttenuationEnabled)
679 if (Timing.TotalTime > lastAttenuationTime+0.2)
681 voipAttenuatedGain = voipAttenuatedGain * 0.9f + 0.1f;
686 voipAttenuatedGain = 1.0f;
693 if (GameSettings.CurrentConfig.Audio.DynamicRangeCompressionEnabled)
695 float targetGain = (Math.Min(1.0f, 1.0f /
PlaybackAmplitude) - 1.0f) * 0.5f + 1.0f;
712 if (updateChannelsThread ==
null || updateChannelsThread.ThreadState.HasFlag(ThreadState.Stopped))
714 bool startedStreamThread =
false;
715 for (
int i = 0; i < playingChannels.Length; i++)
717 lock (playingChannels[i])
719 for (
int j = 0; j < playingChannels[i].Length; j++)
721 if (playingChannels[i][j] ==
null) {
continue; }
722 if (playingChannels[i][j].IsStream && playingChannels[i][j].
IsPlaying)
725 startedStreamThread =
true;
727 if (startedStreamThread) {
break; }
730 if (startedStreamThread) {
break; }
750 bool isUpdateChannelsThreadDying;
751 lock (threadDeathMutex)
753 isUpdateChannelsThreadDying = !needsUpdateChannels;
755 if (updateChannelsThread ==
null || updateChannelsThread.ThreadState.HasFlag(ThreadState.Stopped) || isUpdateChannelsThreadDying)
757 if (updateChannelsThread !=
null && !updateChannelsThread.Join(1000))
759 DebugConsole.ThrowError(
"SoundManager.UpdateChannels thread join timed out!");
761 needsUpdateChannels =
true;
762 updateChannelsThread =
new Thread(UpdateChannels)
764 Name =
"SoundManager.UpdateChannels Thread",
767 updateChannelsThread.Start();
771 private bool needsUpdateChannels =
false;
772 private ManualResetEvent updateChannelsMre =
null;
777 private void UpdateChannels()
779 updateChannelsMre =
new ManualResetEvent(
false);
780 bool killThread =
false;
784 for (
int i = 0; i < playingChannels.Length; i++)
786 lock (playingChannels[i])
788 for (
int j = 0; j < playingChannels[i].Length; j++)
790 if (playingChannels[i][j] ==
null) {
continue; }
791 if (playingChannels[i][j].IsStream)
796 playingChannels[i][j].UpdateStream();
800 playingChannels[i][j].Dispose();
801 playingChannels[i][j] =
null;
804 else if (playingChannels[i][j].FadingOutAndDisposing)
807 playingChannels[i][j].Gain -= 0.1f;
808 if (playingChannels[i][j].Gain <= 0.0f)
810 playingChannels[i][j].Dispose();
811 playingChannels[i][j] =
null;
817 updateChannelsMre.WaitOne(10);
818 updateChannelsMre.Reset();
819 lock (threadDeathMutex)
821 needsUpdateChannels = !killThread;
828 updateChannelsMre?.Set();
831 private void ReloadSounds()
833 for (
int i = loadedSounds.Count - 1; i >= 0; i--)
835 loadedSounds[i].InitializeAlBuffers();
839 private void ReleaseResources(
bool keepSounds)
841 for (
int i = 0; i < playingChannels.Length; i++)
843 lock (playingChannels[i])
845 for (
int j = 0; j < playingChannels[i].Length; j++)
847 playingChannels[i][j]?.Dispose();
852 updateChannelsThread?.Join();
853 for (
int i = loadedSounds.Count - 1; i >= 0; i--)
857 loadedSounds[i].DeleteAlBuffers();
861 loadedSounds[i].Dispose();
867 SoundBuffers.ClearPool();
874 ReleaseResources(
false);
878 throw new Exception(
"Failed to detach the current ALC context! (error code: " +
Alc.
GetError(alcDevice).ToString() +
")");
885 throw new Exception(
"Failed to close ALC device!");
float GetAttributeFloat(string key, float def)
ContentPath? GetAttributeContentPath(string key)
float???? CurrentAmplitude
virtual SoundManager.SourcePoolIndex SourcePoolIndex
IReadOnlyList< Sound > LoadedSounds
int CountPlayingInstances(Sound sound)
float GetCategoryGainMultiplier(string category, int index=-1)
bool GetCategoryMuffle(string category)
uint GetSourceFromIndex(SourcePoolIndex poolIndex, int srcInd)
void RemoveSound(Sound sound)
void MoveSoundToPosition(Sound sound, int pos)
const string SoundCategoryDefault
void InitUpdateChannelThread()
Initializes the thread that handles streaming audio and fading out and disposing channels that are no...
SoundChannel GetChannelFromSound(Sound sound)
float?? PlaybackAmplitude
const string SoundCategoryUi
bool IsPlaying(Sound sound)
const string SoundCategoryWaterAmbience
Sound LoadSound(ContentXElement element, bool stream=false, string overrideFilePath=null)
void SetCategoryGainMultiplier(string category, float gain, int index=0)
void SetCategoryMuffle(string category, bool muffle)
SoundChannel GetSoundChannelFromIndex(SourcePoolIndex poolIndex, int ind)
int UniqueLoadedSoundCount
const string SoundCategoryMusic
const string SoundCategoryVoip
Vector3 ListenerTargetVector
float CompressionDynamicRangeGain
void KillChannels(Sound sound)
int AssignFreeSourceToChannel(SoundChannel newChannel)
bool InitializeAlcDevice(string deviceName)
Sound LoadSound(string filename, bool stream=false)
const int LinearDistanceClamped
static void Listener3f(int param, float value1, float value2, float value3)
static void Listenerfv(int param, float[] values)
static bool IsSource(uint sid)
static void Sourcef(uint sid, int param, float value)
static void Listenerf(int param, float value)
static string GetErrorString(int error)
static void DistanceModel(int distanceModel)
static bool MakeContextCurrent(IntPtr context)
static IReadOnlyList< string > GetStringList(IntPtr device, int param)
static int GetError(IntPtr device)
static void DestroyContext(IntPtr context)
const int AllDevicesSpecifier
static bool CloseDevice(IntPtr device)
static string GetErrorString(int errorCode)
static void GetInteger(IntPtr device, int param, out int data)
static IntPtr CreateContext(IntPtr device, int[] attrList)
const int DefaultDeviceSpecifier
static bool IsExtensionPresent(IntPtr device, string extname)
static string GetString(IntPtr device, int param)