diff --git a/Robust.Client/Audio/AudioManager.Public.cs b/Robust.Client/Audio/AudioManager.Public.cs index 8cb573f3868..d585a278c92 100644 --- a/Robust.Client/Audio/AudioManager.Public.cs +++ b/Robust.Client/Audio/AudioManager.Public.cs @@ -15,6 +15,7 @@ namespace Robust.Client.Audio; internal partial class AudioManager { private float _zOffset; + private long _generatedBuffers; public void SetZOffset(float offset) { @@ -89,8 +90,6 @@ public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null) { var vorbis = AudioLoaderOgg.LoadAudioData(stream); - var buffer = AL.GenBuffer(); - ALFormat format; // NVorbis only supports loading into floats. // If this becomes a problem due to missing extension support (doubt it but ok), @@ -108,19 +107,12 @@ public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null) throw new InvalidOperationException("Unable to load audio with more than 2 channels."); } - unsafe - { - fixed (short* ptr = vorbis.Data.Span) - { - AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(short), - (int) vorbis.SampleRate); - } - } - - _checkAlError(); - - var handle = new ClydeHandle(_audioSampleBuffers.Count); - _audioSampleBuffers.Add(new LoadedAudioSample(buffer)); + var handle = new ClydeHandle(_generatedBuffers++); + MakeVorbisCast(handle, + format, + vorbis.Data.Span.ToArray(), + vorbis.Data.Length * sizeof(short), + (int) vorbis.SampleRate); var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate); return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist); } @@ -130,8 +122,6 @@ public AudioStream LoadAudioWav(Stream stream, string? name = null) { var wav = AudioLoaderWav.LoadAudioData(stream); - var buffer = AL.GenBuffer(); - ALFormat format; if (wav.BitsPerSample == 16) { @@ -168,18 +158,24 @@ public AudioStream LoadAudioWav(Stream stream, string? name = null) throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16"); } + var buffer = new short[wav.Data.Length / sizeof(short)]; + unsafe { - fixed (byte* ptr = wav.Data.Span) + fixed (void* sourcePtr = wav.Data.Span) { - AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate); + fixed (void* destinyPtr = buffer) + { + Buffer.MemoryCopy(sourcePtr, + destinyPtr, + wav.Data.Length, + wav.Data.Length); + } } } - _checkAlError(); - - var handle = new ClydeHandle(_audioSampleBuffers.Count); - _audioSampleBuffers.Add(new LoadedAudioSample(buffer)); + var handle = new ClydeHandle(_generatedBuffers++); + MakeVorbisCast(handle, format, buffer, wav.Data.Length, wav.SampleRate); var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate); return new AudioStream(handle, length, wav.NumChannels, name); } @@ -192,7 +188,8 @@ public AudioStream LoadAudioRaw(ReadOnlySpan samples, int channels, int s 1 => ALFormat.Mono16, 2 => ALFormat.Stereo16, _ => throw new ArgumentOutOfRangeException( - nameof(channels), "Only stereo and mono is currently supported") + nameof(channels), + "Only stereo and mono is currently supported") }; var buffer = AL.GenBuffer(); @@ -208,9 +205,10 @@ public AudioStream LoadAudioRaw(ReadOnlySpan samples, int channels, int s _checkAlError(); - var handle = new ClydeHandle(_audioSampleBuffers.Count); + + var handle = new ClydeHandle(_generatedBuffers++); + MakeVorbisCast(handle, fmt, samples.ToArray(), samples.Length * sizeof(short), sampleRate); var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate); - _audioSampleBuffers.Add(new LoadedAudioSample(buffer)); return new AudioStream(handle, length, channels, name); } @@ -292,8 +290,10 @@ internal void RemoveBufferedAudioSource(int handle) } // ReSharper disable once PossibleInvalidOperationException - // TODO: This really shouldn't be indexing based on the ClydeHandle... - AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value].BufferHandle); + var audioSample = EnsureAudioSample(stream.ClydeHandle!); + + AL.Source(source, ALSourcei.Buffer, audioSample!.BufferHandle); + audioSample.IncreaseUsings(); var audioSource = new AudioSource(this, source, stream); _audioSources.Add(source, new WeakReference(audioSource)); diff --git a/Robust.Client/Audio/AudioManager.Reloader.cs b/Robust.Client/Audio/AudioManager.Reloader.cs new file mode 100644 index 00000000000..848f885d2e0 --- /dev/null +++ b/Robust.Client/Audio/AudioManager.Reloader.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using OpenTK.Audio.OpenAL; +using Robust.Shared.Graphics; + +namespace Robust.Client.Audio; + +internal sealed partial class AudioManager +{ + private readonly Dictionary _audioVorbisCasts = new(); + private readonly Dictionary _loadedAudioSamples = new(); + + public void NotifySourceDisposed(AudioStream sourceStream) + { + OpenALSawmill.Debug($"Ended a source life with a {sourceStream.ClydeHandle} handle."); + var audioSample = _loadedAudioSamples[sourceStream.ClydeHandle!]; + audioSample.DecreaseUsings(); + + if (!audioSample.IsSafeToDelete()) + return; + + OpenALSawmill.Debug($"Enqueued {sourceStream.ClydeHandle} handle's buffer to free up."); + _loadedAudioSamples.Remove(sourceStream.ClydeHandle!); + _bufferDisposeQueue.Enqueue(audioSample.BufferHandle); + } + + internal AudioVorbisCast MakeVorbisCast(IClydeHandle handle, ALFormat format, short[] buffer, int size, int sampleRate) + { + var vorbisCast = new AudioVorbisCast(format, buffer, size, sampleRate); + _audioVorbisCasts.Add(handle, vorbisCast); + return vorbisCast; + } + + internal LoadedAudioSample? EnsureAudioSample(IClydeHandle handle) + { + if (_loadedAudioSamples.TryGetValue(handle, out var value)) + { + return value; + } + + var buffer = AL.GenBuffer(); + + _checkAlError(); + + if (!_audioVorbisCasts.TryGetValue(handle, out var vorbis)) + { + OpenALSawmill.Error($"Could not find audio cast for {handle}."); + return null; + } + + unsafe + { + fixed (short* prt = vorbis.Buffer) + { + AL.BufferData(buffer, vorbis.Format, (IntPtr) prt, vorbis.Size, vorbis.SampleRate); + } + } + + _checkAlError(); + + var sample = new LoadedAudioSample(buffer); + _loadedAudioSamples.Add(handle, sample); + + return sample; + } + + internal readonly struct AudioVorbisCast + { + public readonly ALFormat Format; + public readonly short[] Buffer; + public readonly int Size; + public readonly int SampleRate; + + public AudioVorbisCast(ALFormat format, short[] buffer, int size, int sampleRate) + { + Format = format; + Buffer = buffer; + Size = size; + SampleRate = sampleRate; + } + } +} diff --git a/Robust.Client/Audio/AudioManager.cs b/Robust.Client/Audio/AudioManager.cs index 20db02ce712..02fda71fac2 100644 --- a/Robust.Client/Audio/AudioManager.cs +++ b/Robust.Client/Audio/AudioManager.cs @@ -8,8 +8,8 @@ using Robust.Shared; using Robust.Shared.Audio; using Robust.Shared.Configuration; +using Robust.Shared.Graphics; using Robust.Shared.Log; -using Robust.Shared.Utility; namespace Robust.Client.Audio; @@ -23,8 +23,6 @@ internal sealed partial class AudioManager : IAudioInternal private ALDevice _openALDevice; private ALContext _openALContext; - private readonly List _audioSampleBuffers = new(); - private readonly Dictionary> _audioSources = new(); @@ -76,7 +74,8 @@ private bool _audioOpenDevice() if (_openALDevice == IntPtr.Zero) { OpenALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.", - preferredDevice, ALC.GetError(ALDevice.Null)); + preferredDevice, + ALC.GetError(ALDevice.Null)); _openALDevice = ALC.OpenDevice(null); } @@ -129,7 +128,8 @@ private static void RemoveEfx((int sourceHandle, int filterHandle) handles) EFX.DeleteFilter(handles.filterHandle); } - private void _checkAlcError(ALDevice device, + private void _checkAlcError( + ALDevice device, [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1) { @@ -160,13 +160,30 @@ public void _checkAlError([CallerMemberName] string callerMember = "", [CallerLi } } - private sealed class LoadedAudioSample + internal sealed class LoadedAudioSample { public readonly int BufferHandle; + private int UsingsCount { set; get; } public LoadedAudioSample(int bufferHandle) { BufferHandle = bufferHandle; + UsingsCount = 0; + } + + public void IncreaseUsings() + { + UsingsCount++; + } + + public void DecreaseUsings() + { + UsingsCount--; + } + + public bool IsSafeToDelete() + { + return UsingsCount <= 0; } } diff --git a/Robust.Client/Audio/Sources/AudioSource.cs b/Robust.Client/Audio/Sources/AudioSource.cs index 0558f29f36e..a0a5e7968e5 100644 --- a/Robust.Client/Audio/Sources/AudioSource.cs +++ b/Robust.Client/Audio/Sources/AudioSource.cs @@ -1,8 +1,6 @@ -using System; using System.Numerics; using OpenTK.Audio.OpenAL; using OpenTK.Audio.OpenAL.Extensions.Creative.EFX; -using Robust.Shared.Audio; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -84,6 +82,8 @@ protected override void Dispose(bool disposing) Master._checkAlError(); } + Master.NotifySourceDisposed(_sourceStream); + FilterHandle = 0; SourceHandle = -1; }