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);
}
}