using OpenTK.Audio.OpenAL; using System; using System.Collections.Concurrent; using System.Collections.Generic; namespace Ryujinx.Audio { internal class OpenALAudioTrack : IDisposable { public int SourceId { get; private set; } public int SampleRate { get; private set; } public ALFormat Format { get; private set; } public PlaybackState State { get; set; } public int HardwareChannels { get; } public int VirtualChannels { get; } public uint BufferCount => (uint)_buffers.Count; public ulong PlayedSampleCount { get; set; } private ReleaseCallback _callback; private ConcurrentDictionary _buffers; private Queue _queuedTagsQueue; private Queue _releasedTagsQueue; private bool _disposed; public OpenALAudioTrack(int sampleRate, ALFormat format, int hardwareChannels, int virtualChannels, ReleaseCallback callback) { SampleRate = sampleRate; Format = format; State = PlaybackState.Stopped; SourceId = AL.GenSource(); HardwareChannels = hardwareChannels; VirtualChannels = virtualChannels; _callback = callback; _buffers = new ConcurrentDictionary(); _queuedTagsQueue = new Queue(); _releasedTagsQueue = new Queue(); } public bool ContainsBuffer(long tag) { foreach (long queuedTag in _queuedTagsQueue) { if (queuedTag == tag) { return true; } } return false; } public long[] GetReleasedBuffers(int count) { AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int releasedCount); releasedCount += _releasedTagsQueue.Count; if (count > releasedCount) { count = releasedCount; } List tags = new List(); while (count-- > 0 && _releasedTagsQueue.TryDequeue(out long tag)) { tags.Add(tag); } while (count-- > 0 && _queuedTagsQueue.TryDequeue(out long tag)) { AL.SourceUnqueueBuffers(SourceId, 1); tags.Add(tag); } return tags.ToArray(); } public int AppendBuffer(long tag) { if (_disposed) { throw new ObjectDisposedException(GetType().Name); } int id = AL.GenBuffer(); _buffers.AddOrUpdate(tag, id, (key, oldId) => { AL.DeleteBuffer(oldId); return id; }); _queuedTagsQueue.Enqueue(tag); return id; } public void CallReleaseCallbackIfNeeded() { AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int releasedCount); if (releasedCount > 0) { // If we signal, then we also need to have released buffers available // to return when GetReleasedBuffers is called. // If playback needs to be re-started due to all buffers being processed, // then OpenAL zeros the counts (ReleasedCount), so we keep it on the queue. while (releasedCount-- > 0 && _queuedTagsQueue.TryDequeue(out long tag)) { AL.SourceUnqueueBuffers(SourceId, 1); _releasedTagsQueue.Enqueue(tag); } _callback(); } } public bool FlushBuffers() { while (_queuedTagsQueue.TryDequeue(out long tag)) { _releasedTagsQueue.Enqueue(tag); } _callback(); foreach (var buffer in _buffers) { AL.DeleteBuffer(buffer.Value); } bool heldBuffers = _buffers.Count > 0; _buffers.Clear(); return heldBuffers; } public void SetVolume(float volume) { AL.Source(SourceId, ALSourcef.Gain, volume); } public float GetVolume() { AL.GetSource(SourceId, ALSourcef.Gain, out float volume); return volume; } public void Dispose() { Dispose(true); } protected virtual void Dispose(bool disposing) { if (disposing && !_disposed) { _disposed = true; AL.DeleteSource(SourceId); foreach (int id in _buffers.Values) { AL.DeleteBuffer(id); } } } } }