瀏覽代碼

UI - Add Volume Controls + Mute Toggle (F2) (#2871)

* Add the ability to toggle mute in the status bar.

* Add the ability to toggle mute in the status bar.

* Formatting fixes

* Add hotkey (F2) to mute

* Add default hotkey to config.json

* Add ability to change volume via slider.

* Fix Headless

* Fix SDL2 Problem : Credits to d3xMachina

* Remove unnecessary work

* Address gdk comments

* Toggling with Hotkey now properly restores volume to original level.

* Toggling with Hotkey now properly restores volume to original level.

* Update UI to show Volume % instead of Muted/Unmuted

* Clean up the volume ui a bit.

* Undo unintentionally committed code.

* Implement AudRen Support

* Restore intiial volume level in function definition.

* Finalize UI

* Finalize UI

* Use clamp for bounds check

* Use Math.Clamp for volume in soundio

* Address comments by gdkchan

* Address remaining comments

* Fix missing semicolon

* Address remaining gdkchan comment

* Fix comment

* Change /* to //

* Allow volume slider to change volume immediately.
Also force label text to cast to int to prevent decimals from showing in status bar

* Remove blank line

* Undo setting of volume level when "Cancel" is pressed.

* Fix allignment for settings window code
sharmander 4 年之前
父節點
當前提交
cb43cc7e32
共有 35 個文件被更改,包括 410 次插入93 次删除
  1. 2 2
      Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs
  2. 2 1
      Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
  3. 2 2
      Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs
  4. 12 7
      Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
  5. 4 2
      Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs
  6. 4 4
      Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
  7. 4 2
      Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs
  8. 2 2
      Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs
  9. 2 2
      Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs
  10. 1 1
      Ryujinx.Audio/Common/AudioDeviceSession.cs
  11. 12 2
      Ryujinx.Audio/Integration/HardwareDeviceImpl.cs
  12. 12 0
      Ryujinx.Audio/Integration/IHardwareDevice.cs
  13. 1 1
      Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs
  14. 38 2
      Ryujinx.Audio/Output/AudioOutputManager.cs
  15. 29 2
      Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs
  16. 21 6
      Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs
  17. 11 0
      Ryujinx.Audio/Renderer/Utils/FileHardwareDevice.cs
  18. 11 0
      Ryujinx.Audio/Renderer/Utils/SplitterHardwareDevice.cs
  19. 1 0
      Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs
  20. 8 1
      Ryujinx.HLE/HLEConfiguration.cs
  21. 13 0
      Ryujinx.HLE/HOS/Horizon.cs
  22. 2 2
      Ryujinx.HLE/HOS/Services/Audio/AudioOutManager.cs
  23. 2 2
      Ryujinx.HLE/HOS/Services/Audio/AudioOutManagerServer.cs
  24. 1 1
      Ryujinx.HLE/HOS/Services/Audio/AudioRendererManager.cs
  25. 1 1
      Ryujinx.HLE/HOS/Services/Audio/IAudioOutManager.cs
  26. 15 0
      Ryujinx.HLE/Switch.cs
  27. 3 0
      Ryujinx.Headless.SDL2/Options.cs
  28. 2 1
      Ryujinx.Headless.SDL2/Program.cs
  29. 6 1
      Ryujinx/Configuration/ConfigurationFileFormat.cs
  30. 28 0
      Ryujinx/Configuration/ConfigurationState.cs
  31. 38 7
      Ryujinx/Ui/MainWindow.cs
  32. 69 36
      Ryujinx/Ui/MainWindow.glade
  33. 21 1
      Ryujinx/Ui/RendererWidgetBase.cs
  34. 3 1
      Ryujinx/Ui/StatusUpdatedEventArgs.cs
  35. 27 1
      Ryujinx/Ui/Windows/SettingsWindow.cs

+ 2 - 2
Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs

@@ -52,7 +52,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
             }
         }
 
-        public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
+        public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
         {
             if (channelCount == 0)
             {
@@ -73,7 +73,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
                 throw new ArgumentException($"{channelCount}");
             }
 
-            OpenALHardwareDeviceSession session = new OpenALHardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount);
+            OpenALHardwareDeviceSession session = new OpenALHardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
 
             _sessions.TryAdd(session, 0);
 

+ 2 - 1
Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs

@@ -19,7 +19,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
 
         private object _lock = new object();
 
-        public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+        public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
         {
             _driver = driver;
             _queuedBuffers = new Queue<OpenALAudioBuffer>();
@@ -27,6 +27,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
             _targetFormat = GetALFormat();
             _isActive = false;
             _playedSampleCount = 0;
+            SetVolume(requestedVolume);
         }
 
         private ALFormat GetALFormat()

+ 2 - 2
Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs

@@ -51,7 +51,7 @@ namespace Ryujinx.Audio.Backends.SDL2
             return _pauseEvent;
         }
 
-        public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
+        public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
         {
             if (channelCount == 0)
             {
@@ -68,7 +68,7 @@ namespace Ryujinx.Audio.Backends.SDL2
                 throw new NotImplementedException("Input direction is currently not implemented on SDL2 backend!");
             }
 
-            SDL2HardwareDeviceSession session = new SDL2HardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount);
+            SDL2HardwareDeviceSession session = new SDL2HardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
 
             _sessions.TryAdd(session, 0);
 

+ 12 - 7
Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs

@@ -26,7 +26,7 @@ namespace Ryujinx.Audio.Backends.SDL2
         private float _volume;
         private ushort _nativeSampleFormat;
 
-        public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+        public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
         {
             _driver = driver;
             _updateRequiredEvent = _driver.GetUpdateRequiredEvent();
@@ -37,7 +37,7 @@ namespace Ryujinx.Audio.Backends.SDL2
             _nativeSampleFormat = SDL2HardwareDeviceDriver.GetSDL2Format(RequestedSampleFormat);
             _sampleCount = uint.MaxValue;
             _started = false;
-            _volume = 1.0f;
+            _volume = requestedVolume;
         }
 
         private void EnsureAudioStreamSetup(AudioBuffer buffer)
@@ -82,7 +82,7 @@ namespace Ryujinx.Audio.Backends.SDL2
 
             if (frameCount == 0)
             {
-                // SDL2 left the responsability to the user to clear the buffer.
+                // SDL2 left the responsibility to the user to clear the buffer.
                 streamSpan.Fill(0);
 
                 return;
@@ -92,11 +92,16 @@ namespace Ryujinx.Audio.Backends.SDL2
 
             _ringBuffer.Read(samples, 0, samples.Length);
 
-            samples.AsSpan().CopyTo(streamSpan);
-            streamSpan.Slice(samples.Length).Fill(0);
+            fixed (byte* p = samples)
+            {
+                IntPtr pStreamSrc = (IntPtr)p;
+
+                // Zero the dest buffer
+                streamSpan.Fill(0);
 
-            // Apply volume to written data
-            SDL_MixAudioFormat(stream, stream, _nativeSampleFormat, (uint)samples.Length, (int)(_volume * SDL_MIX_MAXVOLUME));
+                // Apply volume to written data
+                SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_volume * SDL_MIX_MAXVOLUME));
+            }
 
             ulong sampleCount = GetSampleCount(samples.Length);
 

+ 4 - 2
Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs

@@ -130,7 +130,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
             return _pauseEvent;
         }
 
-        public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
+        public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
         {
             if (channelCount == 0)
             {
@@ -142,12 +142,14 @@ namespace Ryujinx.Audio.Backends.SoundIo
                 sampleRate = Constants.TargetSampleRate;
             }
 
+            volume = Math.Clamp(volume, 0, 1);
+
             if (direction != Direction.Output)
             {
                 throw new NotImplementedException("Input direction is currently not implemented on SoundIO backend!");
             }
 
-            SoundIoHardwareDeviceSession session = new SoundIoHardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount);
+            SoundIoHardwareDeviceSession session = new SoundIoHardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
 
             _sessions.TryAdd(session, 0);
 

+ 4 - 4
Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs

@@ -19,21 +19,21 @@ namespace Ryujinx.Audio.Backends.SoundIo
         private ManualResetEvent _updateRequiredEvent;
         private int _disposeState;
 
-        public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+        public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
         {
             _driver = driver;
             _updateRequiredEvent = _driver.GetUpdateRequiredEvent();
             _queuedBuffers = new ConcurrentQueue<SoundIoAudioBuffer>();
             _ringBuffer = new DynamicRingBuffer();
 
-            SetupOutputStream();
+            SetupOutputStream(requestedVolume);
         }
 
-        private void SetupOutputStream()
+        private void SetupOutputStream(float requestedVolume)
         {
             _outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
             _outputStream.WriteCallback += Update;
-
+            _outputStream.Volume = requestedVolume;
             // TODO: Setup other callbacks (errors, ect).
 
             _outputStream.Open();

+ 4 - 2
Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs

@@ -68,7 +68,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
             };
         }
 
-        public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
+        public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
         {
             if (channelCount == 0)
             {
@@ -80,6 +80,8 @@ namespace Ryujinx.Audio.Backends.CompatLayer
                 sampleRate = Constants.TargetSampleRate;
             }
 
+            volume = Math.Clamp(volume, 0, 1);
+
             if (!_realDriver.SupportsDirection(direction))
             {
                 if (direction == Direction.Input)
@@ -94,7 +96,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
 
             uint hardwareChannelCount = SelectHardwareChannelCount(channelCount);
 
-            IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, hardwareChannelCount);
+            IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, hardwareChannelCount, volume);
 
             if (hardwareChannelCount == channelCount)
             {

+ 2 - 2
Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs

@@ -35,7 +35,7 @@ namespace Ryujinx.Audio.Backends.Dummy
             _pauseEvent = new ManualResetEvent(true);
         }
 
-        public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
+        public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
         {
             if (sampleRate == 0)
             {
@@ -49,7 +49,7 @@ namespace Ryujinx.Audio.Backends.Dummy
 
             if (direction == Direction.Output)
             {
-                return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount);
+                return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
             }
             else
             {

+ 2 - 2
Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs

@@ -30,9 +30,9 @@ namespace Ryujinx.Audio.Backends.Dummy
 
         private ulong _playedSampleCount;
 
-        public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+        public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
         {
-            _volume = 1.0f;
+            _volume = requestedVolume;
             _manager = manager;
         }
 

+ 1 - 1
Ryujinx.Audio/Common/AudioDeviceSession.cs

@@ -106,7 +106,7 @@ namespace Ryujinx.Audio.Common
             _bufferAppendedCount = 0;
             _bufferRegisteredCount = 0;
             _bufferReleasedCount = 0;
-            _volume = 1.0f;
+            _volume = deviceSession.GetVolume();
             _state = AudioDeviceState.Stopped;
         }
 

+ 12 - 2
Ryujinx.Audio/Integration/HardwareDeviceImpl.cs

@@ -30,9 +30,9 @@ namespace Ryujinx.Audio.Integration
 
         private byte[] _buffer;
 
-        public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate)
+        public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate, float volume)
         {
-            _session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount);
+            _session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount, volume);
             _channelCount = channelCount;
             _sampleRate = sampleRate;
             _currentBufferTag = 0;
@@ -56,6 +56,16 @@ namespace Ryujinx.Audio.Integration
             _currentBufferTag = _currentBufferTag % 4;
         }
 
+        public void SetVolume(float volume)
+        {
+            _session.SetVolume(volume);
+        }
+
+        public float GetVolume()
+        {
+            return _session.GetVolume();
+        }
+
         public uint GetChannelCount()
         {
             return _channelCount;

+ 12 - 0
Ryujinx.Audio/Integration/IHardwareDevice.cs

@@ -25,6 +25,18 @@ namespace Ryujinx.Audio.Integration
     /// </summary>
     public interface IHardwareDevice : IDisposable
     {
+        /// <summary>
+        /// Sets the volume level for this device.
+        /// </summary>
+        /// <param name="volume">The volume level to set.</param>
+        void SetVolume(float volume);
+
+        /// <summary>
+        /// Gets the volume level for this device.
+        /// </summary>
+        /// <returns>The volume level of this device.</returns>
+        float GetVolume();
+
         /// <summary>
         /// Get the supported sample rate of this device.
         /// </summary>

+ 1 - 1
Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs

@@ -33,7 +33,7 @@ namespace Ryujinx.Audio.Integration
             Output
         }
 
-        IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount);
+        IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume = 1f);
 
         ManualResetEvent GetUpdateRequiredEvent();
         ManualResetEvent GetPauseEvent();

+ 38 - 2
Ryujinx.Audio/Output/AudioOutputManager.cs

@@ -208,13 +208,14 @@ namespace Ryujinx.Audio.Output
                                        SampleFormat sampleFormat,
                                        ref AudioInputConfiguration parameter,
                                        ulong appletResourceUserId,
-                                       uint processHandle)
+                                       uint processHandle,
+                                       float volume)
         {
             int sessionId = AcquireSessionId();
 
             _sessionsBufferEvents[sessionId].Clear();
 
-            IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount);
+            IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount, volume);
 
             AudioOutputSystem audioOut = new AudioOutputSystem(this, _lock, deviceSession, _sessionsBufferEvents[sessionId]);
 
@@ -247,6 +248,41 @@ namespace Ryujinx.Audio.Output
             return result;
         }
 
+        /// <summary>
+        /// Sets the volume for all output devices.
+        /// </summary>
+        /// <param name="volume">The volume to set.</param>
+        public void SetVolume(float volume)
+        {
+            if (_sessions != null)
+            {
+                foreach (AudioOutputSystem session in _sessions)
+                {
+                    session?.SetVolume(volume);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the volume for all output devices.
+        /// </summary>
+        /// <returns>A float indicating the volume level.</returns>
+        public float GetVolume()
+        {
+            if (_sessions != null)
+            {
+                foreach (AudioOutputSystem session in _sessions)
+                {
+                    if (session != null)
+                    {
+                        return session.GetVolume();
+                    }
+                }
+            }
+
+            return 0.0f;
+        }
+
         public void Dispose()
         {
             if (Interlocked.CompareExchange(ref _disposeState, 1, 0) == 0)

+ 29 - 2
Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs

@@ -78,7 +78,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
             }
         }
 
-        public void Start(IHardwareDeviceDriver deviceDriver)
+        public void Start(IHardwareDeviceDriver deviceDriver, float volume)
         {
             OutputDevices = new IHardwareDevice[Constants.AudioRendererSessionCountMax];
 
@@ -89,7 +89,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
             for (int i = 0; i < OutputDevices.Length; i++)
             {
                 // TODO: Don't hardcode sample rate.
-                OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate);
+                OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate, volume);
             }
 
             _mailbox = new Mailbox<MailboxMessage>();
@@ -245,6 +245,33 @@ namespace Ryujinx.Audio.Renderer.Dsp
             _mailbox.SendResponse(MailboxMessage.Stop);
         }
 
+        public float GetVolume()
+        {
+            if (OutputDevices != null)
+            {
+                foreach (IHardwareDevice outputDevice in OutputDevices)
+                {
+                    if (outputDevice != null)
+                    {
+                        return outputDevice.GetVolume();
+                    }
+                }
+            }
+
+            return 0f;
+        }
+
+        public void SetVolume(float volume)
+        {
+            if (OutputDevices != null)
+            {
+                foreach (IHardwareDevice outputDevice in OutputDevices)
+                {
+                    outputDevice?.SetVolume(volume);
+                }
+            }
+        }
+
         public void Dispose()
         {
             Dispose(true);

+ 21 - 6
Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs

@@ -186,12 +186,12 @@ namespace Ryujinx.Audio.Renderer.Server
         /// <summary>
         /// Start the <see cref="AudioProcessor"/> and worker thread.
         /// </summary>
-        private void StartLocked()
+        private void StartLocked(float volume)
         {
             _isRunning = true;
 
             // TODO: virtual device mapping (IAudioDevice)
-            Processor.Start(_deviceDriver);
+            Processor.Start(_deviceDriver, volume);
 
             _workerThread = new Thread(SendCommands)
             {
@@ -263,7 +263,7 @@ namespace Ryujinx.Audio.Renderer.Server
         /// Register a new <see cref="AudioRenderSystem"/>.
         /// </summary>
         /// <param name="renderer">The <see cref="AudioRenderSystem"/> to register.</param>
-        private void Register(AudioRenderSystem renderer)
+        private void Register(AudioRenderSystem renderer, float volume)
         {
             lock (_sessionLock)
             {
@@ -274,7 +274,7 @@ namespace Ryujinx.Audio.Renderer.Server
             {
                 if (!_isRunning)
                 {
-                    StartLocked();
+                    StartLocked(volume);
                 }
             }
         }
@@ -314,7 +314,7 @@ namespace Ryujinx.Audio.Renderer.Server
         /// <param name="workBufferSize">The guest work buffer size.</param>
         /// <param name="processHandle">The process handle of the application.</param>
         /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns>
-        public ResultCode OpenAudioRenderer(out AudioRenderSystem renderer, IVirtualMemoryManager memoryManager, ref AudioRendererConfiguration parameter, ulong appletResourceUserId, ulong workBufferAddress, ulong workBufferSize, uint processHandle)
+        public ResultCode OpenAudioRenderer(out AudioRenderSystem renderer, IVirtualMemoryManager memoryManager, ref AudioRendererConfiguration parameter, ulong appletResourceUserId, ulong workBufferAddress, ulong workBufferSize, uint processHandle, float volume)
         {
             int sessionId = AcquireSessionId();
 
@@ -326,7 +326,7 @@ namespace Ryujinx.Audio.Renderer.Server
             {
                 renderer = audioRenderer;
 
-                Register(renderer);
+                Register(renderer, volume);
             }
             else
             {
@@ -338,6 +338,21 @@ namespace Ryujinx.Audio.Renderer.Server
             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)

+ 11 - 0
Ryujinx.Audio/Renderer/Utils/FileHardwareDevice.cs

@@ -76,6 +76,17 @@ namespace Ryujinx.Audio.Renderer.Utils
             _stream.Flush();
         }
 
+        public void SetVolume(float volume)
+        {
+            // Do nothing, volume is not used for FileHardwareDevice at the moment.
+        }
+
+        public float GetVolume()
+        {
+            // FileHardwareDevice does not incorporate volume.
+            return 0;
+        }
+
         public uint GetChannelCount()
         {
             return _channelCount;

+ 11 - 0
Ryujinx.Audio/Renderer/Utils/SplitterHardwareDevice.cs

@@ -37,6 +37,17 @@ namespace Ryujinx.Audio.Renderer.Utils
             _secondaryDevice?.AppendBuffer(data, channelCount);
         }
 
+        public void SetVolume(float volume)
+        {
+            _baseDevice.SetVolume(volume);
+            _secondaryDevice.SetVolume(volume);
+        }
+
+        public float GetVolume()
+        {
+            return _baseDevice.GetVolume();
+        }
+
         public uint GetChannelCount()
         {
             return _baseDevice.GetChannelCount();

+ 1 - 0
Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs

@@ -6,5 +6,6 @@
         public Key Screenshot { get; set; }
         public Key ShowUi { get; set; }
         public Key Pause { get; set; }
+        public Key ToggleMute { get; set; }
     }
 }

+ 8 - 1
Ryujinx.HLE/HLEConfiguration.cs

@@ -139,6 +139,11 @@ namespace Ryujinx.HLE
         /// </summary>
         public AspectRatio AspectRatio { get; set; }
 
+        /// <summary>
+        /// The audio volume level.
+        /// </summary>
+        public float AudioVolume { get; set; }
+
         /// <summary>
         /// An action called when HLE force a refresh of output after docked mode changed.
         /// </summary>
@@ -164,7 +169,8 @@ namespace Ryujinx.HLE
                                 string                 timeZone,
                                 MemoryManagerMode      memoryManagerMode,
                                 bool                   ignoreMissingServices,
-                                AspectRatio            aspectRatio)
+                                AspectRatio            aspectRatio,
+                                float                  audioVolume)
         {
             VirtualFileSystem      = virtualFileSystem;
             LibHacHorizonManager   = libHacHorizonManager;
@@ -187,6 +193,7 @@ namespace Ryujinx.HLE
             MemoryManagerMode      = memoryManagerMode;
             IgnoreMissingServices  = ignoreMissingServices;
             AspectRatio            = aspectRatio;
+            AudioVolume            = audioVolume;
         }
     }
 }

+ 13 - 0
Ryujinx.HLE/HOS/Horizon.cs

@@ -243,6 +243,7 @@ namespace Ryujinx.HLE.HOS
             AudioOutputManager = new AudioOutputManager();
             AudioInputManager = new AudioInputManager();
             AudioRendererManager = new AudioRendererManager();
+            AudioRendererManager.SetVolume(Device.Configuration.AudioVolume);
             AudioDeviceSessionRegistry = new VirtualDeviceSessionRegistry();
 
             IWritableEvent[] audioOutputRegisterBufferEvents = new IWritableEvent[Constants.AudioOutSessionCountMax];
@@ -255,6 +256,7 @@ namespace Ryujinx.HLE.HOS
             }
 
             AudioOutputManager.Initialize(Device.AudioDeviceDriver, audioOutputRegisterBufferEvents);
+            AudioOutputManager.SetVolume(Device.Configuration.AudioVolume);
 
             IWritableEvent[] audioInputRegisterBufferEvents = new IWritableEvent[Constants.AudioInSessionCountMax];
 
@@ -326,6 +328,17 @@ namespace Ryujinx.HLE.HOS
             }
         }
 
+        public void SetVolume(float volume)
+        {
+            AudioOutputManager.SetVolume(volume);
+            AudioRendererManager.SetVolume(volume);
+        }
+
+        public float GetVolume()
+        {
+            return AudioOutputManager.GetVolume() == 0 ? AudioRendererManager.GetVolume() : AudioOutputManager.GetVolume();
+        }
+
         public void ReturnFocus()
         {
             AppletState.SetFocus(true);

+ 2 - 2
Ryujinx.HLE/HOS/Services/Audio/AudioOutManager.cs

@@ -20,11 +20,11 @@ namespace Ryujinx.HLE.HOS.Services.Audio
             return _impl.ListAudioOuts();
         }
 
-        public ResultCode OpenAudioOut(ServiceCtx context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, string inputDeviceName, ref AudioInputConfiguration parameter, ulong appletResourceUserId, uint processHandle)
+        public ResultCode OpenAudioOut(ServiceCtx context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, string inputDeviceName, ref AudioInputConfiguration parameter, ulong appletResourceUserId, uint processHandle, float volume)
         {
             var memoryManager = context.Process.HandleTable.GetKProcess((int)processHandle).CpuMemory;
 
-            ResultCode result = (ResultCode)_impl.OpenAudioOut(out outputDeviceName, out outputConfiguration, out AudioOutputSystem outSystem, memoryManager, inputDeviceName, SampleFormat.PcmInt16, ref parameter, appletResourceUserId, processHandle);
+            ResultCode result = (ResultCode)_impl.OpenAudioOut(out outputDeviceName, out outputConfiguration, out AudioOutputSystem outSystem, memoryManager, inputDeviceName, SampleFormat.PcmInt16, ref parameter, appletResourceUserId, processHandle, volume);
 
             if (result == ResultCode.Success)
             {

+ 2 - 2
Ryujinx.HLE/HOS/Services/Audio/AudioOutManagerServer.cs

@@ -75,7 +75,7 @@ namespace Ryujinx.HLE.HOS.Services.Audio
 
             string inputDeviceName = MemoryHelper.ReadAsciiString(context.Memory, deviceNameInputPosition, (long)deviceNameInputSize);
 
-            ResultCode resultCode = _impl.OpenAudioOut(context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, inputDeviceName, ref inputConfiguration, appletResourceUserId, processHandle);
+            ResultCode resultCode = _impl.OpenAudioOut(context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, inputDeviceName, ref inputConfiguration, appletResourceUserId, processHandle, context.Device.Configuration.AudioVolume);
 
             if (resultCode == ResultCode.Success)
             {
@@ -142,7 +142,7 @@ namespace Ryujinx.HLE.HOS.Services.Audio
 
             string inputDeviceName = MemoryHelper.ReadAsciiString(context.Memory, deviceNameInputPosition, (long)deviceNameInputSize);
 
-            ResultCode resultCode = _impl.OpenAudioOut(context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, inputDeviceName, ref inputConfiguration, appletResourceUserId, processHandle);
+            ResultCode resultCode = _impl.OpenAudioOut(context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, inputDeviceName, ref inputConfiguration, appletResourceUserId, processHandle, context.Device.Configuration.AudioVolume);
 
             if (resultCode == ResultCode.Success)
             {

+ 1 - 1
Ryujinx.HLE/HOS/Services/Audio/AudioRendererManager.cs

@@ -35,7 +35,7 @@ namespace Ryujinx.HLE.HOS.Services.Audio
         {
             var memoryManager = context.Process.HandleTable.GetKProcess((int)processHandle).CpuMemory;
 
-            ResultCode result = (ResultCode)_impl.OpenAudioRenderer(out AudioRenderSystem renderer, memoryManager, ref parameter, appletResourceUserId, workBufferTransferMemory.Address, workBufferTransferMemory.Size, processHandle);
+            ResultCode result = (ResultCode)_impl.OpenAudioRenderer(out AudioRenderSystem renderer, memoryManager, ref parameter, appletResourceUserId, workBufferTransferMemory.Address, workBufferTransferMemory.Size, processHandle, context.Device.Configuration.AudioVolume);
 
             if (result == ResultCode.Success)
             {

+ 1 - 1
Ryujinx.HLE/HOS/Services/Audio/IAudioOutManager.cs

@@ -7,6 +7,6 @@ namespace Ryujinx.HLE.HOS.Services.Audio
     {
         public string[] ListAudioOuts();
 
-        public ResultCode OpenAudioOut(ServiceCtx context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, string inputDeviceName, ref AudioInputConfiguration parameter, ulong appletResourceUserId, uint processHandle);
+        public ResultCode OpenAudioOut(ServiceCtx context, out string outputDeviceName, out AudioOutputConfiguration outputConfiguration, out IAudioOut obj, string inputDeviceName, ref AudioInputConfiguration parameter, ulong appletResourceUserId, uint processHandle, float volume);
     }
 }

+ 15 - 0
Ryujinx.HLE/Switch.cs

@@ -146,6 +146,21 @@ namespace Ryujinx.HLE
             Gpu.Window.Present(swapBuffersCallback);
         }
 
+        public void SetVolume(float volume)
+        {
+            System.SetVolume(volume);
+        }
+
+        public float GetVolume()
+        {
+            return System.GetVolume();
+        }
+
+        public bool IsAudioMuted()
+        {
+            return System.GetVolume() == 0;
+        }
+
         public void DisposeGpu()
         {
             Gpu.Dispose();

+ 3 - 0
Ryujinx.Headless.SDL2/Options.cs

@@ -109,6 +109,9 @@ namespace Ryujinx.Headless.SDL2
         [Option("memory-manager-mode", Required = false, Default = MemoryManagerMode.HostMappedUnsafe, HelpText = "The selected memory manager mode.")]
         public MemoryManagerMode MemoryManagerMode { get; set; }
 
+        [Option("audio-volume", Required = false, Default = 1.0f, HelpText ="The audio level (0 to 1).")]
+        public float AudioVolume { get; set; }
+
         // Logging
 
         [Option("enable-file-logging", Required = false, Default = false, HelpText = "Enables logging to a file on disk.")]

+ 2 - 1
Ryujinx.Headless.SDL2/Program.cs

@@ -465,7 +465,8 @@ namespace Ryujinx.Headless.SDL2
                                                                   options.SystemTimeZone,
                                                                   options.MemoryManagerMode,
                                                                   (bool)options.IgnoreMissingServices,
-                                                                  options.AspectRatio);
+                                                                  options.AspectRatio,
+                                                                  options.AudioVolume);
 
             return new Switch(configuration);
         }

+ 6 - 1
Ryujinx/Configuration/ConfigurationFileFormat.cs

@@ -14,7 +14,7 @@ namespace Ryujinx.Configuration
         /// <summary>
         /// The current version of the file format
         /// </summary>
-        public const int CurrentVersion = 32;
+        public const int CurrentVersion = 33;
 
         public int Version { get; set; }
 
@@ -179,6 +179,11 @@ namespace Ryujinx.Configuration
         /// </summary>
         public AudioBackend AudioBackend { get; set; }
 
+        /// <summary>
+        /// The audio volume
+        /// </summary>
+        public float AudioVolume { get; set; }
+
         /// <summary>
         /// The selected memory manager mode
         /// </summary>

+ 28 - 0
Ryujinx/Configuration/ConfigurationState.cs

@@ -220,6 +220,11 @@ namespace Ryujinx.Configuration
             /// </summary>
             public ReactiveObject<AudioBackend> AudioBackend { get; private set; }
 
+            /// <summary>
+            /// The audio backend volume
+            /// </summary>
+            public ReactiveObject<float> AudioVolume { get; private set; }
+
             /// <summary>
             /// The selected memory manager mode
             /// </summary>
@@ -257,6 +262,8 @@ namespace Ryujinx.Configuration
                 ExpandRam.Event               += static (sender, e) => LogValueChange(sender, e, nameof(ExpandRam));
                 IgnoreMissingServices         = new ReactiveObject<bool>();
                 IgnoreMissingServices.Event   += static (sender, e) => LogValueChange(sender, e, nameof(IgnoreMissingServices));
+                AudioVolume                   = new ReactiveObject<float>();
+                AudioVolume.Event             += static (sender, e) => LogValueChange(sender, e, nameof(AudioVolume));
             }
         }
 
@@ -460,6 +467,7 @@ namespace Ryujinx.Configuration
                 EnableFsIntegrityChecks   = System.EnableFsIntegrityChecks,
                 FsGlobalAccessLogMode     = System.FsGlobalAccessLogMode,
                 AudioBackend              = System.AudioBackend,
+                AudioVolume               = System.AudioVolume,
                 MemoryManagerMode         = System.MemoryManagerMode,
                 ExpandRam                 = System.ExpandRam,
                 IgnoreMissingServices     = System.IgnoreMissingServices,
@@ -553,6 +561,7 @@ namespace Ryujinx.Configuration
             Hid.Hotkeys.Value = new KeyboardHotkeys
             {
                 ToggleVsync = Key.Tab,
+                ToggleMute = Key.F2,
                 Screenshot = Key.F8,
                 ShowUi = Key.F4,
                 Pause = Key.F5
@@ -929,6 +938,24 @@ namespace Ryujinx.Configuration
                 configurationFileUpdated = true;
             }
 
+            if (configurationFileFormat.Version < 33)
+            {
+                Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33.");
+
+                configurationFileFormat.Hotkeys = new KeyboardHotkeys
+                {
+                    ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
+                    Screenshot = configurationFileFormat.Hotkeys.Screenshot,
+                    ShowUi = configurationFileFormat.Hotkeys.ShowUi,
+                    Pause = configurationFileFormat.Hotkeys.Pause,
+                    ToggleMute = Key.F2
+                };
+
+                configurationFileFormat.AudioVolume = 1;
+
+                configurationFileUpdated = true;
+            }
+
             Logger.EnableFileLog.Value             = configurationFileFormat.EnableFileLog;
             Graphics.BackendThreading.Value        = configurationFileFormat.BackendThreading;
             Graphics.ResScale.Value                = configurationFileFormat.ResScale;
@@ -960,6 +987,7 @@ namespace Ryujinx.Configuration
             System.EnableFsIntegrityChecks.Value   = configurationFileFormat.EnableFsIntegrityChecks;
             System.FsGlobalAccessLogMode.Value     = configurationFileFormat.FsGlobalAccessLogMode;
             System.AudioBackend.Value              = configurationFileFormat.AudioBackend;
+            System.AudioVolume.Value               = configurationFileFormat.AudioVolume;
             System.MemoryManagerMode.Value         = configurationFileFormat.MemoryManagerMode;
             System.ExpandRam.Value                 = configurationFileFormat.ExpandRam;
             System.IgnoreMissingServices.Value     = configurationFileFormat.IgnoreMissingServices;

+ 38 - 7
Ryujinx/Ui/MainWindow.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Diagnostics;
 using System.IO;
 using System.Reflection;
@@ -132,6 +132,7 @@ namespace Ryujinx.Ui
         [GUI] ProgressBar     _progressBar;
         [GUI] Box             _viewBox;
         [GUI] Label           _vSyncStatus;
+        [GUI] Label           _volumeStatus;
         [GUI] Box             _listStatusBox;
         [GUI] Label           _loadingStatusLabel;
         [GUI] ProgressBar     _loadingStatusBar;
@@ -205,6 +206,7 @@ namespace Ryujinx.Ui
             ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
             ConfigurationState.Instance.Graphics.AspectRatio.Event         += UpdateAspectRatioState;
             ConfigurationState.Instance.System.EnableDockedMode.Event      += UpdateDockedModeState;
+            ConfigurationState.Instance.System.AudioVolume.Event           += UpdateAudioVolumeState; 
 
             if (ConfigurationState.Instance.Ui.StartFullscreen)
             {
@@ -305,6 +307,11 @@ namespace Ryujinx.Ui
             }
         }
 
+        private void UpdateAudioVolumeState(object sender, ReactiveEventArgs<float> e)
+        {
+            _emulationContext?.SetVolume(e.NewValue);
+        }
+
         private void WindowStateEvent_Changed(object o, WindowStateEventArgs args)
         {
             _fullScreen.Label = args.Event.NewWindowState.HasFlag(Gdk.WindowState.Fullscreen) ? "Exit Fullscreen" : "Enter Fullscreen";
@@ -562,7 +569,8 @@ namespace Ryujinx.Ui
                                                                           ConfigurationState.Instance.System.TimeZone,
                                                                           ConfigurationState.Instance.System.MemoryManagerMode,
                                                                           ConfigurationState.Instance.System.IgnoreMissingServices,
-                                                                          ConfigurationState.Instance.Graphics.AspectRatio);
+                                                                          ConfigurationState.Instance.Graphics.AspectRatio,
+                                                                          ConfigurationState.Instance.System.AudioVolume);
 
             _emulationContext = new HLE.Switch(configuration);
         }
@@ -1108,11 +1116,12 @@ namespace Ryujinx.Ui
         {
             Application.Invoke(delegate
             {
-                _gameStatus.Text  = args.GameStatus;
-                _fifoStatus.Text  = args.FifoStatus;
-                _gpuName.Text     = args.GpuName;
-                _dockedMode.Text  = args.DockedMode;
-                _aspectRatio.Text = args.AspectRatio;
+                _gameStatus.Text   = args.GameStatus;
+                _fifoStatus.Text   = args.FifoStatus;
+                _gpuName.Text      = args.GpuName;
+                _dockedMode.Text   = args.DockedMode;
+                _aspectRatio.Text  = args.AspectRatio;
+                _volumeStatus.Text = GetVolumeLabelText(args.Volume);
 
                 if (args.VSyncEnabled)
                 {
@@ -1173,6 +1182,28 @@ namespace Ryujinx.Ui
             ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
         }
 
+        private string GetVolumeLabelText(float volume)
+        {
+            string icon = volume == 0 ? "🔇" : "🔊";
+
+            return $"{icon} {(int)(volume * 100)}%";
+        }
+
+        private void VolumeStatus_Clicked(object sender, ButtonReleaseEventArgs args)
+        {
+            if (_emulationContext != null)
+            {
+                if (_emulationContext.IsAudioMuted())
+                {
+                    _emulationContext.SetVolume(ConfigurationState.Instance.System.AudioVolume);
+                }
+                else
+                {
+                    _emulationContext.SetVolume(0);
+                }
+            }
+        }
+
         private void AspectRatio_Clicked(object sender, ButtonReleaseEventArgs args)
         {
             AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;

+ 69 - 36
Ryujinx/Ui/MainWindow.glade

@@ -294,35 +294,35 @@
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
                     <child>
-						<object class="GtkMenuItem" id="_pauseEmulation">
-							<property name="visible">True</property>
-							<property name="can_focus">False</property>
-							<property name="tooltip_text" translatable="yes">Pause emulation</property>
-							<property name="label" translatable="yes">Pause Emulation</property>
-							<property name="use_underline">True</property>
-							<signal name="activate" handler="PauseEmulation_Pressed" swapped="no"/>
-						</object>
-					</child>
-					  <child>
-						  <object class="GtkMenuItem" id="_resumeEmulation">
-							  <property name="visible">True</property>
-							  <property name="can_focus">False</property>
-							  <property name="tooltip_text" translatable="yes">Resume emulation</property>
-							  <property name="label" translatable="yes">Resume Emulation</property>
-							  <property name="use_underline">True</property>
-							  <signal name="activate" handler="ResumeEmulation_Pressed" swapped="no"/>
-						  </object>
-					  </child>
-					  <child>
-						<object class="GtkMenuItem" id="_stopEmulation">
-							<property name="visible">True</property>
-							<property name="can_focus">False</property>
-							<property name="tooltip_text" translatable="yes">Stop emulation of the current game and return to game selection</property>
-							<property name="label" translatable="yes">Stop Emulation</property>
-							<property name="use_underline">True</property>
-							<signal name="activate" handler="StopEmulation_Pressed" swapped="no"/>
-						</object>
-					</child>
+                      <object class="GtkMenuItem" id="_pauseEmulation">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="tooltip_text" translatable="yes">Pause emulation</property>
+                        <property name="label" translatable="yes">Pause Emulation</property>
+                        <property name="use_underline">True</property>
+                        <signal name="activate" handler="PauseEmulation_Pressed" swapped="no"/>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkMenuItem" id="_resumeEmulation">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="tooltip_text" translatable="yes">Resume emulation</property>
+                        <property name="label" translatable="yes">Resume Emulation</property>
+                        <property name="use_underline">True</property>
+                        <signal name="activate" handler="ResumeEmulation_Pressed" swapped="no"/>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkMenuItem" id="_stopEmulation">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="tooltip_text" translatable="yes">Stop emulation of the current game and return to game selection</property>
+                        <property name="label" translatable="yes">Stop Emulation</property>
+                        <property name="use_underline">True</property>
+                        <signal name="activate" handler="StopEmulation_Pressed" swapped="no"/>
+                      </object>
+                    </child>
                     <child>
                       <object class="GtkSeparatorMenuItem">
                         <property name="visible">True</property>
@@ -647,14 +647,15 @@
                       <object class="GtkEventBox">
                         <property name="visible">True</property>
                         <property name="can_focus">False</property>
-                        <signal name="button_release_event" handler="AspectRatio_Clicked" swapped="no"/>
+                        <signal name="button_release_event" handler="VolumeStatus_Clicked" swapped="no"/>
                         <child>
-                          <object class="GtkLabel" id="_aspectRatio">
+                          <object class="GtkLabel" id="_volumeStatus">
                             <property name="visible">True</property>
                             <property name="can_focus">False</property>
                             <property name="halign">start</property>
                             <property name="margin_left">5</property>
                             <property name="margin_right">5</property>
+                            <property name="label" translatable="yes"></property>
                           </object>
                         </child>
                       </object>
@@ -675,6 +676,38 @@
                         <property name="position">5</property>
                       </packing>
                     </child>
+                    <child>
+                      <object class="GtkEventBox">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <signal name="button_release_event" handler="AspectRatio_Clicked" swapped="no"/>
+                        <child>
+                          <object class="GtkLabel" id="_aspectRatio">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="margin_left">5</property>
+                            <property name="margin_right">5</property>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">6</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSeparator">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">7</property>
+                      </packing>
+                    </child>
                     <child>
                       <object class="GtkLabel" id="_gameStatus">
                         <property name="visible">True</property>
@@ -686,7 +719,7 @@
                       <packing>
                         <property name="expand">False</property>
                         <property name="fill">True</property>
-                        <property name="position">6</property>
+                        <property name="position">8</property>
                       </packing>
                     </child>
                     <child>
@@ -697,7 +730,7 @@
                       <packing>
                         <property name="expand">False</property>
                         <property name="fill">True</property>
-                        <property name="position">7</property>
+                        <property name="position">9</property>
                       </packing>
                     </child>
                     <child>
@@ -711,7 +744,7 @@
                       <packing>
                         <property name="expand">False</property>
                         <property name="fill">True</property>
-                        <property name="position">8</property>
+                        <property name="position">10</property>
                       </packing>
                     </child>
                     <child>
@@ -722,7 +755,7 @@
                       <packing>
                         <property name="expand">False</property>
                         <property name="fill">True</property>
-                        <property name="position">9</property>
+                        <property name="position">11</property>
                       </packing>
                     </child>
                     <child>
@@ -736,7 +769,7 @@
                       <packing>
                         <property name="expand">True</property>
                         <property name="fill">True</property>
-                        <property name="position">10</property>
+                        <property name="position">12</property>
                       </packing>
                     </child>
                   </object>

+ 21 - 1
Ryujinx/Ui/RendererWidgetBase.cs

@@ -425,6 +425,7 @@ namespace Ryujinx.Ui
 
                         StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
                             Device.EnableDeviceVsync,
+                            Device.GetVolume(),
                             dockedMode,
                             ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
                             $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
@@ -598,6 +599,19 @@ namespace Ryujinx.Ui
                     (Toplevel as MainWindow)?.TogglePause();
                 }
 
+                if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute) &&
+                    !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute))
+                {
+                    if (Device.IsAudioMuted()) 
+                    {
+                        Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
+                    }
+                    else
+                    {
+                        Device.SetVolume(0);
+                    }
+                }
+
                 _prevHotkeyState = currentHotkeyState;
             }
 
@@ -627,7 +641,8 @@ namespace Ryujinx.Ui
             ToggleVSync = 1 << 0,
             Screenshot = 1 << 1,
             ShowUi = 1 << 2,
-            Pause = 1 << 3
+            Pause = 1 << 3,
+            ToggleMute = 1 << 4
         }
 
         private KeyboardHotkeyState GetHotkeyState()
@@ -654,6 +669,11 @@ namespace Ryujinx.Ui
                 state |= KeyboardHotkeyState.Pause;
             }
 
+            if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute))
+            {
+                state |= KeyboardHotkeyState.ToggleMute;
+            }
+
             return state;
         }
     }

+ 3 - 1
Ryujinx/Ui/StatusUpdatedEventArgs.cs

@@ -5,15 +5,17 @@ namespace Ryujinx.Ui
     public class StatusUpdatedEventArgs : EventArgs
     {
         public bool   VSyncEnabled;
+        public float  Volume;
         public string DockedMode;
         public string AspectRatio;
         public string GameStatus;
         public string FifoStatus;
         public string GpuName;
 
-        public StatusUpdatedEventArgs(bool vSyncEnabled, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName)
+        public StatusUpdatedEventArgs(bool vSyncEnabled, float volume, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName)
         {
             VSyncEnabled = vSyncEnabled;
+            Volume       = volume;
             DockedMode   = dockedMode;
             AspectRatio  = aspectRatio;
             GameStatus   = gameStatus;

+ 27 - 1
Ryujinx/Ui/Windows/SettingsWindow.cs

@@ -30,7 +30,8 @@ namespace Ryujinx.Ui.Windows
         private readonly TimeZoneContentManager _timeZoneContentManager;
         private readonly HashSet<string>        _validTzRegions;
 
-        private long _systemTimeOffset;
+        private long  _systemTimeOffset;
+        private float _previousVolumeLevel;
 
 #pragma warning disable CS0649, IDE0044
         [GUI] CheckButton     _errorLogToggle;
@@ -65,6 +66,8 @@ namespace Ryujinx.Ui.Windows
         [GUI] EntryCompletion _systemTimeZoneCompletion;
         [GUI] Box             _audioBackendBox;
         [GUI] ComboBox        _audioBackendSelect;
+        [GUI] Label           _audioVolumeLabel;
+        [GUI] Scale           _audioVolumeSlider;
         [GUI] SpinButton      _systemTimeYearSpin;
         [GUI] SpinButton      _systemTimeMonthSpin;
         [GUI] SpinButton      _systemTimeDaySpin;
@@ -364,6 +367,20 @@ namespace Ryujinx.Ui.Windows
             _audioBackendBox.Add(_audioBackendSelect);
             _audioBackendSelect.Show();
 
+            _previousVolumeLevel            = ConfigurationState.Instance.System.AudioVolume;
+            _audioVolumeLabel               = new Label("Volume: ");
+            _audioVolumeSlider              = new Scale(Orientation.Horizontal, 0, 100, 1);
+            _audioVolumeLabel.MarginStart   = 10;
+            _audioVolumeSlider.ValuePos     = PositionType.Right;
+            _audioVolumeSlider.WidthRequest = 200;
+
+            _audioVolumeSlider.Value        =  _previousVolumeLevel * 100;
+            _audioVolumeSlider.ValueChanged += VolumeSlider_OnChange;
+            _audioBackendBox.Add(_audioVolumeLabel);
+            _audioBackendBox.Add(_audioVolumeSlider);
+            _audioVolumeLabel.Show();
+            _audioVolumeSlider.Show();
+
             bool openAlIsSupported  = false;
             bool soundIoIsSupported = false;
             bool sdl2IsSupported    = false;
@@ -498,6 +515,9 @@ namespace Ryujinx.Ui.Windows
             ConfigurationState.Instance.Graphics.BackendThreading.Value        = backendThreading;
             ConfigurationState.Instance.Graphics.ResScale.Value                = int.Parse(_resScaleCombo.ActiveId);
             ConfigurationState.Instance.Graphics.ResScaleCustom.Value          = resScaleCustom;
+            ConfigurationState.Instance.System.AudioVolume.Value               = (float)_audioVolumeSlider.Value / 100.0f;
+
+            _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume.Value;
 
             if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter))
             {
@@ -651,6 +671,11 @@ namespace Ryujinx.Ui.Windows
             controllerWindow.Show();
         }
 
+        private void VolumeSlider_OnChange(object sender, EventArgs args)
+        {
+            ConfigurationState.Instance.System.AudioVolume.Value = (float)(_audioVolumeSlider.Value / 100);
+        }
+
         private void SaveToggle_Activated(object sender, EventArgs args)
         {
             SaveSettings();
@@ -664,6 +689,7 @@ namespace Ryujinx.Ui.Windows
 
         private void CloseToggle_Activated(object sender, EventArgs args)
         {
+            ConfigurationState.Instance.System.AudioVolume.Value = _previousVolumeLevel;
             Dispose();
         }
     }