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 int[] _sessionIds; /// /// The events linked to each session. /// private IWritableEvent[] _sessionsSystemEvent; /// /// The sessions instances. /// private 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 AudioRenderSystem(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() { 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(); } } } }