diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9e3e9c97d..090d0272b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -30,6 +30,7 @@
+
diff --git a/Ryujinx.sln b/Ryujinx.sln
index deddb97a0..0ea302f66 100644
--- a/Ryujinx.sln
+++ b/Ryujinx.sln
@@ -59,6 +59,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Input", "src\Ryujin
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Input.SDL3", "src\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj", "{D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Input.Midi", "src\Ryujinx.Input.Midi\Ryujinx.Input.Midi.csproj", "{2E49F4FB-4ABA-49A5-9292-8C969F22F44E}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec.FFmpeg", "src\Ryujinx.Graphics.Nvdec.FFmpeg\Ryujinx.Graphics.Nvdec.FFmpeg.csproj", "{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
@@ -392,6 +394,18 @@ Global
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Release|x64.Build.0 = Release|Any CPU
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Release|x86.ActiveCfg = Release|Any CPU
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Release|x86.Build.0 = Release|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Debug|x64.Build.0 = Debug|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Debug|x86.Build.0 = Debug|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Release|x64.ActiveCfg = Release|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Release|x64.Build.0 = Release|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Release|x86.ActiveCfg = Release|Any CPU
+ {2E49F4FB-4ABA-49A5-9292-8C969F22F44E}.Release|x86.Build.0 = Release|Any CPU
{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}.Debug|x64.ActiveCfg = Debug|Any CPU
diff --git a/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs b/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs
index c3336dc64..b1b9cd2f2 100644
--- a/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs
+++ b/src/Ryujinx.Common/Configuration/Hid/InputBackendType.cs
@@ -7,6 +7,7 @@ namespace Ryujinx.Common.Configuration.Hid
{
Invalid,
WindowKeyboard,
+ Midi,
GamepadSDL2, //backcompat
GamepadSDL3,
}
diff --git a/src/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs b/src/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs
index bd8be2491..74abcf71f 100644
--- a/src/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs
+++ b/src/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs
@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Configuration.Hid.Midi;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid
@@ -7,6 +8,7 @@ namespace Ryujinx.Common.Configuration.Hid
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(InputConfig))]
[JsonSerializable(typeof(StandardKeyboardInputConfig))]
+ [JsonSerializable(typeof(StandardMidiInputConfig))]
[JsonSerializable(typeof(StandardControllerInputConfig))]
public partial class InputConfigJsonSerializerContext : JsonSerializerContext
{
diff --git a/src/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs b/src/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs
index eadaab492..a2c3c5a61 100644
--- a/src/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs
+++ b/src/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs
@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Configuration.Hid.Midi;
using Ryujinx.Common.Utilities;
using System;
using System.Text.Json;
@@ -58,6 +59,7 @@ namespace Ryujinx.Common.Configuration.Hid
return backendType switch
{
InputBackendType.WindowKeyboard => JsonSerializer.Deserialize(ref reader, _serializerContext.StandardKeyboardInputConfig),
+ InputBackendType.Midi => JsonSerializer.Deserialize(ref reader, _serializerContext.StandardMidiInputConfig),
InputBackendType.GamepadSDL2 or InputBackendType.GamepadSDL3 => JsonSerializer.Deserialize(ref reader, _serializerContext.StandardControllerInputConfig),
_ => throw new InvalidOperationException($"Unknown backend type {backendType}"),
};
@@ -70,6 +72,9 @@ namespace Ryujinx.Common.Configuration.Hid
case InputBackendType.WindowKeyboard:
JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, _serializerContext.StandardKeyboardInputConfig);
break;
+ case InputBackendType.Midi:
+ JsonSerializer.Serialize(writer, value as StandardMidiInputConfig, _serializerContext.StandardMidiInputConfig);
+ break;
case InputBackendType.GamepadSDL2 or InputBackendType.GamepadSDL3:
JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, _serializerContext.StandardControllerInputConfig);
break;
diff --git a/src/Ryujinx.Common/Configuration/Hid/Midi/MidiBinding.cs b/src/Ryujinx.Common/Configuration/Hid/Midi/MidiBinding.cs
new file mode 100644
index 000000000..8219062b0
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Midi/MidiBinding.cs
@@ -0,0 +1,42 @@
+using System;
+
+namespace Ryujinx.Common.Configuration.Hid.Midi
+{
+ public struct MidiBinding : IEquatable
+ {
+ public MidiBindingKind Kind { get; set; }
+ public byte Number { get; set; }
+ public byte Channel { get; set; }
+ public byte Threshold { get; set; }
+
+ public readonly bool IsUnbound => Kind == MidiBindingKind.Unbound;
+
+ public readonly bool Equals(MidiBinding other)
+ {
+ return Kind == other.Kind &&
+ Number == other.Number &&
+ Channel == other.Channel &&
+ Threshold == other.Threshold;
+ }
+
+ public override readonly bool Equals(object obj)
+ {
+ return obj is MidiBinding other && Equals(other);
+ }
+
+ public override readonly int GetHashCode()
+ {
+ return HashCode.Combine((byte)Kind, Number, Channel, Threshold);
+ }
+
+ public static bool operator ==(MidiBinding left, MidiBinding right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(MidiBinding left, MidiBinding right)
+ {
+ return !left.Equals(right);
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Midi/MidiBindingKind.cs b/src/Ryujinx.Common/Configuration/Hid/Midi/MidiBindingKind.cs
new file mode 100644
index 000000000..68f26478a
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Midi/MidiBindingKind.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Configuration.Hid.Midi
+{
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum MidiBindingKind : byte
+ {
+ Unbound,
+ Note,
+ ControlChange,
+ }
+}
diff --git a/src/Ryujinx.Common/Configuration/Hid/Midi/StandardMidiInputConfig.cs b/src/Ryujinx.Common/Configuration/Hid/Midi/StandardMidiInputConfig.cs
new file mode 100644
index 000000000..0b3bb4558
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/Hid/Midi/StandardMidiInputConfig.cs
@@ -0,0 +1,6 @@
+using Ryujinx.Common.Configuration.Hid.Keyboard;
+
+namespace Ryujinx.Common.Configuration.Hid.Midi
+{
+ public class StandardMidiInputConfig : GenericKeyboardInputConfig { }
+}
diff --git a/src/Ryujinx.Input.Midi/Assigner/MidiBindingAssigner.cs b/src/Ryujinx.Input.Midi/Assigner/MidiBindingAssigner.cs
new file mode 100644
index 000000000..62acb05f1
--- /dev/null
+++ b/src/Ryujinx.Input.Midi/Assigner/MidiBindingAssigner.cs
@@ -0,0 +1,58 @@
+using Ryujinx.Common.Configuration.Hid.Midi;
+
+namespace Ryujinx.Input.Midi.Assigner
+{
+ public sealed class MidiBindingAssigner
+ {
+ private readonly IMidiGamepad _gamepad;
+ private MidiCapturedInput? _capturedInput;
+
+ public MidiBindingAssigner(IMidiGamepad gamepad)
+ {
+ _gamepad = gamepad;
+ }
+
+ public void Initialize()
+ {
+ _gamepad?.ResetCapturedInputs();
+ _capturedInput = null;
+ }
+
+ public void ReadInput()
+ {
+ if (_gamepad != null && _gamepad.TryDequeueCapturedInput(out MidiCapturedInput input))
+ {
+ _capturedInput = input;
+ }
+ }
+
+ public bool IsAnyButtonPressed()
+ {
+ return _capturedInput.HasValue;
+ }
+
+ public bool ShouldCancel()
+ {
+ return _gamepad == null || !_gamepad.IsConnected;
+ }
+
+ public MidiBinding? GetPressedBinding(bool captureAnyChannel, byte threshold)
+ {
+ if (!_capturedInput.HasValue)
+ {
+ return null;
+ }
+
+ MidiCapturedInput input = _capturedInput.Value;
+ _capturedInput = null;
+
+ return new MidiBinding
+ {
+ Kind = input.Kind,
+ Number = input.Number,
+ Channel = captureAnyChannel ? (byte)0 : input.Channel,
+ Threshold = threshold,
+ };
+ }
+ }
+}
diff --git a/src/Ryujinx.Input.Midi/IMidiGamepad.cs b/src/Ryujinx.Input.Midi/IMidiGamepad.cs
new file mode 100644
index 000000000..360dd0414
--- /dev/null
+++ b/src/Ryujinx.Input.Midi/IMidiGamepad.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.Input.Midi
+{
+ public interface IMidiGamepad : IGamepad
+ {
+ bool TryDequeueCapturedInput(out MidiCapturedInput input);
+ void ResetCapturedInputs();
+ }
+}
diff --git a/src/Ryujinx.Input.Midi/MidiCapturedInput.cs b/src/Ryujinx.Input.Midi/MidiCapturedInput.cs
new file mode 100644
index 000000000..ac13e332c
--- /dev/null
+++ b/src/Ryujinx.Input.Midi/MidiCapturedInput.cs
@@ -0,0 +1,20 @@
+using Ryujinx.Common.Configuration.Hid.Midi;
+
+namespace Ryujinx.Input.Midi
+{
+ public readonly struct MidiCapturedInput
+ {
+ public MidiBindingKind Kind { get; }
+ public byte Number { get; }
+ public byte Channel { get; }
+ public byte Value { get; }
+
+ public MidiCapturedInput(MidiBindingKind kind, byte number, byte channel, byte value)
+ {
+ Kind = kind;
+ Number = number;
+ Channel = channel;
+ Value = value;
+ }
+ }
+}
diff --git a/src/Ryujinx.Input.Midi/MidiDeviceConnection.cs b/src/Ryujinx.Input.Midi/MidiDeviceConnection.cs
new file mode 100644
index 000000000..47e57d335
--- /dev/null
+++ b/src/Ryujinx.Input.Midi/MidiDeviceConnection.cs
@@ -0,0 +1,58 @@
+using Melanchall.DryWetMidi.Core;
+using Melanchall.DryWetMidi.Multimedia;
+using System;
+
+namespace Ryujinx.Input.Midi
+{
+ internal sealed class MidiDeviceConnection : IDisposable
+ {
+ private readonly InputDevice _device;
+
+ public MidiDeviceState State { get; } = new();
+
+ public bool IsConnected { get; private set; }
+
+ public MidiDeviceConnection(InputDevice device)
+ {
+ _device = device;
+ _device.EventReceived += HandleEventReceived;
+ _device.StartEventsListening();
+ IsConnected = true;
+ }
+
+ private void HandleEventReceived(object sender, MidiEventReceivedEventArgs e)
+ {
+ switch (e.Event)
+ {
+ case NoteOnEvent noteOn:
+ {
+ byte channel = (byte)noteOn.Channel;
+ byte noteNumber = (byte)noteOn.NoteNumber;
+ byte velocity = (byte)noteOn.Velocity;
+
+ State.SetNoteValue(channel, noteNumber, velocity);
+ break;
+ }
+ case NoteOffEvent noteOff:
+ State.SetNoteValue((byte)noteOff.Channel, (byte)noteOff.NoteNumber, 0);
+ break;
+ case ControlChangeEvent controlChange:
+ State.SetControlValue((byte)controlChange.Channel, (byte)controlChange.ControlNumber, (byte)controlChange.ControlValue);
+ break;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (!IsConnected)
+ {
+ return;
+ }
+
+ IsConnected = false;
+ _device.EventReceived -= HandleEventReceived;
+ _device.StopEventsListening();
+ _device.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Input.Midi/MidiDeviceState.cs b/src/Ryujinx.Input.Midi/MidiDeviceState.cs
new file mode 100644
index 000000000..c4bfa38be
--- /dev/null
+++ b/src/Ryujinx.Input.Midi/MidiDeviceState.cs
@@ -0,0 +1,94 @@
+using Ryujinx.Common.Configuration.Hid.Midi;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Ryujinx.Input.Midi
+{
+ internal class MidiDeviceState
+ {
+ private readonly byte[,] _noteVelocities = new byte[16, 128];
+ private readonly byte[,] _controlValues = new byte[16, 128];
+ private readonly Queue _capturedInputs = [];
+ private readonly Lock _lock = new();
+
+ public void SetNoteValue(byte channel, byte note, byte velocity)
+ {
+ lock (_lock)
+ {
+ _noteVelocities[channel, note] = velocity;
+
+ if (velocity > 0)
+ {
+ _capturedInputs.Enqueue(new MidiCapturedInput(MidiBindingKind.Note, note, (byte)(channel + 1), velocity));
+ }
+ }
+ }
+
+ public void SetControlValue(byte channel, byte control, byte value)
+ {
+ lock (_lock)
+ {
+ _controlValues[channel, control] = value;
+ _capturedInputs.Enqueue(new MidiCapturedInput(MidiBindingKind.ControlChange, control, (byte)(channel + 1), value));
+ }
+ }
+
+ public bool IsBindingActive(MidiBinding binding)
+ {
+ if (binding.IsUnbound)
+ {
+ return false;
+ }
+
+ lock (_lock)
+ {
+ int threshold = binding.Threshold == 0 ? 1 : binding.Threshold;
+
+ for (int channelIndex = 0; channelIndex < 16; channelIndex++)
+ {
+ if (binding.Channel != 0 && binding.Channel != channelIndex + 1)
+ {
+ continue;
+ }
+
+ int currentValue = binding.Kind switch
+ {
+ MidiBindingKind.Note => _noteVelocities[channelIndex, binding.Number],
+ MidiBindingKind.ControlChange => _controlValues[channelIndex, binding.Number],
+ _ => 0,
+ };
+
+ if (currentValue >= threshold)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ public bool TryDequeueCapturedInput(out MidiCapturedInput input)
+ {
+ lock (_lock)
+ {
+ if (_capturedInputs.Count > 0)
+ {
+ input = _capturedInputs.Dequeue();
+ return true;
+ }
+ }
+
+ input = default;
+ return false;
+ }
+
+ public void ResetCapturedInputs()
+ {
+ lock (_lock)
+ {
+ _capturedInputs.Clear();
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Input.Midi/MidiGamepad.cs b/src/Ryujinx.Input.Midi/MidiGamepad.cs
new file mode 100644
index 000000000..ed8f64518
--- /dev/null
+++ b/src/Ryujinx.Input.Midi/MidiGamepad.cs
@@ -0,0 +1,185 @@
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Midi;
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Threading;
+
+namespace Ryujinx.Input.Midi
+{
+ public sealed class MidiGamepad : IMidiGamepad
+ {
+ private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, MidiBinding From)
+ {
+ public bool IsValid => To is not GamepadButtonInputId.Unbound && !From.IsUnbound;
+ }
+
+ private readonly MidiDeviceConnection _connection;
+ private readonly List _buttonsUserMapping = [];
+ private readonly Lock _userMappingLock = new();
+
+ private StandardMidiInputConfig _configuration;
+
+ public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
+ public string Id { get; }
+ public string Name { get; }
+ public bool IsConnected => _connection.IsConnected;
+
+ internal MidiGamepad(string id, string name, MidiDeviceConnection connection)
+ {
+ Id = id;
+ Name = name;
+ _connection = connection;
+ }
+
+ public bool IsPressed(GamepadButtonInputId inputId)
+ {
+ return false;
+ }
+
+ public (float, float) GetStick(StickInputId inputId)
+ {
+ return (0, 0);
+ }
+
+ public Vector3 GetMotionData(MotionInputId inputId)
+ {
+ return Vector3.Zero;
+ }
+
+ public void SetTriggerThreshold(float triggerThreshold)
+ {
+ }
+
+ public void SetConfiguration(InputConfig configuration)
+ {
+ lock (_userMappingLock)
+ {
+ _configuration = (StandardMidiInputConfig)configuration;
+ _buttonsUserMapping.Clear();
+
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, _configuration.LeftJoyconStick.StickButton));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, _configuration.LeftJoycon.DpadUp));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, _configuration.LeftJoycon.DpadDown));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, _configuration.LeftJoycon.DpadLeft));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, _configuration.LeftJoycon.DpadRight));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, _configuration.LeftJoycon.ButtonMinus));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, _configuration.LeftJoycon.ButtonL));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, _configuration.LeftJoycon.ButtonZl));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, _configuration.LeftJoycon.ButtonSr));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, _configuration.LeftJoycon.ButtonSl));
+
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, _configuration.RightJoyconStick.StickButton));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, _configuration.RightJoycon.ButtonA));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, _configuration.RightJoycon.ButtonB));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, _configuration.RightJoycon.ButtonX));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, _configuration.RightJoycon.ButtonY));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, _configuration.RightJoycon.ButtonPlus));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, _configuration.RightJoycon.ButtonR));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, _configuration.RightJoycon.ButtonZr));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, _configuration.RightJoycon.ButtonSr));
+ _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, _configuration.RightJoycon.ButtonSl));
+ }
+ }
+
+ public void SetLed(uint packedRgb)
+ {
+ }
+
+ public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
+ {
+ }
+
+ public GamepadStateSnapshot GetMappedStateSnapshot()
+ {
+ GamepadStateSnapshot result = default;
+
+ lock (_userMappingLock)
+ {
+ if (_configuration == null)
+ {
+ return result;
+ }
+
+ foreach (ButtonMappingEntry entry in _buttonsUserMapping)
+ {
+ if (!entry.IsValid || result.IsPressed(entry.To))
+ {
+ continue;
+ }
+
+ result.SetPressed(entry.To, _connection.State.IsBindingActive(entry.From));
+ }
+
+ (short leftStickX, short leftStickY) = GetStickValues(_configuration.LeftJoyconStick);
+ (short rightStickX, short rightStickY) = GetStickValues(_configuration.RightJoyconStick);
+
+ result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
+ result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
+ }
+
+ return result;
+ }
+
+ public GamepadStateSnapshot GetStateSnapshot()
+ {
+ return GetMappedStateSnapshot();
+ }
+
+ public bool TryDequeueCapturedInput(out MidiCapturedInput input)
+ {
+ return _connection.State.TryDequeueCapturedInput(out input);
+ }
+
+ public void ResetCapturedInputs()
+ {
+ _connection.State.ResetCapturedInputs();
+ }
+
+ public void Dispose()
+ {
+ _connection.Dispose();
+ }
+
+ private static float ConvertRawStickValue(short value)
+ {
+ const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
+
+ return value * ConvertRate;
+ }
+
+ private (short, short) GetStickValues(Ryujinx.Common.Configuration.Hid.Keyboard.JoyconConfigKeyboardStick stickConfig)
+ {
+ short stickX = 0;
+ short stickY = 0;
+
+ if (_connection.State.IsBindingActive(stickConfig.StickUp))
+ {
+ stickY += 1;
+ }
+
+ if (_connection.State.IsBindingActive(stickConfig.StickDown))
+ {
+ stickY -= 1;
+ }
+
+ if (_connection.State.IsBindingActive(stickConfig.StickRight))
+ {
+ stickX += 1;
+ }
+
+ if (_connection.State.IsBindingActive(stickConfig.StickLeft))
+ {
+ stickX -= 1;
+ }
+
+ Vector2 stick = new(stickX, stickY);
+ if (stick != Vector2.Zero)
+ {
+ stick = Vector2.Normalize(stick);
+ }
+
+ return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
+ }
+ }
+}
diff --git a/src/Ryujinx.Input.Midi/MidiGamepadDriver.cs b/src/Ryujinx.Input.Midi/MidiGamepadDriver.cs
new file mode 100644
index 000000000..e5ae144b4
--- /dev/null
+++ b/src/Ryujinx.Input.Midi/MidiGamepadDriver.cs
@@ -0,0 +1,101 @@
+using Melanchall.DryWetMidi.Multimedia;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Input.Midi
+{
+ public sealed class MidiGamepadDriver : IGamepadDriver
+ {
+ public string DriverName => "MIDI";
+
+ public ReadOnlySpan GamepadsIds => EnumerateDeviceIds().ToArray();
+
+ public event Action OnGamepadConnected
+ {
+ add { }
+ remove { }
+ }
+
+ public event Action OnGamepadDisconnected
+ {
+ add { }
+ remove { }
+ }
+
+ public bool IsSupported => OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
+
+ public string GetDeviceName(string id)
+ {
+ return TryGetDeviceInfo(id, out _, out string name) ? name : null;
+ }
+
+ public IGamepad GetGamepad(string id)
+ {
+ if (!TryGetDeviceInfo(id, out int index, out string name))
+ {
+ return null;
+ }
+
+ InputDevice device = InputDevice.GetByIndex(index);
+
+ if (device == null)
+ {
+ return null;
+ }
+
+ return new MidiGamepad(id, name, new MidiDeviceConnection(device));
+ }
+
+ public IEnumerable GetGamepads()
+ {
+ foreach (string id in EnumerateDeviceIds())
+ {
+ IGamepad gamepad = GetGamepad(id);
+
+ if (gamepad != null)
+ {
+ yield return gamepad;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ }
+
+ private IEnumerable EnumerateDeviceIds()
+ {
+ if (!IsSupported)
+ {
+ yield break;
+ }
+
+ for (int index = 0; index < InputDevice.GetDevicesCount(); index++)
+ {
+ yield return index.ToString();
+ }
+ }
+
+ private bool TryGetDeviceInfo(string id, out int index, out string name)
+ {
+ index = -1;
+ name = null;
+
+ if (!IsSupported || !int.TryParse(id, out index))
+ {
+ return false;
+ }
+
+ if (index < 0 || index >= InputDevice.GetDevicesCount())
+ {
+ return false;
+ }
+
+ using InputDevice device = InputDevice.GetByIndex(index);
+ name = device?.Name;
+
+ return device != null;
+ }
+ }
+}
diff --git a/src/Ryujinx.Input.Midi/Ryujinx.Input.Midi.csproj b/src/Ryujinx.Input.Midi/Ryujinx.Input.Midi.csproj
new file mode 100644
index 000000000..997b6300e
--- /dev/null
+++ b/src/Ryujinx.Input.Midi/Ryujinx.Input.Midi.csproj
@@ -0,0 +1,16 @@
+
+
+
+ true
+ $(DefaultItemExcludes);._*
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx.Input/HLE/InputManager.cs b/src/Ryujinx.Input/HLE/InputManager.cs
index 2825542a0..a2cb0f9b4 100644
--- a/src/Ryujinx.Input/HLE/InputManager.cs
+++ b/src/Ryujinx.Input/HLE/InputManager.cs
@@ -1,14 +1,32 @@
+using Ryujinx.Common.Configuration.Hid;
using System;
+using System.Collections.Generic;
namespace Ryujinx.Input.HLE
{
- public class InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver)
+ public class InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver midiDriver = null)
: IDisposable
{
+ private readonly Dictionary _drivers = new()
+ {
+ { InputBackendType.WindowKeyboard, keyboardDriver },
+ { InputBackendType.Midi, midiDriver },
+ { InputBackendType.GamepadSDL2, gamepadDriver },
+ { InputBackendType.GamepadSDL3, gamepadDriver },
+ };
+
public IGamepadDriver KeyboardDriver { get; } = keyboardDriver;
public IGamepadDriver GamepadDriver { get; } = gamepadDriver;
+ public IGamepadDriver MidiDriver { get; } = midiDriver;
public IGamepadDriver MouseDriver { get; private set; }
+ public IGamepadDriver GetDriver(InputBackendType backend)
+ {
+ _drivers.TryGetValue(backend, out IGamepadDriver driver);
+
+ return driver;
+ }
+
public void SetMouseDriver(IGamepadDriver mouseDriver)
{
MouseDriver?.Dispose();
@@ -18,7 +36,7 @@ namespace Ryujinx.Input.HLE
public NpadManager CreateNpadManager()
{
- return new NpadManager(KeyboardDriver, GamepadDriver, MouseDriver);
+ return new NpadManager(this, MouseDriver);
}
public TouchScreenManager CreateTouchScreenManager()
@@ -37,6 +55,7 @@ namespace Ryujinx.Input.HLE
{
KeyboardDriver?.Dispose();
GamepadDriver?.Dispose();
+ MidiDriver?.Dispose();
MouseDriver?.Dispose();
}
}
diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs
index f2936aa72..65b97391d 100644
--- a/src/Ryujinx.Input/HLE/NpadManager.cs
+++ b/src/Ryujinx.Input/HLE/NpadManager.cs
@@ -1,7 +1,5 @@
using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid;
-using Ryujinx.Common.Configuration.Hid.Controller;
-using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Buffers;
@@ -30,6 +28,7 @@ namespace Ryujinx.Input.HLE
private readonly NpadController[] _controllers;
+ private readonly InputManager _inputManager;
private readonly IGamepadDriver _keyboardDriver;
private readonly IGamepadDriver _gamepadDriver;
private readonly IGamepadDriver _mouseDriver;
@@ -43,13 +42,14 @@ namespace Ryujinx.Input.HLE
private readonly List _hleInputStates = [];
private readonly List _hleMotionStates = new(NpadDevices.MaxControllers);
- public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver)
+ public NpadManager(InputManager inputManager, IGamepadDriver mouseDriver)
{
_controllers = new NpadController[MaxControllers];
_cemuHookClient = new CemuHookClient(this);
- _keyboardDriver = keyboardDriver;
- _gamepadDriver = gamepadDriver;
+ _inputManager = inputManager;
+ _keyboardDriver = inputManager.KeyboardDriver;
+ _gamepadDriver = inputManager.GamepadDriver;
_mouseDriver = mouseDriver;
_inputConfig = [];
@@ -102,16 +102,7 @@ namespace Ryujinx.Input.HLE
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config)
{
- IGamepadDriver targetDriver = _gamepadDriver;
-
- if (config is StandardControllerInputConfig)
- {
- targetDriver = _gamepadDriver;
- }
- else if (config is StandardKeyboardInputConfig)
- {
- targetDriver = _keyboardDriver;
- }
+ IGamepadDriver targetDriver = _inputManager.GetDriver(config.Backend);
Debug.Assert(targetDriver != null, "Unknown input configuration!");
diff --git a/src/Ryujinx.Tests/HLE/MidiInputConfigTests.cs b/src/Ryujinx.Tests/HLE/MidiInputConfigTests.cs
new file mode 100644
index 000000000..27391e818
--- /dev/null
+++ b/src/Ryujinx.Tests/HLE/MidiInputConfigTests.cs
@@ -0,0 +1,112 @@
+using NUnit.Framework;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Configuration.Hid.Midi;
+using Ryujinx.Common.Utilities;
+using Ryujinx.Input;
+using Ryujinx.Input.HLE;
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Tests.HLE
+{
+ public class MidiInputConfigTests
+ {
+ private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+ [Test]
+ public void MidiInputConfigRoundTripsThroughInputConfigConverter()
+ {
+ StandardMidiInputConfig config = new()
+ {
+ Version = InputConfig.CurrentVersion,
+ Backend = InputBackendType.Midi,
+ Id = "0",
+ Name = "Test MIDI Device",
+ ControllerType = ControllerType.ProController,
+ PlayerIndex = PlayerIndex.Player1,
+ LeftJoycon = new LeftJoyconCommonConfig
+ {
+ DpadUp = new MidiBinding { Kind = MidiBindingKind.Note, Number = 60, Channel = 1, Threshold = 10 },
+ DpadDown = new MidiBinding { Kind = MidiBindingKind.ControlChange, Number = 1, Channel = 0, Threshold = 64 },
+ },
+ LeftJoyconStick = new JoyconConfigKeyboardStick
+ {
+ StickUp = new MidiBinding { Kind = MidiBindingKind.Note, Number = 61, Channel = 0, Threshold = 1 },
+ StickButton = new MidiBinding { Kind = MidiBindingKind.ControlChange, Number = 64, Channel = 2, Threshold = 127 },
+ },
+ RightJoycon = new RightJoyconCommonConfig
+ {
+ ButtonA = new MidiBinding { Kind = MidiBindingKind.Note, Number = 72, Channel = 3, Threshold = 20 },
+ },
+ RightJoyconStick = new JoyconConfigKeyboardStick
+ {
+ StickLeft = new MidiBinding { Kind = MidiBindingKind.ControlChange, Number = 10, Channel = 0, Threshold = 90 },
+ },
+ };
+
+ string json = JsonHelper.Serialize(config, SerializerContext.InputConfig);
+ InputConfig roundTripped = JsonHelper.Deserialize(json, SerializerContext.InputConfig);
+
+ Assert.That(roundTripped, Is.TypeOf());
+
+ StandardMidiInputConfig midiConfig = (StandardMidiInputConfig)roundTripped;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(midiConfig.Backend, Is.EqualTo(InputBackendType.Midi));
+ Assert.That(midiConfig.LeftJoycon.DpadUp, Is.EqualTo(config.LeftJoycon.DpadUp));
+ Assert.That(midiConfig.LeftJoycon.DpadDown, Is.EqualTo(config.LeftJoycon.DpadDown));
+ Assert.That(midiConfig.LeftJoyconStick.StickUp, Is.EqualTo(config.LeftJoyconStick.StickUp));
+ Assert.That(midiConfig.LeftJoyconStick.StickButton, Is.EqualTo(config.LeftJoyconStick.StickButton));
+ Assert.That(midiConfig.RightJoycon.ButtonA, Is.EqualTo(config.RightJoycon.ButtonA));
+ Assert.That(midiConfig.RightJoyconStick.StickLeft, Is.EqualTo(config.RightJoyconStick.StickLeft));
+ });
+ }
+
+ [Test]
+ public void InputManagerReturnsMidiDriverForMidiBackend()
+ {
+ using StubGamepadDriver keyboard = new("Keyboard");
+ using StubGamepadDriver gamepad = new("Gamepad");
+ using StubGamepadDriver midi = new("MIDI");
+ using InputManager inputManager = new(keyboard, gamepad, midi);
+
+ Assert.That(inputManager.GetDriver(InputBackendType.WindowKeyboard), Is.SameAs(keyboard));
+ Assert.That(inputManager.GetDriver(InputBackendType.GamepadSDL3), Is.SameAs(gamepad));
+ Assert.That(inputManager.GetDriver(InputBackendType.Midi), Is.SameAs(midi));
+ }
+
+ private sealed class StubGamepadDriver(string driverName) : IGamepadDriver
+ {
+ public string DriverName { get; } = driverName;
+ public ReadOnlySpan GamepadsIds => [];
+
+ public event Action OnGamepadConnected
+ {
+ add { }
+ remove { }
+ }
+
+ public event Action OnGamepadDisconnected
+ {
+ add { }
+ remove { }
+ }
+
+ public IGamepad GetGamepad(string id)
+ {
+ return null;
+ }
+
+ public IEnumerable GetGamepads()
+ {
+ yield break;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Tests/Ryujinx.Tests.csproj b/src/Ryujinx.Tests/Ryujinx.Tests.csproj
index 2ac6410ef..e23942457 100644
--- a/src/Ryujinx.Tests/Ryujinx.Tests.csproj
+++ b/src/Ryujinx.Tests/Ryujinx.Tests.csproj
@@ -27,6 +27,7 @@
+
diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs
index af61b7b63..02d0052ab 100644
--- a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs
+++ b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs
@@ -9,6 +9,7 @@ using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Configuration.Hid.Midi;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.Cpu;
@@ -74,18 +75,24 @@ namespace Ryujinx.Headless
IGamepad gamepad = _inputManager.KeyboardDriver.GetGamepad(inputId);
- bool isKeyboard = true;
+ InputBackendType inputBackend = InputBackendType.WindowKeyboard;
if (gamepad == null)
{
gamepad = _inputManager.GamepadDriver.GetGamepad(inputId);
- isKeyboard = false;
+ inputBackend = InputBackendType.GamepadSDL3;
if (gamepad == null)
{
- Logger.Error?.Print(LogClass.Application, $"{index} gamepad not found (\"{inputId}\")");
+ gamepad = _inputManager.MidiDriver?.GetGamepad(inputId);
+ inputBackend = InputBackendType.Midi;
- return null;
+ if (gamepad == null)
+ {
+ Logger.Error?.Print(LogClass.Application, $"{index} gamepad not found (\"{inputId}\")");
+
+ return null;
+ }
}
}
@@ -97,7 +104,7 @@ namespace Ryujinx.Headless
if (inputProfileName == null || inputProfileName.Equals("default"))
{
- if (isKeyboard)
+ if (inputBackend == InputBackendType.WindowKeyboard)
{
config = new StandardKeyboardInputConfig
{
@@ -150,6 +157,58 @@ namespace Ryujinx.Headless
},
};
}
+ else if (inputBackend == InputBackendType.Midi)
+ {
+ static MidiBinding UnboundBinding() => new() { Kind = MidiBindingKind.Unbound, Threshold = 1 };
+
+ config = new StandardMidiInputConfig
+ {
+ Version = InputConfig.CurrentVersion,
+ Backend = InputBackendType.Midi,
+ Id = null,
+ ControllerType = ControllerType.JoyconPair,
+ LeftJoycon = new LeftJoyconCommonConfig
+ {
+ DpadUp = UnboundBinding(),
+ DpadDown = UnboundBinding(),
+ DpadLeft = UnboundBinding(),
+ DpadRight = UnboundBinding(),
+ ButtonMinus = UnboundBinding(),
+ ButtonL = UnboundBinding(),
+ ButtonZl = UnboundBinding(),
+ ButtonSl = UnboundBinding(),
+ ButtonSr = UnboundBinding(),
+ },
+ LeftJoyconStick = new JoyconConfigKeyboardStick
+ {
+ StickUp = UnboundBinding(),
+ StickDown = UnboundBinding(),
+ StickLeft = UnboundBinding(),
+ StickRight = UnboundBinding(),
+ StickButton = UnboundBinding(),
+ },
+ RightJoycon = new RightJoyconCommonConfig
+ {
+ ButtonA = UnboundBinding(),
+ ButtonB = UnboundBinding(),
+ ButtonX = UnboundBinding(),
+ ButtonY = UnboundBinding(),
+ ButtonPlus = UnboundBinding(),
+ ButtonR = UnboundBinding(),
+ ButtonZr = UnboundBinding(),
+ ButtonSl = UnboundBinding(),
+ ButtonSr = UnboundBinding(),
+ },
+ RightJoyconStick = new JoyconConfigKeyboardStick
+ {
+ StickUp = UnboundBinding(),
+ StickDown = UnboundBinding(),
+ StickLeft = UnboundBinding(),
+ StickRight = UnboundBinding(),
+ StickButton = UnboundBinding(),
+ },
+ };
+ }
else
{
bool isNintendoStyle = gamepadName.Contains("Nintendo");
@@ -229,10 +288,14 @@ namespace Ryujinx.Headless
{
string profileBasePath;
- if (isKeyboard)
+ if (inputBackend == InputBackendType.WindowKeyboard)
{
profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "keyboard");
}
+ else if (inputBackend == InputBackendType.Midi)
+ {
+ profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "midi");
+ }
else
{
profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "controller");
@@ -262,7 +325,12 @@ namespace Ryujinx.Headless
config.Id = inputId;
config.PlayerIndex = index;
- string inputTypeName = isKeyboard ? "Keyboard" : "Gamepad";
+ string inputTypeName = inputBackend switch
+ {
+ InputBackendType.WindowKeyboard => "Keyboard",
+ InputBackendType.Midi => "MIDI",
+ _ => "Gamepad",
+ };
Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} configured with {inputTypeName} \"{config.Id}\"");
diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.cs b/src/Ryujinx/Headless/HeadlessRyujinx.cs
index bba505dbb..f521163a9 100644
--- a/src/Ryujinx/Headless/HeadlessRyujinx.cs
+++ b/src/Ryujinx/Headless/HeadlessRyujinx.cs
@@ -22,6 +22,7 @@ using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
+using Ryujinx.Input.Midi;
using Ryujinx.Input.SDL3;
using Ryujinx.SDL3.Common;
using System;
@@ -181,7 +182,7 @@ namespace Ryujinx.Headless
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
_userChannelPersistence = new UserChannelPersistence();
- _inputManager = new InputManager(new SDL3KeyboardDriver(), new SDL3GamepadDriver());
+ _inputManager = new InputManager(new SDL3KeyboardDriver(), new SDL3GamepadDriver(), new MidiGamepadDriver());
GraphicsConfig.EnableShaderCache = !option.DisableShaderCache;
@@ -216,6 +217,18 @@ namespace Ryujinx.Headless
gamepad.Dispose();
}
+ if (_inputManager.MidiDriver != null)
+ {
+ foreach (string id in _inputManager.MidiDriver.GamepadsIds)
+ {
+ IGamepad gamepad = _inputManager.MidiDriver.GetGamepad(id);
+
+ Logger.Info?.Print(LogClass.Application, $"- {id} (\"{gamepad.Name}\")");
+
+ gamepad.Dispose();
+ }
+ }
+
return;
}
diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj
index 489b2c313..b2d795e92 100644
--- a/src/Ryujinx/Ryujinx.csproj
+++ b/src/Ryujinx/Ryujinx.csproj
@@ -76,6 +76,7 @@
+
diff --git a/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs b/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs
index d153adc74..cd1fbad60 100644
--- a/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs
+++ b/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs
@@ -2,6 +2,7 @@ using Avalonia.Data.Converters;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Configuration.Hid.Midi;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -173,6 +174,15 @@ namespace Ryujinx.Ava.UI.Helpers
keyString = stickInputId.ToString();
}
+ break;
+ case MidiBinding midiBinding:
+ keyString = midiBinding.Kind switch
+ {
+ MidiBindingKind.Unbound => LocaleManager.Instance[LocaleKeys.KeyUnbound],
+ MidiBindingKind.Note => $"Note {midiBinding.Number} Ch {(midiBinding.Channel == 0 ? "Any" : midiBinding.Channel)} T{midiBinding.Threshold}",
+ MidiBindingKind.ControlChange => $"CC {midiBinding.Number} Ch {(midiBinding.Channel == 0 ? "Any" : midiBinding.Channel)} T{midiBinding.Threshold}",
+ _ => midiBinding.ToString(),
+ };
break;
}
diff --git a/src/Ryujinx/UI/Models/DeviceType.cs b/src/Ryujinx/UI/Models/DeviceType.cs
index bb4fc3b30..d999a1794 100644
--- a/src/Ryujinx/UI/Models/DeviceType.cs
+++ b/src/Ryujinx/UI/Models/DeviceType.cs
@@ -4,6 +4,7 @@ namespace Ryujinx.Ava.UI.Models
{
None,
Keyboard,
+ Midi,
Controller,
}
}
diff --git a/src/Ryujinx/UI/Models/Input/MidiInputConfig.cs b/src/Ryujinx/UI/Models/Input/MidiInputConfig.cs
new file mode 100644
index 000000000..ea69131ba
--- /dev/null
+++ b/src/Ryujinx/UI/Models/Input/MidiInputConfig.cs
@@ -0,0 +1,202 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Midi;
+
+namespace Ryujinx.Ava.UI.Models.Input
+{
+ public partial class MidiInputConfig : BaseModel
+ {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public ControllerType ControllerType { get; set; }
+ public PlayerIndex PlayerIndex { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding LeftStickUp { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding LeftStickDown { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding LeftStickLeft { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding LeftStickRight { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding LeftStickButton { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding RightStickUp { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding RightStickDown { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding RightStickLeft { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding RightStickRight { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding RightStickButton { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding DpadUp { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding DpadDown { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding DpadLeft { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding DpadRight { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding ButtonMinus { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding ButtonPlus { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding ButtonA { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding ButtonB { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding ButtonX { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding ButtonY { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding ButtonL { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding ButtonR { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding ButtonZl { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding ButtonZr { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding LeftButtonSl { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding LeftButtonSr { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding RightButtonSl { get; set; }
+
+ [ObservableProperty]
+ public partial MidiBinding RightButtonSr { get; set; }
+
+ public MidiInputConfig(InputConfig config)
+ {
+ if (config == null)
+ {
+ return;
+ }
+
+ Id = config.Id;
+ Name = config.Name;
+ ControllerType = config.ControllerType;
+ PlayerIndex = config.PlayerIndex;
+
+ if (config is not StandardMidiInputConfig midiConfig)
+ {
+ return;
+ }
+
+ LeftStickUp = midiConfig.LeftJoyconStick.StickUp;
+ LeftStickDown = midiConfig.LeftJoyconStick.StickDown;
+ LeftStickLeft = midiConfig.LeftJoyconStick.StickLeft;
+ LeftStickRight = midiConfig.LeftJoyconStick.StickRight;
+ LeftStickButton = midiConfig.LeftJoyconStick.StickButton;
+
+ RightStickUp = midiConfig.RightJoyconStick.StickUp;
+ RightStickDown = midiConfig.RightJoyconStick.StickDown;
+ RightStickLeft = midiConfig.RightJoyconStick.StickLeft;
+ RightStickRight = midiConfig.RightJoyconStick.StickRight;
+ RightStickButton = midiConfig.RightJoyconStick.StickButton;
+
+ DpadUp = midiConfig.LeftJoycon.DpadUp;
+ DpadDown = midiConfig.LeftJoycon.DpadDown;
+ DpadLeft = midiConfig.LeftJoycon.DpadLeft;
+ DpadRight = midiConfig.LeftJoycon.DpadRight;
+ ButtonL = midiConfig.LeftJoycon.ButtonL;
+ ButtonMinus = midiConfig.LeftJoycon.ButtonMinus;
+ LeftButtonSl = midiConfig.LeftJoycon.ButtonSl;
+ LeftButtonSr = midiConfig.LeftJoycon.ButtonSr;
+ ButtonZl = midiConfig.LeftJoycon.ButtonZl;
+
+ ButtonA = midiConfig.RightJoycon.ButtonA;
+ ButtonB = midiConfig.RightJoycon.ButtonB;
+ ButtonX = midiConfig.RightJoycon.ButtonX;
+ ButtonY = midiConfig.RightJoycon.ButtonY;
+ ButtonR = midiConfig.RightJoycon.ButtonR;
+ ButtonPlus = midiConfig.RightJoycon.ButtonPlus;
+ RightButtonSl = midiConfig.RightJoycon.ButtonSl;
+ RightButtonSr = midiConfig.RightJoycon.ButtonSr;
+ ButtonZr = midiConfig.RightJoycon.ButtonZr;
+ }
+
+ public InputConfig GetConfig()
+ {
+ return new StandardMidiInputConfig
+ {
+ Id = Id,
+ Name = Name,
+ Backend = InputBackendType.Midi,
+ PlayerIndex = PlayerIndex,
+ ControllerType = ControllerType,
+ LeftJoycon = new LeftJoyconCommonConfig
+ {
+ DpadUp = DpadUp,
+ DpadDown = DpadDown,
+ DpadLeft = DpadLeft,
+ DpadRight = DpadRight,
+ ButtonL = ButtonL,
+ ButtonMinus = ButtonMinus,
+ ButtonZl = ButtonZl,
+ ButtonSl = LeftButtonSl,
+ ButtonSr = LeftButtonSr,
+ },
+ RightJoycon = new RightJoyconCommonConfig
+ {
+ ButtonA = ButtonA,
+ ButtonB = ButtonB,
+ ButtonX = ButtonX,
+ ButtonY = ButtonY,
+ ButtonPlus = ButtonPlus,
+ ButtonSl = RightButtonSl,
+ ButtonSr = RightButtonSr,
+ ButtonR = ButtonR,
+ ButtonZr = ButtonZr,
+ },
+ LeftJoyconStick = new Ryujinx.Common.Configuration.Hid.Keyboard.JoyconConfigKeyboardStick
+ {
+ StickUp = LeftStickUp,
+ StickDown = LeftStickDown,
+ StickRight = LeftStickRight,
+ StickLeft = LeftStickLeft,
+ StickButton = LeftStickButton,
+ },
+ RightJoyconStick = new Ryujinx.Common.Configuration.Hid.Keyboard.JoyconConfigKeyboardStick
+ {
+ StickUp = RightStickUp,
+ StickDown = RightStickDown,
+ StickLeft = RightStickLeft,
+ StickRight = RightStickRight,
+ StickButton = RightStickButton,
+ },
+ Version = InputConfig.CurrentVersion,
+ };
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/Models/Input/StickVisualizer.cs b/src/Ryujinx/UI/Models/Input/StickVisualizer.cs
index f88f4ea72..16991d699 100644
--- a/src/Ryujinx/UI/Models/Input/StickVisualizer.cs
+++ b/src/Ryujinx/UI/Models/Input/StickVisualizer.cs
@@ -61,6 +61,18 @@ namespace Ryujinx.Ava.UI.Models.Input
}
}
+ private MidiInputConfig _midiConfig;
+ public MidiInputConfig MidiConfig
+ {
+ get => _midiConfig;
+ set
+ {
+ _midiConfig = value;
+
+ OnPropertyChanged();
+ }
+ }
+
private (float, float) _uiStickLeft;
public (float, float) UiStickLeft
{
@@ -131,6 +143,13 @@ namespace Ryujinx.Ava.UI.Models.Input
return;
}
+ else if (config is MidiInputViewModel midiConfig)
+ {
+ MidiConfig = midiConfig.Config;
+ Type = DeviceType.Midi;
+
+ return;
+ }
Type = DeviceType.None;
}
@@ -209,6 +228,17 @@ namespace Ryujinx.Ava.UI.Models.Input
rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick);
}
+ break;
+ case DeviceType.Midi:
+ IGamepad midi = Parent.SelectedGamepad;
+
+ if (midi != null)
+ {
+ GamepadStateSnapshot snapshot = midi.GetMappedStateSnapshot();
+ leftBuffer = snapshot.GetStick(StickInputId.Left);
+ rightBuffer = snapshot.GetStick(StickInputId.Right);
+ }
+
break;
case DeviceType.None:
@@ -252,6 +282,7 @@ namespace Ryujinx.Ava.UI.Models.Input
}
KeyboardConfig = null;
+ MidiConfig = null;
GamepadConfig = null;
Parent = null;
diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
index e5f085e0f..ff935f791 100644
--- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
@@ -16,9 +16,11 @@ using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Configuration.Hid.Midi;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.Input;
+using Ryujinx.Input.Midi;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -40,6 +42,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private const string JoyConLeftResource = "Ryujinx/Assets/Icons/Controller_JoyConLeft.svg";
private const string JoyConRightResource = "Ryujinx/Assets/Icons/Controller_JoyConRight.svg";
private const string KeyboardString = "keyboard";
+ private const string MidiString = "midi";
private const string ControllerString = "controller";
private readonly MainWindow _mainWindow;
@@ -95,13 +98,15 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
// XAML Flags
public bool ShowSettings => _device > 0;
- public bool IsController => _device > 1;
- public bool IsKeyboard => !IsController;
+ public DeviceType CurrentDeviceType => _device >= 0 && _device < Devices.Count ? Devices[_device].Type : DeviceType.None;
+ public bool IsController => CurrentDeviceType == DeviceType.Controller;
+ public bool IsKeyboard => CurrentDeviceType == DeviceType.Keyboard;
+ public bool IsMidi => CurrentDeviceType == DeviceType.Midi;
public bool IsRight { get; set; }
public bool IsLeft { get; set; }
public string RevertDeviceId { get; set; }
- public bool HasLed => (SelectedGamepad.Features & GamepadFeaturesFlag.Led) != 0;
- public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense");
+ public bool HasLed => SelectedGamepad != null && (SelectedGamepad.Features & GamepadFeaturesFlag.Led) != 0;
+ public bool CanClearLed => SelectedGamepad?.Name.ContainsIgnoreCase("DualSense") == true;
public event Action NotifyChangesEvent;
@@ -349,6 +354,11 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig), VisualStick);
}
+ if (Config is StandardMidiInputConfig midiInputConfig)
+ {
+ ConfigViewModel = new MidiInputViewModel(this, new MidiInputConfig(midiInputConfig), VisualStick);
+ }
+
if (Config is StandardControllerInputConfig controllerInputConfig)
{
ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig), VisualStick);
@@ -407,6 +417,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
type = DeviceType.Keyboard;
}
+ else if (Config is StandardMidiInputConfig)
+ {
+ type = DeviceType.Midi;
+ }
if (Config is StandardControllerInputConfig)
{
@@ -452,6 +466,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
SelectedGamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
}
}
+ else if (type == DeviceType.Midi)
+ {
+ SelectedGamepad = _mainWindow.InputManager.MidiDriver?.GetGamepad(id);
+ }
else
{
SelectedGamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
@@ -609,6 +627,19 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
}
+ if (_mainWindow.InputManager.MidiDriver is MidiGamepadDriver midiDriver)
+ {
+ foreach (string id in midiDriver.GamepadsIds)
+ {
+ string name = midiDriver.GetDeviceName(id);
+
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ Devices.Add((DeviceType.Midi, id, GetShortGamepadName(name)));
+ }
+ }
+ }
+
DeviceList.AddRange(Devices.Select(x => x.Name));
Device = Math.Min(Device, DeviceList.Count);
}
@@ -623,6 +654,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
path = Path.Combine(path, KeyboardString);
}
+ else if (type == DeviceType.Midi)
+ {
+ path = Path.Combine(path, MidiString);
+ }
else if (type == DeviceType.Controller)
{
path = Path.Combine(path, ControllerString);
@@ -720,6 +755,62 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
},
};
}
+ else if (activeDevice.Type == DeviceType.Midi)
+ {
+ string id = activeDevice.Id;
+ string name = activeDevice.Name;
+
+ static MidiBinding UnboundBinding() => new() { Kind = MidiBindingKind.Unbound, Threshold = 1 };
+
+ config = new StandardMidiInputConfig
+ {
+ Version = InputConfig.CurrentVersion,
+ Backend = InputBackendType.Midi,
+ Id = id,
+ Name = name,
+ ControllerType = ControllerType.ProController,
+ LeftJoycon = new LeftJoyconCommonConfig
+ {
+ DpadUp = UnboundBinding(),
+ DpadDown = UnboundBinding(),
+ DpadLeft = UnboundBinding(),
+ DpadRight = UnboundBinding(),
+ ButtonMinus = UnboundBinding(),
+ ButtonL = UnboundBinding(),
+ ButtonZl = UnboundBinding(),
+ ButtonSl = UnboundBinding(),
+ ButtonSr = UnboundBinding(),
+ },
+ LeftJoyconStick = new JoyconConfigKeyboardStick
+ {
+ StickUp = UnboundBinding(),
+ StickDown = UnboundBinding(),
+ StickLeft = UnboundBinding(),
+ StickRight = UnboundBinding(),
+ StickButton = UnboundBinding(),
+ },
+ RightJoycon = new RightJoyconCommonConfig
+ {
+ ButtonA = UnboundBinding(),
+ ButtonB = UnboundBinding(),
+ ButtonX = UnboundBinding(),
+ ButtonY = UnboundBinding(),
+ ButtonPlus = UnboundBinding(),
+ ButtonR = UnboundBinding(),
+ ButtonZr = UnboundBinding(),
+ ButtonSl = UnboundBinding(),
+ ButtonSr = UnboundBinding(),
+ },
+ RightJoyconStick = new JoyconConfigKeyboardStick
+ {
+ StickUp = UnboundBinding(),
+ StickDown = UnboundBinding(),
+ StickLeft = UnboundBinding(),
+ StickRight = UnboundBinding(),
+ StickButton = UnboundBinding(),
+ },
+ };
+ }
else if (activeDevice.Type == DeviceType.Controller)
{
bool isNintendoStyle = Devices.ToList().FirstOrDefault(x => x.Id == activeDevice.Id).Name.Contains("Nintendo");
@@ -905,6 +996,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
config = (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig();
}
+ else if (IsMidi)
+ {
+ config = (ConfigViewModel as MidiInputViewModel).Config.GetConfig();
+ }
else if (IsController)
{
config = (ConfigViewModel as ControllerInputViewModel).Config.GetConfig();
@@ -1008,15 +1103,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
KeyboardInputConfig inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config;
inputConfig.Id = device.Id;
}
+ else if (device.Type == DeviceType.Midi)
+ {
+ MidiInputConfig inputConfig = (ConfigViewModel as MidiInputViewModel).Config;
+ inputConfig.Id = device.Id;
+ }
else
{
GamepadInputConfig inputConfig = (ConfigViewModel as ControllerInputViewModel).Config;
inputConfig.Id = device.Id.Split(" ")[0];
}
- InputConfig config = !IsController
- ? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig()
- : (ConfigViewModel as ControllerInputViewModel).Config.GetConfig();
+ InputConfig config = GetCurrentConfigFromViewModel();
config.ControllerType = Controllers[_controller].Type;
config.PlayerIndex = _playerId;
config.Name = device.Name;
@@ -1055,6 +1153,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
OnPropertyChanged(nameof(IsController));
OnPropertyChanged(nameof(ShowSettings));
OnPropertyChanged(nameof(IsKeyboard));
+ OnPropertyChanged(nameof(IsMidi));
OnPropertyChanged(nameof(IsRight));
OnPropertyChanged(nameof(IsLeft));
NotifyChangesEvent?.Invoke();
@@ -1075,5 +1174,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
AvaloniaKeyboardDriver.Dispose();
}
+
+ private InputConfig GetCurrentConfigFromViewModel()
+ {
+ return ConfigViewModel switch
+ {
+ KeyboardInputViewModel keyboardViewModel => keyboardViewModel.Config.GetConfig(),
+ MidiInputViewModel midiViewModel => midiViewModel.Config.GetConfig(),
+ ControllerInputViewModel controllerViewModel => controllerViewModel.Config.GetConfig(),
+ _ => null,
+ };
+ }
}
}
diff --git a/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs
index 178e2c955..f00e33e93 100644
--- a/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs
@@ -6,6 +6,20 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
public partial class KeyboardInputViewModel : BaseModel
{
+ public bool ShowMidiCaptureOptions => false;
+
+ public bool CaptureAnyChannel
+ {
+ get;
+ set;
+ } = true;
+
+ public int CaptureThreshold
+ {
+ get;
+ set;
+ } = 1;
+
public KeyboardInputConfig Config
{
get;
diff --git a/src/Ryujinx/UI/ViewModels/Input/MidiInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/MidiInputViewModel.cs
new file mode 100644
index 000000000..2fc55922d
--- /dev/null
+++ b/src/Ryujinx/UI/ViewModels/Input/MidiInputViewModel.cs
@@ -0,0 +1,77 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Ryujinx.Ava.UI.Models.Input;
+
+namespace Ryujinx.Ava.UI.ViewModels.Input
+{
+ public partial class MidiInputViewModel : BaseModel
+ {
+ public bool ShowMidiCaptureOptions => true;
+
+ public MidiInputConfig Config
+ {
+ get;
+ set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public StickVisualizer Visualizer
+ {
+ get;
+ set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public bool IsLeft
+ {
+ get;
+ set
+ {
+ field = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasSides));
+ }
+ }
+
+ public bool IsRight
+ {
+ get;
+ set
+ {
+ field = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasSides));
+ }
+ }
+
+ public bool HasSides => IsLeft ^ IsRight;
+
+ [ObservableProperty]
+ public partial int CaptureThreshold { get; set; } = 1;
+
+ [ObservableProperty]
+ public partial bool CaptureAnyChannel { get; set; } = true;
+
+ public readonly InputViewModel ParentModel;
+
+ public MidiInputViewModel(InputViewModel model, MidiInputConfig config, StickVisualizer visualizer)
+ {
+ ParentModel = model;
+ Visualizer = visualizer;
+ model.NotifyChangesEvent += OnParentModelChanged;
+ OnParentModelChanged();
+ Config = config;
+ }
+
+ public void OnParentModelChanged()
+ {
+ IsLeft = ParentModel.IsLeft;
+ IsRight = ParentModel.IsRight;
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/Views/Input/InputView.axaml b/src/Ryujinx/UI/Views/Input/InputView.axaml
index c4f61e78e..ece178770 100644
--- a/src/Ryujinx/UI/Views/Input/InputView.axaml
+++ b/src/Ryujinx/UI/Views/Input/InputView.axaml
@@ -230,6 +230,9 @@
+
+
+
diff --git a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml
index 22fd369d9..ce9af6d79 100644
--- a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml
+++ b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml
@@ -2,6 +2,7 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input"
@@ -11,8 +12,7 @@
d:DesignHeight="800"
d:DesignWidth="800"
x:Class="Ryujinx.Ava.UI.Views.Input.KeyboardInputView"
- x:DataType="viewModels:KeyboardInputViewModel"
- x:CompileBindings="True"
+ x:CompileBindings="False"
mc:Ignorable="d"
Focusable="True">
@@ -29,6 +29,37 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Orientation="Vertical">
+
+
+
+
+
+
+
+
+
+
+ public partial class KeyboardInputView : RyujinxControl
{
- private ButtonKeyAssigner _currentAssigner;
+ private ButtonKeyAssigner _keyboardAssigner;
+ private ToggleButton _midiToggleButton;
+ private CancellationTokenSource _midiCancellationTokenSource;
+ private bool _shouldUnbindMidi;
public KeyboardInputView()
{
@@ -37,157 +48,148 @@ namespace Ryujinx.Ava.UI.Views.Input
{
base.OnPointerReleased(e);
- if (_currentAssigner is { ToggledButton.IsPointerOver: false })
+ ToggleButton currentButton = GetCurrentToggleButton();
+
+ if (currentButton is { IsPointerOver: false })
{
- _currentAssigner.Cancel();
+ CancelCurrentAssignment();
}
}
- private void Button_IsCheckedChanged(object sender, RoutedEventArgs e)
+ private async void Button_IsCheckedChanged(object sender, RoutedEventArgs e)
{
if (sender is not ToggleButton button)
- return;
-
- if (button.IsChecked is true)
{
- if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
+ return;
+ }
+
+ if (button.IsChecked is not true)
+ {
+ if (GetCurrentToggleButton() == button)
{
- return;
+ CancelCurrentAssignment();
}
- if (_currentAssigner == null)
- {
- _currentAssigner = new ButtonKeyAssigner(button);
+ return;
+ }
- Focus(NavigationMethod.Pointer);
+ if (GetCurrentToggleButton() == button)
+ {
+ return;
+ }
- PointerPressed += MouseClick;
+ if (GetCurrentToggleButton() != null)
+ {
+ CancelCurrentAssignment();
+ button.IsChecked = false;
+ return;
+ }
- IKeyboard keyboard =
- (IKeyboard)ViewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
- IButtonAssigner assigner =
- new KeyboardKeyAssigner((IKeyboard)ViewModel.ParentModel.SelectedGamepad);
+ Focus(NavigationMethod.Pointer);
+ PointerPressed += MouseClick;
- _currentAssigner.ButtonAssigned += (_, be) =>
- {
- if (be.ButtonValue.HasValue)
- {
- Button buttonValue = be.ButtonValue.Value;
- ViewModel.ParentModel.IsModified = true;
-
- switch (button.Name)
- {
- case "ButtonZl":
- ViewModel.Config.ButtonZl = buttonValue.AsHidType();
- break;
- case "ButtonL":
- ViewModel.Config.ButtonL = buttonValue.AsHidType();
- break;
- case "ButtonMinus":
- ViewModel.Config.ButtonMinus = buttonValue.AsHidType();
- break;
- case "LeftStickButton":
- ViewModel.Config.LeftStickButton = buttonValue.AsHidType();
- break;
- case "LeftStickUp":
- ViewModel.Config.LeftStickUp = buttonValue.AsHidType();
- break;
- case "LeftStickDown":
- ViewModel.Config.LeftStickDown = buttonValue.AsHidType();
- break;
- case "LeftStickRight":
- ViewModel.Config.LeftStickRight = buttonValue.AsHidType();
- break;
- case "LeftStickLeft":
- ViewModel.Config.LeftStickLeft = buttonValue.AsHidType();
- break;
- case "DpadUp":
- ViewModel.Config.DpadUp = buttonValue.AsHidType();
- break;
- case "DpadDown":
- ViewModel.Config.DpadDown = buttonValue.AsHidType();
- break;
- case "DpadLeft":
- ViewModel.Config.DpadLeft = buttonValue.AsHidType();
- break;
- case "DpadRight":
- ViewModel.Config.DpadRight = buttonValue.AsHidType();
- break;
- case "LeftButtonSr":
- ViewModel.Config.LeftButtonSr = buttonValue.AsHidType();
- break;
- case "LeftButtonSl":
- ViewModel.Config.LeftButtonSl = buttonValue.AsHidType();
- break;
- case "RightButtonSr":
- ViewModel.Config.RightButtonSr = buttonValue.AsHidType();
- break;
- case "RightButtonSl":
- ViewModel.Config.RightButtonSl = buttonValue.AsHidType();
- break;
- case "ButtonZr":
- ViewModel.Config.ButtonZr = buttonValue.AsHidType();
- break;
- case "ButtonR":
- ViewModel.Config.ButtonR = buttonValue.AsHidType();
- break;
- case "ButtonPlus":
- ViewModel.Config.ButtonPlus = buttonValue.AsHidType();
- break;
- case "ButtonA":
- ViewModel.Config.ButtonA = buttonValue.AsHidType();
- break;
- case "ButtonB":
- ViewModel.Config.ButtonB = buttonValue.AsHidType();
- break;
- case "ButtonX":
- ViewModel.Config.ButtonX = buttonValue.AsHidType();
- break;
- case "ButtonY":
- ViewModel.Config.ButtonY = buttonValue.AsHidType();
- break;
- case "RightStickButton":
- ViewModel.Config.RightStickButton = buttonValue.AsHidType();
- break;
- case "RightStickUp":
- ViewModel.Config.RightStickUp = buttonValue.AsHidType();
- break;
- case "RightStickDown":
- ViewModel.Config.RightStickDown = buttonValue.AsHidType();
- break;
- case "RightStickRight":
- ViewModel.Config.RightStickRight = buttonValue.AsHidType();
- break;
- case "RightStickLeft":
- ViewModel.Config.RightStickLeft = buttonValue.AsHidType();
- break;
- }
- }
- };
-
- _currentAssigner.GetInputAndAssign(assigner, keyboard);
- }
- else
- {
- if (_currentAssigner != null)
- {
- _currentAssigner.Cancel();
- _currentAssigner = null;
- button.IsChecked = false;
- }
- }
+ if (DataContext is MidiInputViewModel midiViewModel)
+ {
+ await StartMidiAssignmentAsync(button, midiViewModel);
}
else
{
- _currentAssigner?.Cancel();
- _currentAssigner = null;
+ StartKeyboardAssignment(button);
}
}
+ private void StartKeyboardAssignment(ToggleButton button)
+ {
+ _keyboardAssigner = new ButtonKeyAssigner(button);
+
+ IKeyboard keyboard = (IKeyboard)GetCancelKeyboard();
+ IButtonAssigner assigner = new KeyboardKeyAssigner((IKeyboard)GetSelectedGamepad());
+
+ _keyboardAssigner.ButtonAssigned += (_, be) =>
+ {
+ _keyboardAssigner = null;
+
+ if (be.ButtonValue.HasValue)
+ {
+ Button buttonValue = be.ButtonValue.Value;
+ AssignKeyboardBinding(button.Name, buttonValue.AsHidType());
+ }
+ };
+
+ _keyboardAssigner.GetInputAndAssign(assigner, keyboard);
+ }
+
+ private async Task StartMidiAssignmentAsync(ToggleButton button, MidiInputViewModel viewModel)
+ {
+ if (GetSelectedGamepad() is not IMidiGamepad midiGamepad)
+ {
+ button.IsChecked = false;
+ PointerPressed -= MouseClick;
+ return;
+ }
+
+ _midiToggleButton = button;
+ _shouldUnbindMidi = false;
+ _midiCancellationTokenSource = new CancellationTokenSource();
+
+ MidiBindingAssigner assigner = new(midiGamepad);
+ IKeyboard keyboard = (IKeyboard)GetCancelKeyboard();
+
+ assigner.Initialize();
+
+ MidiBinding? binding = null;
+
+ try
+ {
+ binding = await Task.Run(async () =>
+ {
+ while (!_midiCancellationTokenSource.IsCancellationRequested)
+ {
+ await Task.Delay(10, _midiCancellationTokenSource.Token);
+
+ assigner.ReadInput();
+
+ if (assigner.IsAnyButtonPressed() || assigner.ShouldCancel() || (keyboard != null && keyboard.IsPressed(Ryujinx.Input.Key.Escape)))
+ {
+ break;
+ }
+ }
+
+ if (_shouldUnbindMidi)
+ {
+ return CreateUnboundMidiBinding();
+ }
+
+ return assigner.GetPressedBinding(viewModel.CaptureAnyChannel, (byte)viewModel.CaptureThreshold);
+ }, _midiCancellationTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ PointerPressed -= MouseClick;
+
+ if (_midiToggleButton != null)
+ {
+ _midiToggleButton.IsChecked = false;
+ }
+
+ _midiToggleButton = null;
+ _midiCancellationTokenSource?.Dispose();
+ _midiCancellationTokenSource = null;
+
+ if (binding.HasValue)
+ {
+ AssignMidiBinding(button.Name, binding.Value);
+ }
+ });
+ }
+
private void MouseClick(object sender, PointerPressedEventArgs e)
{
bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;
-
bool shouldRemoveBinding = e.GetCurrentPoint(this).Properties.IsRightButtonPressed;
if (shouldRemoveBinding)
@@ -195,61 +197,177 @@ namespace Ryujinx.Ava.UI.Views.Input
DeleteBind();
}
- _currentAssigner?.Cancel(shouldUnbind);
-
+ CancelCurrentAssignment(shouldUnbind);
PointerPressed -= MouseClick;
}
private void DeleteBind()
{
+ ToggleButton button = GetCurrentToggleButton();
- if (_currentAssigner != null)
+ if (button == null)
{
- Dictionary buttonActions = new()
- {
- { "ButtonZl", () => ViewModel.Config.ButtonZl = Key.Unbound },
- { "ButtonL", () => ViewModel.Config.ButtonL = Key.Unbound },
- { "ButtonMinus", () => ViewModel.Config.ButtonMinus = Key.Unbound },
- { "LeftStickButton", () => ViewModel.Config.LeftStickButton = Key.Unbound },
- { "LeftStickUp", () => ViewModel.Config.LeftStickUp = Key.Unbound },
- { "LeftStickDown", () => ViewModel.Config.LeftStickDown = Key.Unbound },
- { "LeftStickRight", () => ViewModel.Config.LeftStickRight = Key.Unbound },
- { "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = Key.Unbound },
- { "DpadUp", () => ViewModel.Config.DpadUp = Key.Unbound },
- { "DpadDown", () => ViewModel.Config.DpadDown = Key.Unbound },
- { "DpadLeft", () => ViewModel.Config.DpadLeft = Key.Unbound },
- { "DpadRight", () => ViewModel.Config.DpadRight = Key.Unbound },
- { "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = Key.Unbound },
- { "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = Key.Unbound },
- { "RightButtonSr", () => ViewModel.Config.RightButtonSr = Key.Unbound },
- { "RightButtonSl", () => ViewModel.Config.RightButtonSl = Key.Unbound },
- { "ButtonZr", () => ViewModel.Config.ButtonZr = Key.Unbound },
- { "ButtonR", () => ViewModel.Config.ButtonR = Key.Unbound },
- { "ButtonPlus", () => ViewModel.Config.ButtonPlus = Key.Unbound },
- { "ButtonA", () => ViewModel.Config.ButtonA = Key.Unbound },
- { "ButtonB", () => ViewModel.Config.ButtonB = Key.Unbound },
- { "ButtonX", () => ViewModel.Config.ButtonX = Key.Unbound },
- { "ButtonY", () => ViewModel.Config.ButtonY = Key.Unbound },
- { "RightStickButton", () => ViewModel.Config.RightStickButton = Key.Unbound },
- { "RightStickUp", () => ViewModel.Config.RightStickUp = Key.Unbound },
- { "RightStickDown", () => ViewModel.Config.RightStickDown = Key.Unbound },
- { "RightStickRight", () => ViewModel.Config.RightStickRight = Key.Unbound },
- { "RightStickLeft", () => ViewModel.Config.RightStickLeft = Key.Unbound }
- };
-
- if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
- {
- action();
- ViewModel.ParentModel.IsModified = true;
- }
+ return;
}
+
+ if (DataContext is MidiInputViewModel)
+ {
+ AssignMidiBinding(button.Name, CreateUnboundMidiBinding());
+ }
+ else
+ {
+ AssignKeyboardBinding(button.Name, Key.Unbound);
+ }
+ }
+
+ private void CancelCurrentAssignment(bool shouldUnbind = false)
+ {
+ if (_keyboardAssigner != null)
+ {
+ _keyboardAssigner.Cancel(shouldUnbind);
+ _keyboardAssigner = null;
+ }
+
+ if (_midiToggleButton != null)
+ {
+ _shouldUnbindMidi = shouldUnbind;
+ _midiCancellationTokenSource?.Cancel();
+ }
+ }
+
+ private ToggleButton GetCurrentToggleButton()
+ {
+ return _keyboardAssigner?.ToggledButton ?? _midiToggleButton;
+ }
+
+ private IGamepad GetSelectedGamepad()
+ {
+ return DataContext switch
+ {
+ KeyboardInputViewModel keyboardViewModel => keyboardViewModel.ParentModel.SelectedGamepad,
+ MidiInputViewModel midiViewModel => midiViewModel.ParentModel.SelectedGamepad,
+ _ => null,
+ };
+ }
+
+ private IGamepad GetCancelKeyboard()
+ {
+ return DataContext switch
+ {
+ KeyboardInputViewModel keyboardViewModel => keyboardViewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"),
+ MidiInputViewModel midiViewModel => midiViewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"),
+ _ => null,
+ };
+ }
+
+ private void AssignKeyboardBinding(string controlName, Key value)
+ {
+ if (DataContext is not KeyboardInputViewModel viewModel)
+ {
+ return;
+ }
+
+ viewModel.ParentModel.IsModified = true;
+
+ Dictionary buttonActions = new()
+ {
+ { "ButtonZl", () => viewModel.Config.ButtonZl = value },
+ { "ButtonL", () => viewModel.Config.ButtonL = value },
+ { "ButtonMinus", () => viewModel.Config.ButtonMinus = value },
+ { "LeftStickButton", () => viewModel.Config.LeftStickButton = value },
+ { "LeftStickUp", () => viewModel.Config.LeftStickUp = value },
+ { "LeftStickDown", () => viewModel.Config.LeftStickDown = value },
+ { "LeftStickRight", () => viewModel.Config.LeftStickRight = value },
+ { "LeftStickLeft", () => viewModel.Config.LeftStickLeft = value },
+ { "DpadUp", () => viewModel.Config.DpadUp = value },
+ { "DpadDown", () => viewModel.Config.DpadDown = value },
+ { "DpadLeft", () => viewModel.Config.DpadLeft = value },
+ { "DpadRight", () => viewModel.Config.DpadRight = value },
+ { "LeftButtonSr", () => viewModel.Config.LeftButtonSr = value },
+ { "LeftButtonSl", () => viewModel.Config.LeftButtonSl = value },
+ { "RightButtonSr", () => viewModel.Config.RightButtonSr = value },
+ { "RightButtonSl", () => viewModel.Config.RightButtonSl = value },
+ { "ButtonZr", () => viewModel.Config.ButtonZr = value },
+ { "ButtonR", () => viewModel.Config.ButtonR = value },
+ { "ButtonPlus", () => viewModel.Config.ButtonPlus = value },
+ { "ButtonA", () => viewModel.Config.ButtonA = value },
+ { "ButtonB", () => viewModel.Config.ButtonB = value },
+ { "ButtonX", () => viewModel.Config.ButtonX = value },
+ { "ButtonY", () => viewModel.Config.ButtonY = value },
+ { "RightStickButton", () => viewModel.Config.RightStickButton = value },
+ { "RightStickUp", () => viewModel.Config.RightStickUp = value },
+ { "RightStickDown", () => viewModel.Config.RightStickDown = value },
+ { "RightStickRight", () => viewModel.Config.RightStickRight = value },
+ { "RightStickLeft", () => viewModel.Config.RightStickLeft = value },
+ };
+
+ if (buttonActions.TryGetValue(controlName, out Action action))
+ {
+ action();
+ }
+ }
+
+ private void AssignMidiBinding(string controlName, MidiBinding value)
+ {
+ if (DataContext is not MidiInputViewModel viewModel)
+ {
+ return;
+ }
+
+ viewModel.ParentModel.IsModified = true;
+
+ Dictionary buttonActions = new()
+ {
+ { "ButtonZl", () => viewModel.Config.ButtonZl = value },
+ { "ButtonL", () => viewModel.Config.ButtonL = value },
+ { "ButtonMinus", () => viewModel.Config.ButtonMinus = value },
+ { "LeftStickButton", () => viewModel.Config.LeftStickButton = value },
+ { "LeftStickUp", () => viewModel.Config.LeftStickUp = value },
+ { "LeftStickDown", () => viewModel.Config.LeftStickDown = value },
+ { "LeftStickRight", () => viewModel.Config.LeftStickRight = value },
+ { "LeftStickLeft", () => viewModel.Config.LeftStickLeft = value },
+ { "DpadUp", () => viewModel.Config.DpadUp = value },
+ { "DpadDown", () => viewModel.Config.DpadDown = value },
+ { "DpadLeft", () => viewModel.Config.DpadLeft = value },
+ { "DpadRight", () => viewModel.Config.DpadRight = value },
+ { "LeftButtonSr", () => viewModel.Config.LeftButtonSr = value },
+ { "LeftButtonSl", () => viewModel.Config.LeftButtonSl = value },
+ { "RightButtonSr", () => viewModel.Config.RightButtonSr = value },
+ { "RightButtonSl", () => viewModel.Config.RightButtonSl = value },
+ { "ButtonZr", () => viewModel.Config.ButtonZr = value },
+ { "ButtonR", () => viewModel.Config.ButtonR = value },
+ { "ButtonPlus", () => viewModel.Config.ButtonPlus = value },
+ { "ButtonA", () => viewModel.Config.ButtonA = value },
+ { "ButtonB", () => viewModel.Config.ButtonB = value },
+ { "ButtonX", () => viewModel.Config.ButtonX = value },
+ { "ButtonY", () => viewModel.Config.ButtonY = value },
+ { "RightStickButton", () => viewModel.Config.RightStickButton = value },
+ { "RightStickUp", () => viewModel.Config.RightStickUp = value },
+ { "RightStickDown", () => viewModel.Config.RightStickDown = value },
+ { "RightStickRight", () => viewModel.Config.RightStickRight = value },
+ { "RightStickLeft", () => viewModel.Config.RightStickLeft = value },
+ };
+
+ if (buttonActions.TryGetValue(controlName, out Action action))
+ {
+ action();
+ }
+ }
+
+ private static MidiBinding CreateUnboundMidiBinding()
+ {
+ return new MidiBinding
+ {
+ Kind = MidiBindingKind.Unbound,
+ Threshold = 1,
+ };
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
- _currentAssigner?.Cancel();
- _currentAssigner = null;
+
+ CancelCurrentAssignment();
}
}
}
diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
index e7934f38a..268dbb97a 100644
--- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
+++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
@@ -29,6 +29,7 @@ using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input.HLE;
+using Ryujinx.Input.Midi;
using Ryujinx.Input.SDL3;
using System;
using System.Collections.Generic;
@@ -105,7 +106,7 @@ namespace Ryujinx.Ava.UI.Windows
if (Program.PreviewerDetached)
{
- InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL3GamepadDriver());
+ InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL3GamepadDriver(), new MidiGamepadDriver());
_ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it);
this.ScalingChanged += OnScalingChanged;