using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using Silk.NET.OpenAL; namespace Godosa.Audio; /// 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. 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(); 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 _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; } /// Before the context goes (HRTF change): streams re-claim a source afterwards. private void ReleaseStreams() { foreach (var s in _streams) Release(s); } }