using Ryujinx.Audio.Integration;
using Ryujinx.Audio.Renderer.Dsp;
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
using Ryujinx.Memory;
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace Ryujinx.Audio.Renderer.Server
{
///
/// The audio renderer manager.
///
public class AudioRendererManager : IDisposable
{
///
/// Lock used for session allocation.
///
private readonly object _sessionLock = new();
///
/// Lock used to control the running state.
///
private readonly object _audioProcessorLock = new();
///
/// The session ids allocation table.
///
private readonly int[] _sessionIds;
///
/// The events linked to each session.
///
private IWritableEvent[] _sessionsSystemEvent;
///
/// The sessions instances.
///
private readonly AudioRenderSystem[] _sessions;
///
/// The count of active sessions.
///
private int _activeSessionCount;
///
/// The worker thread used to run .
///
private Thread _workerThread;
///
/// Indicate if the worker thread and are running.
///
private bool _isRunning;
///
/// The audio device driver to create audio outputs.
///
private IHardwareDeviceDriver _deviceDriver;
///
/// Tick source used to measure elapsed time.
///
public ITickSource TickSource { get; }
///
/// The instance associated to this manager.
///
public AudioProcessor Processor { get; }
///
/// The dispose state.
///
private int _disposeState;
///
/// Create a new .
///
/// Tick source used to measure elapsed time.
public AudioRendererManager(ITickSource tickSource)
{
Processor = new AudioProcessor();
TickSource = tickSource;
_sessionIds = new int[Constants.AudioRendererSessionCountMax];
_sessions = new AudioRenderSystem[Constants.AudioRendererSessionCountMax];
_activeSessionCount = 0;
for (int i = 0; i < _sessionIds.Length; i++)
{
_sessionIds[i] = i;
}
}
///
/// Initialize the .
///
/// The events associated to each session.
/// The device driver to use to create audio outputs.
public void Initialize(IWritableEvent[] sessionSystemEvents, IHardwareDeviceDriver deviceDriver)
{
_sessionsSystemEvent = sessionSystemEvents;
_deviceDriver = deviceDriver;
}
///
/// Get the work buffer size required by a session.
///
/// The user configuration
/// The work buffer size required by a session.
public static ulong GetWorkBufferSize(ref AudioRendererConfiguration parameter)
{
return AudioRenderSystem.GetWorkBufferSize(ref parameter);
}
///
/// Acquire a new session id.
///
/// A new session id.
private int AcquireSessionId()
{
lock (_sessionLock)
{
int index = _activeSessionCount;
Debug.Assert(index < _sessionIds.Length);
int sessionId = _sessionIds[index];
_sessionIds[index] = -1;
_activeSessionCount++;
Logger.Info?.Print(LogClass.AudioRenderer, $"Registered new renderer ({sessionId})");
return sessionId;
}
}
///
/// Release a given .
///
/// The session id to release.
private void ReleaseSessionId(int sessionId)
{
lock (_sessionLock)
{
Debug.Assert(_activeSessionCount > 0);
int newIndex = --_activeSessionCount;
_sessionIds[newIndex] = sessionId;
}
Logger.Info?.Print(LogClass.AudioRenderer, $"Unregistered renderer ({sessionId})");
}
///
/// Check if there is any audio renderer active.
///
/// Returns true if there is any audio renderer active.
private bool HasAnyActiveRendererLocked()
{
foreach (AudioRenderSystem renderer in _sessions)
{
if (renderer != null)
{
return true;
}
}
return false;
}
///
/// Start the and worker thread.
///
private void StartLocked(float volume)
{
_isRunning = true;
// TODO: virtual device mapping (IAudioDevice)
Processor.Start(_deviceDriver, volume);
_workerThread = new Thread(SendCommands)
{
Name = "AudioRendererManager.Worker",
};
_workerThread.Start();
}
///
/// Stop the and worker thread.
///
private void StopLocked()
{
_isRunning = false;
_workerThread.Join();
Processor.Stop();
Logger.Info?.Print(LogClass.AudioRenderer, "Stopped audio renderer");
}
///
/// Stop sending commands to the without stopping the worker thread.
///
public void StopSendingCommands()
{
lock (_sessionLock)
{
foreach (AudioRenderSystem renderer in _sessions)
{
renderer?.Disable();
}
}
lock (_audioProcessorLock)
{
if (_isRunning)
{
StopLocked();
}
}
}
///
/// Worker main function. This is used to dispatch audio renderer commands to the .
///
private void SendCommands()
{
Logger.Info?.Print(LogClass.AudioRenderer, "Starting audio renderer");
Processor.Wait();
while (_isRunning)
{
lock (_sessionLock)
{
foreach (AudioRenderSystem renderer in _sessions)
{
renderer?.SendCommands();
}
}
Processor.Signal();
Processor.Wait();
}
}
///
/// Register a new .
///
/// The to register.
private void Register(AudioRenderSystem renderer, float volume)
{
lock (_sessionLock)
{
_sessions[renderer.GetSessionId()] = renderer;
}
lock (_audioProcessorLock)
{
if (!_isRunning)
{
StartLocked(volume);
}
}
}
///
/// Unregister a new .
///
/// The to unregister.
internal void Unregister(AudioRenderSystem renderer)
{
lock (_sessionLock)
{
int sessionId = renderer.GetSessionId();
_sessions[renderer.GetSessionId()] = null;
ReleaseSessionId(sessionId);
}
lock (_audioProcessorLock)
{
if (_isRunning && !HasAnyActiveRendererLocked())
{
StopLocked();
}
}
}
///
/// Open a new
///
/// The new
/// The memory manager that will be used for all guest memory operations.
/// The user configuration
/// The applet resource user id of the application.
/// The guest work buffer address.
/// The guest work buffer size.
/// The process handle of the application.
/// A reporting an error or a success.
public ResultCode OpenAudioRenderer(
out AudioRenderSystem renderer,
IVirtualMemoryManager memoryManager,
ref AudioRendererConfiguration parameter,
ulong appletResourceUserId,
ulong workBufferAddress,
ulong workBufferSize,
uint processHandle,
float volume)
{
int sessionId = AcquireSessionId();
AudioRenderSystem audioRenderer = new(this, _sessionsSystemEvent[sessionId]);
// TODO: Eventually, we should try to use the guest supplied work buffer instead of allocating
// our own. However, it was causing problems on some applications that would unmap the memory
// before the audio renderer was fully disposed.
Memory workBufferMemory = GC.AllocateArray((int)workBufferSize, pinned: true);
ResultCode result = audioRenderer.Initialize(
ref parameter,
processHandle,
workBufferMemory,
workBufferAddress,
workBufferSize,
sessionId,
appletResourceUserId,
memoryManager);
if (result == ResultCode.Success)
{
renderer = audioRenderer;
Register(renderer, volume);
}
else
{
ReleaseSessionId(sessionId);
renderer = null;
}
return result;
}
public float GetVolume()
{
if (Processor != null)
{
return Processor.GetVolume();
}
return 0f;
}
public void SetVolume(float volume)
{
Processor?.SetVolume(volume);
}
public void Dispose()
{
GC.SuppressFinalize(this);
if (Interlocked.CompareExchange(ref _disposeState, 1, 0) == 0)
{
Dispose(true);
}
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Clone the sessions array to dispose them outside the lock.
AudioRenderSystem[] sessions;
lock (_sessionLock)
{
sessions = _sessions.ToArray();
}
foreach (AudioRenderSystem renderer in sessions)
{
renderer?.Dispose();
}
lock (_audioProcessorLock)
{
if (_isRunning)
{
StopLocked();
}
}
Processor.Dispose();
}
}
}
}