aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Audio/OpenAlAudio.Streams.cs
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
committergodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
commit39066900773e7857faf2d02e7ed51b71d219d97d (patch)
treeffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Audio/OpenAlAudio.Streams.cs
downloadgodosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz
godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip
godosa-engine: initial public history
Diffstat (limited to 'src/Godosa.Audio/OpenAlAudio.Streams.cs')
-rw-r--r--src/Godosa.Audio/OpenAlAudio.Streams.cs119
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);
+ }
+}