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
This commit is contained in:
sharmander 2021-12-23 11:33:56 -05:00 committed by GitHub
parent e7c2dc8ec3
commit cb43cc7e32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 411 additions and 94 deletions

View file

@ -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) if (channelCount == 0)
{ {
@ -73,7 +73,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
throw new ArgumentException($"{channelCount}"); 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); _sessions.TryAdd(session, 0);

View file

@ -19,7 +19,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
private object _lock = new object(); 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; _driver = driver;
_queuedBuffers = new Queue<OpenALAudioBuffer>(); _queuedBuffers = new Queue<OpenALAudioBuffer>();
@ -27,6 +27,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
_targetFormat = GetALFormat(); _targetFormat = GetALFormat();
_isActive = false; _isActive = false;
_playedSampleCount = 0; _playedSampleCount = 0;
SetVolume(requestedVolume);
} }
private ALFormat GetALFormat() private ALFormat GetALFormat()

View file

@ -51,7 +51,7 @@ namespace Ryujinx.Audio.Backends.SDL2
return _pauseEvent; 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) if (channelCount == 0)
{ {
@ -68,7 +68,7 @@ namespace Ryujinx.Audio.Backends.SDL2
throw new NotImplementedException("Input direction is currently not implemented on SDL2 backend!"); 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); _sessions.TryAdd(session, 0);

View file

@ -26,7 +26,7 @@ namespace Ryujinx.Audio.Backends.SDL2
private float _volume; private float _volume;
private ushort _nativeSampleFormat; 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; _driver = driver;
_updateRequiredEvent = _driver.GetUpdateRequiredEvent(); _updateRequiredEvent = _driver.GetUpdateRequiredEvent();
@ -37,7 +37,7 @@ namespace Ryujinx.Audio.Backends.SDL2
_nativeSampleFormat = SDL2HardwareDeviceDriver.GetSDL2Format(RequestedSampleFormat); _nativeSampleFormat = SDL2HardwareDeviceDriver.GetSDL2Format(RequestedSampleFormat);
_sampleCount = uint.MaxValue; _sampleCount = uint.MaxValue;
_started = false; _started = false;
_volume = 1.0f; _volume = requestedVolume;
} }
private void EnsureAudioStreamSetup(AudioBuffer buffer) private void EnsureAudioStreamSetup(AudioBuffer buffer)
@ -82,7 +82,7 @@ namespace Ryujinx.Audio.Backends.SDL2
if (frameCount == 0) 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); streamSpan.Fill(0);
return; return;
@ -92,11 +92,16 @@ namespace Ryujinx.Audio.Backends.SDL2
_ringBuffer.Read(samples, 0, samples.Length); _ringBuffer.Read(samples, 0, samples.Length);
samples.AsSpan().CopyTo(streamSpan); fixed (byte* p = samples)
streamSpan.Slice(samples.Length).Fill(0); {
IntPtr pStreamSrc = (IntPtr)p;
// Apply volume to written data // Zero the dest buffer
SDL_MixAudioFormat(stream, stream, _nativeSampleFormat, (uint)samples.Length, (int)(_volume * SDL_MIX_MAXVOLUME)); streamSpan.Fill(0);
// Apply volume to written data
SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_volume * SDL_MIX_MAXVOLUME));
}
ulong sampleCount = GetSampleCount(samples.Length); ulong sampleCount = GetSampleCount(samples.Length);

View file

@ -130,7 +130,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
return _pauseEvent; 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) if (channelCount == 0)
{ {
@ -142,12 +142,14 @@ namespace Ryujinx.Audio.Backends.SoundIo
sampleRate = Constants.TargetSampleRate; sampleRate = Constants.TargetSampleRate;
} }
volume = Math.Clamp(volume, 0, 1);
if (direction != Direction.Output) if (direction != Direction.Output)
{ {
throw new NotImplementedException("Input direction is currently not implemented on SoundIO backend!"); 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); _sessions.TryAdd(session, 0);

View file

@ -19,21 +19,21 @@ namespace Ryujinx.Audio.Backends.SoundIo
private ManualResetEvent _updateRequiredEvent; private ManualResetEvent _updateRequiredEvent;
private int _disposeState; 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; _driver = driver;
_updateRequiredEvent = _driver.GetUpdateRequiredEvent(); _updateRequiredEvent = _driver.GetUpdateRequiredEvent();
_queuedBuffers = new ConcurrentQueue<SoundIoAudioBuffer>(); _queuedBuffers = new ConcurrentQueue<SoundIoAudioBuffer>();
_ringBuffer = new DynamicRingBuffer(); _ringBuffer = new DynamicRingBuffer();
SetupOutputStream(); SetupOutputStream(requestedVolume);
} }
private void SetupOutputStream() private void SetupOutputStream(float requestedVolume)
{ {
_outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount); _outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
_outputStream.WriteCallback += Update; _outputStream.WriteCallback += Update;
_outputStream.Volume = requestedVolume;
// TODO: Setup other callbacks (errors, ect). // TODO: Setup other callbacks (errors, ect).
_outputStream.Open(); _outputStream.Open();

View file

@ -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) if (channelCount == 0)
{ {
@ -80,6 +80,8 @@ namespace Ryujinx.Audio.Backends.CompatLayer
sampleRate = Constants.TargetSampleRate; sampleRate = Constants.TargetSampleRate;
} }
volume = Math.Clamp(volume, 0, 1);
if (!_realDriver.SupportsDirection(direction)) if (!_realDriver.SupportsDirection(direction))
{ {
if (direction == Direction.Input) if (direction == Direction.Input)
@ -94,7 +96,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
uint hardwareChannelCount = SelectHardwareChannelCount(channelCount); 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) if (hardwareChannelCount == channelCount)
{ {

View file

@ -35,7 +35,7 @@ namespace Ryujinx.Audio.Backends.Dummy
_pauseEvent = new ManualResetEvent(true); _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) if (sampleRate == 0)
{ {
@ -49,7 +49,7 @@ namespace Ryujinx.Audio.Backends.Dummy
if (direction == Direction.Output) if (direction == Direction.Output)
{ {
return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount); return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
} }
else else
{ {

View file

@ -30,9 +30,9 @@ namespace Ryujinx.Audio.Backends.Dummy
private ulong _playedSampleCount; 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; _manager = manager;
} }

View file

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

View file

@ -30,9 +30,9 @@ namespace Ryujinx.Audio.Integration
private byte[] _buffer; 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; _channelCount = channelCount;
_sampleRate = sampleRate; _sampleRate = sampleRate;
_currentBufferTag = 0; _currentBufferTag = 0;
@ -56,6 +56,16 @@ namespace Ryujinx.Audio.Integration
_currentBufferTag = _currentBufferTag % 4; _currentBufferTag = _currentBufferTag % 4;
} }
public void SetVolume(float volume)
{
_session.SetVolume(volume);
}
public float GetVolume()
{
return _session.GetVolume();
}
public uint GetChannelCount() public uint GetChannelCount()
{ {
return _channelCount; return _channelCount;

View file

@ -25,6 +25,18 @@ namespace Ryujinx.Audio.Integration
/// </summary> /// </summary>
public interface IHardwareDevice : IDisposable 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> /// <summary>
/// Get the supported sample rate of this device. /// Get the supported sample rate of this device.
/// </summary> /// </summary>

View file

@ -33,7 +33,7 @@ namespace Ryujinx.Audio.Integration
Output 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 GetUpdateRequiredEvent();
ManualResetEvent GetPauseEvent(); ManualResetEvent GetPauseEvent();

View file

@ -208,13 +208,14 @@ namespace Ryujinx.Audio.Output
SampleFormat sampleFormat, SampleFormat sampleFormat,
ref AudioInputConfiguration parameter, ref AudioInputConfiguration parameter,
ulong appletResourceUserId, ulong appletResourceUserId,
uint processHandle) uint processHandle,
float volume)
{ {
int sessionId = AcquireSessionId(); int sessionId = AcquireSessionId();
_sessionsBufferEvents[sessionId].Clear(); _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]); AudioOutputSystem audioOut = new AudioOutputSystem(this, _lock, deviceSession, _sessionsBufferEvents[sessionId]);
@ -247,6 +248,41 @@ namespace Ryujinx.Audio.Output
return result; 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() public void Dispose()
{ {
if (Interlocked.CompareExchange(ref _disposeState, 1, 0) == 0) if (Interlocked.CompareExchange(ref _disposeState, 1, 0) == 0)

View file

@ -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]; OutputDevices = new IHardwareDevice[Constants.AudioRendererSessionCountMax];
@ -89,7 +89,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
for (int i = 0; i < OutputDevices.Length; i++) for (int i = 0; i < OutputDevices.Length; i++)
{ {
// TODO: Don't hardcode sample rate. // 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>(); _mailbox = new Mailbox<MailboxMessage>();
@ -245,6 +245,33 @@ namespace Ryujinx.Audio.Renderer.Dsp
_mailbox.SendResponse(MailboxMessage.Stop); _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() public void Dispose()
{ {
Dispose(true); Dispose(true);

View file

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

View file

@ -76,6 +76,17 @@ namespace Ryujinx.Audio.Renderer.Utils
_stream.Flush(); _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() public uint GetChannelCount()
{ {
return _channelCount; return _channelCount;

View file

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

View file

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

View file

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

View file

@ -243,6 +243,7 @@ namespace Ryujinx.HLE.HOS
AudioOutputManager = new AudioOutputManager(); AudioOutputManager = new AudioOutputManager();
AudioInputManager = new AudioInputManager(); AudioInputManager = new AudioInputManager();
AudioRendererManager = new AudioRendererManager(); AudioRendererManager = new AudioRendererManager();
AudioRendererManager.SetVolume(Device.Configuration.AudioVolume);
AudioDeviceSessionRegistry = new VirtualDeviceSessionRegistry(); AudioDeviceSessionRegistry = new VirtualDeviceSessionRegistry();
IWritableEvent[] audioOutputRegisterBufferEvents = new IWritableEvent[Constants.AudioOutSessionCountMax]; IWritableEvent[] audioOutputRegisterBufferEvents = new IWritableEvent[Constants.AudioOutSessionCountMax];
@ -255,6 +256,7 @@ namespace Ryujinx.HLE.HOS
} }
AudioOutputManager.Initialize(Device.AudioDeviceDriver, audioOutputRegisterBufferEvents); AudioOutputManager.Initialize(Device.AudioDeviceDriver, audioOutputRegisterBufferEvents);
AudioOutputManager.SetVolume(Device.Configuration.AudioVolume);
IWritableEvent[] audioInputRegisterBufferEvents = new IWritableEvent[Constants.AudioInSessionCountMax]; 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() public void ReturnFocus()
{ {
AppletState.SetFocus(true); AppletState.SetFocus(true);

View file

@ -20,11 +20,11 @@ namespace Ryujinx.HLE.HOS.Services.Audio
return _impl.ListAudioOuts(); 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; 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) if (result == ResultCode.Success)
{ {

View file

@ -75,7 +75,7 @@ namespace Ryujinx.HLE.HOS.Services.Audio
string inputDeviceName = MemoryHelper.ReadAsciiString(context.Memory, deviceNameInputPosition, (long)deviceNameInputSize); 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) if (resultCode == ResultCode.Success)
{ {
@ -142,7 +142,7 @@ namespace Ryujinx.HLE.HOS.Services.Audio
string inputDeviceName = MemoryHelper.ReadAsciiString(context.Memory, deviceNameInputPosition, (long)deviceNameInputSize); 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) if (resultCode == ResultCode.Success)
{ {

View file

@ -35,7 +35,7 @@ namespace Ryujinx.HLE.HOS.Services.Audio
{ {
var memoryManager = context.Process.HandleTable.GetKProcess((int)processHandle).CpuMemory; 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) if (result == ResultCode.Success)
{ {

View file

@ -7,6 +7,6 @@ namespace Ryujinx.HLE.HOS.Services.Audio
{ {
public string[] ListAudioOuts(); 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);
} }
} }

View file

@ -146,6 +146,21 @@ namespace Ryujinx.HLE
Gpu.Window.Present(swapBuffersCallback); 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() public void DisposeGpu()
{ {
Gpu.Dispose(); Gpu.Dispose();

View file

@ -109,6 +109,9 @@ namespace Ryujinx.Headless.SDL2
[Option("memory-manager-mode", Required = false, Default = MemoryManagerMode.HostMappedUnsafe, HelpText = "The selected memory manager mode.")] [Option("memory-manager-mode", Required = false, Default = MemoryManagerMode.HostMappedUnsafe, HelpText = "The selected memory manager mode.")]
public MemoryManagerMode MemoryManagerMode { get; set; } 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 // Logging
[Option("enable-file-logging", Required = false, Default = false, HelpText = "Enables logging to a file on disk.")] [Option("enable-file-logging", Required = false, Default = false, HelpText = "Enables logging to a file on disk.")]

View file

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

View file

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

View file

@ -220,6 +220,11 @@ namespace Ryujinx.Configuration
/// </summary> /// </summary>
public ReactiveObject<AudioBackend> AudioBackend { get; private set; } public ReactiveObject<AudioBackend> AudioBackend { get; private set; }
/// <summary>
/// The audio backend volume
/// </summary>
public ReactiveObject<float> AudioVolume { get; private set; }
/// <summary> /// <summary>
/// The selected memory manager mode /// The selected memory manager mode
/// </summary> /// </summary>
@ -257,6 +262,8 @@ namespace Ryujinx.Configuration
ExpandRam.Event += static (sender, e) => LogValueChange(sender, e, nameof(ExpandRam)); ExpandRam.Event += static (sender, e) => LogValueChange(sender, e, nameof(ExpandRam));
IgnoreMissingServices = new ReactiveObject<bool>(); IgnoreMissingServices = new ReactiveObject<bool>();
IgnoreMissingServices.Event += static (sender, e) => LogValueChange(sender, e, nameof(IgnoreMissingServices)); 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, EnableFsIntegrityChecks = System.EnableFsIntegrityChecks,
FsGlobalAccessLogMode = System.FsGlobalAccessLogMode, FsGlobalAccessLogMode = System.FsGlobalAccessLogMode,
AudioBackend = System.AudioBackend, AudioBackend = System.AudioBackend,
AudioVolume = System.AudioVolume,
MemoryManagerMode = System.MemoryManagerMode, MemoryManagerMode = System.MemoryManagerMode,
ExpandRam = System.ExpandRam, ExpandRam = System.ExpandRam,
IgnoreMissingServices = System.IgnoreMissingServices, IgnoreMissingServices = System.IgnoreMissingServices,
@ -553,6 +561,7 @@ namespace Ryujinx.Configuration
Hid.Hotkeys.Value = new KeyboardHotkeys Hid.Hotkeys.Value = new KeyboardHotkeys
{ {
ToggleVsync = Key.Tab, ToggleVsync = Key.Tab,
ToggleMute = Key.F2,
Screenshot = Key.F8, Screenshot = Key.F8,
ShowUi = Key.F4, ShowUi = Key.F4,
Pause = Key.F5 Pause = Key.F5
@ -929,6 +938,24 @@ namespace Ryujinx.Configuration
configurationFileUpdated = true; 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; Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading;
Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScale.Value = configurationFileFormat.ResScale;
@ -960,6 +987,7 @@ namespace Ryujinx.Configuration
System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks; System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks;
System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode; System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode;
System.AudioBackend.Value = configurationFileFormat.AudioBackend; System.AudioBackend.Value = configurationFileFormat.AudioBackend;
System.AudioVolume.Value = configurationFileFormat.AudioVolume;
System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode; System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode;
System.ExpandRam.Value = configurationFileFormat.ExpandRam; System.ExpandRam.Value = configurationFileFormat.ExpandRam;
System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices; System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices;

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
@ -132,6 +132,7 @@ namespace Ryujinx.Ui
[GUI] ProgressBar _progressBar; [GUI] ProgressBar _progressBar;
[GUI] Box _viewBox; [GUI] Box _viewBox;
[GUI] Label _vSyncStatus; [GUI] Label _vSyncStatus;
[GUI] Label _volumeStatus;
[GUI] Box _listStatusBox; [GUI] Box _listStatusBox;
[GUI] Label _loadingStatusLabel; [GUI] Label _loadingStatusLabel;
[GUI] ProgressBar _loadingStatusBar; [GUI] ProgressBar _loadingStatusBar;
@ -205,6 +206,7 @@ namespace Ryujinx.Ui
ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState; ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState; ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState; ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
if (ConfigurationState.Instance.Ui.StartFullscreen) 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) private void WindowStateEvent_Changed(object o, WindowStateEventArgs args)
{ {
_fullScreen.Label = args.Event.NewWindowState.HasFlag(Gdk.WindowState.Fullscreen) ? "Exit Fullscreen" : "Enter Fullscreen"; _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.TimeZone,
ConfigurationState.Instance.System.MemoryManagerMode, ConfigurationState.Instance.System.MemoryManagerMode,
ConfigurationState.Instance.System.IgnoreMissingServices, ConfigurationState.Instance.System.IgnoreMissingServices,
ConfigurationState.Instance.Graphics.AspectRatio); ConfigurationState.Instance.Graphics.AspectRatio,
ConfigurationState.Instance.System.AudioVolume);
_emulationContext = new HLE.Switch(configuration); _emulationContext = new HLE.Switch(configuration);
} }
@ -1108,11 +1116,12 @@ namespace Ryujinx.Ui
{ {
Application.Invoke(delegate Application.Invoke(delegate
{ {
_gameStatus.Text = args.GameStatus; _gameStatus.Text = args.GameStatus;
_fifoStatus.Text = args.FifoStatus; _fifoStatus.Text = args.FifoStatus;
_gpuName.Text = args.GpuName; _gpuName.Text = args.GpuName;
_dockedMode.Text = args.DockedMode; _dockedMode.Text = args.DockedMode;
_aspectRatio.Text = args.AspectRatio; _aspectRatio.Text = args.AspectRatio;
_volumeStatus.Text = GetVolumeLabelText(args.Volume);
if (args.VSyncEnabled) if (args.VSyncEnabled)
{ {
@ -1173,6 +1182,28 @@ namespace Ryujinx.Ui
ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value; 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) private void AspectRatio_Clicked(object sender, ButtonReleaseEventArgs args)
{ {
AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value; AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;

View file

@ -294,35 +294,35 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<child> <child>
<object class="GtkMenuItem" id="_pauseEmulation"> <object class="GtkMenuItem" id="_pauseEmulation">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Pause emulation</property> <property name="tooltip_text" translatable="yes">Pause emulation</property>
<property name="label" translatable="yes">Pause Emulation</property> <property name="label" translatable="yes">Pause Emulation</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<signal name="activate" handler="PauseEmulation_Pressed" swapped="no"/> <signal name="activate" handler="PauseEmulation_Pressed" swapped="no"/>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkMenuItem" id="_resumeEmulation"> <object class="GtkMenuItem" id="_resumeEmulation">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Resume emulation</property> <property name="tooltip_text" translatable="yes">Resume emulation</property>
<property name="label" translatable="yes">Resume Emulation</property> <property name="label" translatable="yes">Resume Emulation</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<signal name="activate" handler="ResumeEmulation_Pressed" swapped="no"/> <signal name="activate" handler="ResumeEmulation_Pressed" swapped="no"/>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkMenuItem" id="_stopEmulation"> <object class="GtkMenuItem" id="_stopEmulation">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</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="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="label" translatable="yes">Stop Emulation</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<signal name="activate" handler="StopEmulation_Pressed" swapped="no"/> <signal name="activate" handler="StopEmulation_Pressed" swapped="no"/>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkSeparatorMenuItem"> <object class="GtkSeparatorMenuItem">
<property name="visible">True</property> <property name="visible">True</property>
@ -647,14 +647,15 @@
<object class="GtkEventBox"> <object class="GtkEventBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</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> <child>
<object class="GtkLabel" id="_aspectRatio"> <object class="GtkLabel" id="_volumeStatus">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="halign">start</property> <property name="halign">start</property>
<property name="margin_left">5</property> <property name="margin_left">5</property>
<property name="margin_right">5</property> <property name="margin_right">5</property>
<property name="label" translatable="yes"></property>
</object> </object>
</child> </child>
</object> </object>
@ -676,12 +677,19 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkLabel" id="_gameStatus"> <object class="GtkEventBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="halign">start</property> <signal name="button_release_event" handler="AspectRatio_Clicked" swapped="no"/>
<property name="margin_left">5</property> <child>
<property name="margin_right">5</property> <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> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -701,7 +709,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkLabel" id="_fifoStatus"> <object class="GtkLabel" id="_gameStatus">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="halign">start</property> <property name="halign">start</property>
@ -725,6 +733,31 @@
<property name="position">9</property> <property name="position">9</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkLabel" id="_fifoStatus">
<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>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">10</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">11</property>
</packing>
</child>
<child> <child>
<object class="GtkLabel" id="_gpuName"> <object class="GtkLabel" id="_gpuName">
<property name="visible">True</property> <property name="visible">True</property>
@ -736,7 +769,7 @@
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">10</property> <property name="position">12</property>
</packing> </packing>
</child> </child>
</object> </object>

View file

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

View file

@ -5,15 +5,17 @@ namespace Ryujinx.Ui
public class StatusUpdatedEventArgs : EventArgs public class StatusUpdatedEventArgs : EventArgs
{ {
public bool VSyncEnabled; public bool VSyncEnabled;
public float Volume;
public string DockedMode; public string DockedMode;
public string AspectRatio; public string AspectRatio;
public string GameStatus; public string GameStatus;
public string FifoStatus; public string FifoStatus;
public string GpuName; 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; VSyncEnabled = vSyncEnabled;
Volume = volume;
DockedMode = dockedMode; DockedMode = dockedMode;
AspectRatio = aspectRatio; AspectRatio = aspectRatio;
GameStatus = gameStatus; GameStatus = gameStatus;

View file

@ -30,7 +30,8 @@ namespace Ryujinx.Ui.Windows
private readonly TimeZoneContentManager _timeZoneContentManager; private readonly TimeZoneContentManager _timeZoneContentManager;
private readonly HashSet<string> _validTzRegions; private readonly HashSet<string> _validTzRegions;
private long _systemTimeOffset; private long _systemTimeOffset;
private float _previousVolumeLevel;
#pragma warning disable CS0649, IDE0044 #pragma warning disable CS0649, IDE0044
[GUI] CheckButton _errorLogToggle; [GUI] CheckButton _errorLogToggle;
@ -65,6 +66,8 @@ namespace Ryujinx.Ui.Windows
[GUI] EntryCompletion _systemTimeZoneCompletion; [GUI] EntryCompletion _systemTimeZoneCompletion;
[GUI] Box _audioBackendBox; [GUI] Box _audioBackendBox;
[GUI] ComboBox _audioBackendSelect; [GUI] ComboBox _audioBackendSelect;
[GUI] Label _audioVolumeLabel;
[GUI] Scale _audioVolumeSlider;
[GUI] SpinButton _systemTimeYearSpin; [GUI] SpinButton _systemTimeYearSpin;
[GUI] SpinButton _systemTimeMonthSpin; [GUI] SpinButton _systemTimeMonthSpin;
[GUI] SpinButton _systemTimeDaySpin; [GUI] SpinButton _systemTimeDaySpin;
@ -364,6 +367,20 @@ namespace Ryujinx.Ui.Windows
_audioBackendBox.Add(_audioBackendSelect); _audioBackendBox.Add(_audioBackendSelect);
_audioBackendSelect.Show(); _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 openAlIsSupported = false;
bool soundIoIsSupported = false; bool soundIoIsSupported = false;
bool sdl2IsSupported = false; bool sdl2IsSupported = false;
@ -498,6 +515,9 @@ namespace Ryujinx.Ui.Windows
ConfigurationState.Instance.Graphics.BackendThreading.Value = backendThreading; ConfigurationState.Instance.Graphics.BackendThreading.Value = backendThreading;
ConfigurationState.Instance.Graphics.ResScale.Value = int.Parse(_resScaleCombo.ActiveId); ConfigurationState.Instance.Graphics.ResScale.Value = int.Parse(_resScaleCombo.ActiveId);
ConfigurationState.Instance.Graphics.ResScaleCustom.Value = resScaleCustom; 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)) if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter))
{ {
@ -651,6 +671,11 @@ namespace Ryujinx.Ui.Windows
controllerWindow.Show(); 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) private void SaveToggle_Activated(object sender, EventArgs args)
{ {
SaveSettings(); SaveSettings();
@ -664,6 +689,7 @@ namespace Ryujinx.Ui.Windows
private void CloseToggle_Activated(object sender, EventArgs args) private void CloseToggle_Activated(object sender, EventArgs args)
{ {
ConfigurationState.Instance.System.AudioVolume.Value = _previousVolumeLevel;
Dispose(); Dispose();
} }
} }