2 using System.Threading;
3 using System.Collections.Generic;
6 using Microsoft.Xna.Framework;
22 private IntPtr alcDevice;
23 private IntPtr alcContext;
32 private readonly List<Sound> loadedSounds;
36 private readonly
object threadDeathMutex =
new object();
42 private Thread updateChannelsThread;
44 private Vector3 listenerPosition;
47 get {
return listenerPosition; }
51 listenerPosition = value;
56 throw new Exception(
"Failed to set listener position: " +
Al.
GetErrorString(alError));
61 private readonly
float[] listenerOrientation =
new float[6];
64 get {
return new Vector3(listenerOrientation[0], listenerOrientation[1], listenerOrientation[2]); }
68 listenerOrientation[0] = value.X; listenerOrientation[1] = value.Y; listenerOrientation[2] = value.Z;
73 throw new Exception(
"Failed to set listener target vector: " +
Al.
GetErrorString(alError));
79 get {
return new Vector3(listenerOrientation[3], listenerOrientation[4], listenerOrientation[5]); }
83 listenerOrientation[3] = value.X; listenerOrientation[4] = value.Y; listenerOrientation[5] = value.Z;
88 throw new Exception(
"Failed to set listener up vector: " +
Al.
GetErrorString(alError));
93 private float listenerGain;
96 get {
return listenerGain; }
100 if (Math.Abs(
ListenerGain - value) < 0.001f) {
return; }
101 listenerGain = value;
106 throw new Exception(
"Failed to set listener gain: " +
Al.
GetErrorString(alError));
116 float aggregateAmplitude = 0.0f;
121 for (
int i = 0; i < 2; i++)
123 foreach (
SoundChannel soundChannel
in playingChannels[i].Where(ch => ch !=
null))
126 amplitude *= soundChannel.
Gain;
128 if (dist > soundChannel.
Near)
130 amplitude *= 1.0f - Math.Min(1.0f, (dist - soundChannel.
Near) / (soundChannel.
Far - soundChannel.
Near));
132 aggregateAmplitude += amplitude;
135 return aggregateAmplitude;
141 private float voipAttenuatedGain;
142 private double lastAttenuationTime;
145 get {
return voipAttenuatedGain; }
148 lastAttenuationTime = Timing.TotalTime;
149 voipAttenuatedGain = value;
155 get {
return loadedSounds.Count; }
159 get {
return loadedSounds.Select(s => s.Filename).Distinct().Count(); }
162 private class CategoryModifier
164 public float[] GainMultipliers;
167 public CategoryModifier(
int gainMultiplierIndex,
float gain,
bool muffle)
170 GainMultipliers =
new float[gainMultiplierIndex + 1];
171 for (
int i = 0; i < GainMultipliers.Length; i++)
173 if (i == gainMultiplierIndex)
175 GainMultipliers[i] = gain;
179 GainMultipliers[i] = 1.0f;
184 public void SetGainMultiplier(
int index,
float gain)
186 if (GainMultipliers.Length < index + 1)
188 int oldLength = GainMultipliers.Length;
189 Array.Resize(ref GainMultipliers, index + 1);
190 for (
int i = oldLength; i < GainMultipliers.Length; i++)
192 GainMultipliers[i] = 1.0f;
195 GainMultipliers[index] = gain;
199 private readonly Dictionary<string, CategoryModifier> categoryModifiers =
new Dictionary<string, CategoryModifier>();
203 loadedSounds =
new List<Sound>();
204 updateChannelsThread =
null;
210 string deviceName = GameSettings.CurrentConfig.Audio.AudioOutputDevice;
212 if (
string.IsNullOrEmpty(deviceName))
219 if (audioDeviceNames.Any() && !audioDeviceNames.Any(n => n.Equals(deviceName, StringComparison.OrdinalIgnoreCase)))
221 deviceName = audioDeviceNames[0];
224 if (GameSettings.CurrentConfig.Audio.AudioOutputDevice != deviceName)
226 SetAudioOutputDevice(deviceName);
238 private static void SetAudioOutputDevice(
string deviceName)
240 var config = GameSettings.CurrentConfig;
241 config.Audio.AudioOutputDevice = deviceName;
242 GameSettings.SetCurrentConfig(config);
247 ReleaseResources(
true);
249 DebugConsole.NewMessage($
"Attempting to open ALC device \"{deviceName}\"");
251 alcDevice = IntPtr.Zero;
253 for (
int i = 0; i < 3; i++)
255 alcDevice =
Alc.OpenDevice(deviceName);
256 if (alcDevice == IntPtr.Zero)
259 DebugConsole.NewMessage($
"ALC device initialization attempt #{i + 1} failed: device is null (error code {Alc.GetErrorString(alcError)})");
260 if (!
string.IsNullOrEmpty(deviceName))
263 DebugConsole.NewMessage($
"Switching to default device...");
271 DebugConsole.NewMessage($
"ALC device initialization attempt #{i + 1} failed: error code {Alc.GetErrorString(alcError)}");
275 DebugConsole.NewMessage($
"Failed to close ALC device");
277 alcDevice = IntPtr.Zero;
285 if (alcDevice == IntPtr.Zero)
287 DebugConsole.ThrowError(
"ALC device creation failed too many times!");
296 DebugConsole.ThrowError(
"Error determining if disconnect can be detected: " + alcError.ToString() +
". Disabling audio playback...");
303 int[] alcContextAttrs =
new int[] { };
305 if (alcContext ==
null)
307 DebugConsole.ThrowError(
"Failed to create an ALC context! (error code: " +
Alc.
GetError(alcDevice).ToString() +
"). Disabling audio playback...");
314 DebugConsole.ThrowError(
"Failed to assign the current ALC context! (error code: " +
Alc.
GetError(alcDevice).ToString() +
"). Disabling audio playback...");
322 DebugConsole.ThrowError(
"Error after assigning ALC context: " +
Alc.
GetErrorString(alcError) +
". Disabling audio playback...");
332 DebugConsole.ThrowError(
"Error setting distance model: " +
Al.
GetErrorString(alError) +
". Disabling audio playback...");
351 if (!File.Exists(filename))
353 throw new System.IO.FileNotFoundException(
"Sound file \"" + filename +
"\" doesn't exist!");
359 loadedSounds.Add(newSound);
369 if (!File.Exists(filePath))
371 throw new System.IO.FileNotFoundException($
"Sound file \"{filePath}\" doesn't exist! Content package \"{(element.ContentPackage?.Name ?? "Unknown")}\".");
374 var newSound =
new OggSound(
this, filePath, stream, xElement: element)
376 BaseGain = element.GetAttributeFloat(
"volume", 1.0f)
379 newSound.BaseNear = range * 0.4f;
380 newSound.BaseFar = range;
384 loadedSounds.Add(newSound);
391 if (
Disabled || ind < 0 || ind >= playingChannels[(
int)poolIndex].Length)
return null;
392 return playingChannels[(int)poolIndex][ind];
397 if (
Disabled || srcInd < 0 || srcInd >= sourcePools[(
int)poolIndex].ALSources.Length)
return 0;
399 if (!
Al.
IsSource(sourcePools[(
int)poolIndex].ALSources[srcInd]))
401 throw new Exception(
"alSources[" + srcInd.ToString() +
"] is invalid!");
404 return sourcePools[(int)poolIndex].ALSources[srcInd];
415 lock (playingChannels[poolIndex])
417 for (
int i = 0; i < playingChannels[poolIndex].Length; i++)
419 if (playingChannels[poolIndex][i] ==
null || !playingChannels[poolIndex][i].
IsPlaying)
421 if (playingChannels[poolIndex][i] !=
null) { playingChannels[poolIndex][i].
Dispose(); }
422 playingChannels[poolIndex][i] = newChannel;
433 public void DebugSource(
int ind)
435 for (
int i = 0; i < sourcePools[0].ALSources.Length; i++)
448 for (
int i = 0; i < playingChannels[(int)sound.
SourcePoolIndex].Length; i++)
453 if (playingChannels[(
int)sound.
SourcePoolIndex][i].IsPlaying)
return true;
466 for (
int i = 0; i < playingChannels[(int)sound.
SourcePoolIndex].Length; i++)
471 if (playingChannels[(
int)sound.
SourcePoolIndex][i].IsPlaying) { count++; };
483 for (
int i = 0; i < playingChannels[(int)sound.
SourcePoolIndex].Length; i++)
500 for (
int i = 0; i < playingChannels[(int)sound.
SourcePoolIndex].Length; i++)
515 for (
int i = 0; i < loadedSounds.Count; i++)
517 if (loadedSounds[i] == sound)
519 loadedSounds.RemoveAt(i);
530 int index = loadedSounds.IndexOf(sound);
533 loadedSounds.SiftElement(index, pos);
541 category = category.ToLower();
542 lock (categoryModifiers)
544 if (!categoryModifiers.ContainsKey(category))
546 categoryModifiers.Add(category,
new CategoryModifier(index, gain,
false));
550 categoryModifiers[category].SetGainMultiplier(index, gain);
554 for (
int i = 0; i < playingChannels.Length; i++)
556 lock (playingChannels[i])
558 for (
int j = 0; j < playingChannels[i].Length; j++)
560 if (playingChannels[i][j] !=
null && playingChannels[i][j].
IsPlaying)
562 playingChannels[i][j].Gain = playingChannels[i][j].Gain;
572 category = category.ToLower();
573 lock (categoryModifiers)
575 if (categoryModifiers ==
null || !categoryModifiers.TryGetValue(category, out CategoryModifier categoryModifier)) {
return 1.0f; }
578 float accumulatedMultipliers = 1.0f;
579 for (
int i = 0; i < categoryModifier.GainMultipliers.Length; i++)
581 accumulatedMultipliers *= categoryModifier.GainMultipliers[i];
583 return accumulatedMultipliers;
587 return categoryModifier.GainMultipliers[index];
596 category = category.ToLower();
597 lock (categoryModifiers)
599 if (!categoryModifiers.ContainsKey(category))
601 categoryModifiers.Add(category,
new CategoryModifier(0, 1.0f, muffle));
605 categoryModifiers[category].Muffle = muffle;
609 for (
int i = 0; i < playingChannels.Length; i++)
611 lock (playingChannels[i])
613 for (
int j = 0; j < playingChannels[i].Length; j++)
615 if (playingChannels[i][j] !=
null && playingChannels[i][j].
IsPlaying)
617 if (playingChannels[i][j]?.Category.ToLower() == category) { playingChannels[i][j].Muffled = muffle; }
628 category = category.ToLower();
629 lock (categoryModifiers)
631 if (categoryModifiers ==
null || !categoryModifiers.TryGetValue(category, out CategoryModifier categoryModifier)) {
return false; }
632 return categoryModifier.Muffle;
646 throw new Exception(
"Failed to determine if device is connected: " + alcError.ToString());
649 if (isConnected == 0)
657 DebugConsole.ThrowError(
"Playback device has been disconnected. You can select another available device in the settings.");
658 SetAudioOutputDevice(
"<disconnected>");
664 if (
GameMain.
Client !=
null && GameSettings.CurrentConfig.Audio.VoipAttenuationEnabled)
666 if (Timing.TotalTime > lastAttenuationTime+0.2)
668 voipAttenuatedGain = voipAttenuatedGain * 0.9f + 0.1f;
673 voipAttenuatedGain = 1.0f;
680 if (GameSettings.CurrentConfig.Audio.DynamicRangeCompressionEnabled)
682 float targetGain = (Math.Min(1.0f, 1.0f /
PlaybackAmplitude) - 1.0f) * 0.5f + 1.0f;
699 if (updateChannelsThread ==
null || updateChannelsThread.ThreadState.HasFlag(ThreadState.Stopped))
701 bool startedStreamThread =
false;
702 for (
int i = 0; i < playingChannels.Length; i++)
704 lock (playingChannels[i])
706 for (
int j = 0; j < playingChannels[i].Length; j++)
708 if (playingChannels[i][j] ==
null) {
continue; }
709 if (playingChannels[i][j].IsStream && playingChannels[i][j].
IsPlaying)
712 startedStreamThread =
true;
714 if (startedStreamThread) {
break; }
717 if (startedStreamThread) {
break; }
737 bool isUpdateChannelsThreadDying;
738 lock (threadDeathMutex)
740 isUpdateChannelsThreadDying = !needsUpdateChannels;
742 if (updateChannelsThread ==
null || updateChannelsThread.ThreadState.HasFlag(ThreadState.Stopped) || isUpdateChannelsThreadDying)
744 if (updateChannelsThread !=
null && !updateChannelsThread.Join(1000))
746 DebugConsole.ThrowError(
"SoundManager.UpdateChannels thread join timed out!");
748 needsUpdateChannels =
true;
749 updateChannelsThread =
new Thread(UpdateChannels)
751 Name =
"SoundManager.UpdateChannels Thread",
754 updateChannelsThread.Start();
758 private bool needsUpdateChannels =
false;
759 private ManualResetEvent updateChannelsMre =
null;
764 private void UpdateChannels()
766 updateChannelsMre =
new ManualResetEvent(
false);
767 bool killThread =
false;
771 for (
int i = 0; i < playingChannels.Length; i++)
773 lock (playingChannels[i])
775 for (
int j = 0; j < playingChannels[i].Length; j++)
777 if (playingChannels[i][j] ==
null) {
continue; }
778 if (playingChannels[i][j].IsStream)
783 playingChannels[i][j].UpdateStream();
787 playingChannels[i][j].Dispose();
788 playingChannels[i][j] =
null;
791 else if (playingChannels[i][j].FadingOutAndDisposing)
794 playingChannels[i][j].Gain -= 0.1f;
795 if (playingChannels[i][j].Gain <= 0.0f)
797 playingChannels[i][j].Dispose();
798 playingChannels[i][j] =
null;
804 updateChannelsMre.WaitOne(10);
805 updateChannelsMre.Reset();
806 lock (threadDeathMutex)
808 needsUpdateChannels = !killThread;
815 updateChannelsMre?.Set();
818 private void ReloadSounds()
820 for (
int i = loadedSounds.Count - 1; i >= 0; i--)
822 loadedSounds[i].InitializeAlBuffers();
826 private void ReleaseResources(
bool keepSounds)
828 for (
int i = 0; i < playingChannels.Length; i++)
830 lock (playingChannels[i])
832 for (
int j = 0; j < playingChannels[i].Length; j++)
834 playingChannels[i][j]?.Dispose();
839 updateChannelsThread?.Join();
840 for (
int i = loadedSounds.Count - 1; i >= 0; i--)
844 loadedSounds[i].DeleteAlBuffers();
848 loadedSounds[i].Dispose();
854 SoundBuffers.ClearPool();
861 ReleaseResources(
false);
865 throw new Exception(
"Failed to detach the current ALC context! (error code: " +
Alc.
GetError(alcDevice).ToString() +
")");
872 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)
void InitUpdateChannelThread()
Initializes the thread that handles streaming audio and fading out and disposing channels that are no...
SoundChannel GetChannelFromSound(Sound sound)
float?? PlaybackAmplitude
bool IsPlaying(Sound sound)
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
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)