aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Audio/OpenAlAudio.Streams.cs
blob: 435c0582bd4c0797774fbee5df7dff45dfe1c6fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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);
    }
}