diff options
Diffstat (limited to 'src/Godosa.Audio/OpenAlAudio.Streams.cs')
| -rw-r--r-- | src/Godosa.Audio/OpenAlAudio.Streams.cs | 119 |
1 files changed, 119 insertions, 0 deletions
diff --git a/src/Godosa.Audio/OpenAlAudio.Streams.cs b/src/Godosa.Audio/OpenAlAudio.Streams.cs new file mode 100644 index 0000000..435c058 --- /dev/null +++ b/src/Godosa.Audio/OpenAlAudio.Streams.cs @@ -0,0 +1,119 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using Silk.NET.OpenAL; + +namespace Godosa.Audio; + +/// <summary>Endless stereo streams (ambience beds, music — audio spec §5–§6): each holds one +/// source with a queue of buffers the audio thread refills from its generator.</summary> +public sealed unsafe partial class OpenAlAudio +{ + private const int StreamBuffers = 4, StreamFrames = 2048; // ~190 ms queued + + public sealed class StreamHandle + { + internal StreamHandle(IStereoStream source, int channel) => (Source, Channel) = (source, channel); + internal IStereoStream Source { get; } + internal int Channel { get; } + internal int Slot = -1; + internal uint[] Buffers = System.Array.Empty<uint>(); + private int _gainBits = System.BitConverter.SingleToInt32Bits(1f); // written by the UI thread + + public float Gain + { + get => System.BitConverter.Int32BitsToSingle(Volatile.Read(ref _gainBits)); + set => Volatile.Write(ref _gainBits, System.BitConverter.SingleToInt32Bits(value)); + } + } + + private readonly bool[] _streamSlot = new bool[SourceCount]; + private readonly List<StreamHandle> _streams = new(); // audio thread only + private readonly ConcurrentQueue<(StreamHandle Stream, bool Add)> _streamCommands = new(); + private readonly float[] _streamMix = new float[StreamFrames * 2]; + private readonly short[] _streamPcm = new short[StreamFrames * 2]; + + public StreamHandle AddStream(IStereoStream source, int channel, float gain = 1f) + { + var handle = new StreamHandle(source, channel) { Gain = gain }; + _streamCommands.Enqueue((handle, true)); + return handle; + } + + public void RemoveStream(StreamHandle handle) => _streamCommands.Enqueue((handle, false)); + + private void UpdateStreams(AudioPauseState state) + { + var al = _al!; + while (_streamCommands.TryDequeue(out var command)) + { + if (command.Add) _streams.Add(command.Stream); + else if (_streams.Remove(command.Stream)) Release(command.Stream); + } + foreach (var s in _streams) + { + if (s.Slot < 0 && !Claim(s)) continue; + uint source = _sources[s.Slot]; + _gain[s.Slot] = s.Gain; + SetGain(s.Slot, s.Gain * _mix.ChannelGain(s.Channel, state)); + al.GetSourceProperty(source, GetSourceInteger.BuffersProcessed, out int processed); + for (int i = 0; i < processed; i++) + { + uint buffer; + al.SourceUnqueueBuffers(source, 1, &buffer); + Fill(s, buffer); + al.SourceQueueBuffers(source, 1, &buffer); + } + if (SourceState(source) != Silk.NET.OpenAL.SourceState.Playing) al.SourcePlay(source); // start, or recover from a starve + } + } + + private bool Claim(StreamHandle s) + { + var al = _al!; + int slot = FreeSource(); + if (slot < 0) return false; + s.Slot = slot; + _streamSlot[slot] = true; + _channel[slot] = s.Channel; + _appliedGain[slot] = -1f; + _pausedByUs[slot] = false; + uint source = _sources[slot]; + al.SetSourceProperty(source, SourceInteger.Buffer, 0); + al.SetSourceProperty(source, SourceBoolean.Looping, false); + al.SetSourceProperty(source, SourceBoolean.SourceRelative, true); + al.SetSourceProperty(source, SourceVector3.Position, System.Numerics.Vector3.Zero); + al.SetSourceProperty(source, SourceFloat.Pitch, 1f); + SendToReverb(source, positioned: false); + s.Buffers = al.GenBuffers(StreamBuffers); + foreach (uint b in s.Buffers) Fill(s, b); + fixed (uint* ids = s.Buffers) al.SourceQueueBuffers(source, StreamBuffers, ids); + if (EngineDebug.Audio) EngineDebug.Write("audio", $"stream {s.Source.GetType().Name} {s.Channel} on source {slot}"); + return true; + } + + private void Fill(StreamHandle s, uint buffer) + { + s.Source.Render(_streamMix, StreamFrames); + for (int i = 0; i < _streamMix.Length; i++) _streamPcm[i] = Pcm.ToShort(_streamMix[i]); + _al!.BufferData(buffer, BufferFormat.Stereo16, _streamPcm, Pcm.SampleRate); + } + + private void Release(StreamHandle s) + { + if (s.Slot < 0) return; + var al = _al!; + uint source = _sources[s.Slot]; + al.SourceStop(source); + al.SetSourceProperty(source, SourceInteger.Buffer, 0); // detaches the queue + al.DeleteBuffers(s.Buffers); + _streamSlot[s.Slot] = false; + s.Slot = -1; + } + + /// <summary>Before the context goes (HRTF change): streams re-claim a source afterwards.</summary> + private void ReleaseStreams() + { + foreach (var s in _streams) Release(s); + } +} |
