[GUI] Added Atrac9 audio decoder and improved GUI with audio preview and controller support (#64)

* [GUI] Added Atrac9 audio decoder and improved GUI with audio preview and controller support

* fix: package.lock.json for SharpEmu.CLI to match the other projects

* fix: packages.lock.json file to include new dependencies for GUI improvements

* rollForward: "disable"
This commit is contained in:
Berk
2026-07-11 19:14:08 +03:00
committed by GitHub
parent f43f7cde9c
commit 79a7437cd8
37 changed files with 4601 additions and 77 deletions
+25 -58
View File
@@ -5,21 +5,6 @@ using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Pad;
/// <summary>
/// Snapshot of the DualSense state, already translated to ORBIS pad
/// conventions (SCE_PAD_BUTTON bits; sticks 0..255 with 128 centered;
/// triggers 0..255).
/// </summary>
internal readonly record struct DualSenseState(
bool Connected,
uint Buttons,
byte LeftX,
byte LeftY,
byte RightX,
byte RightY,
byte L2,
byte R2);
/// <summary>
/// Reads a DualSense controller over raw HID on a background thread.
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
@@ -31,26 +16,8 @@ internal static class DualSenseReader
private const ushort DualSenseProductId = 0x0CE6;
private const ushort DualSenseEdgeProductId = 0x0DF2;
// SCE_PAD_BUTTON bit values.
private const uint ButtonL3 = 0x0002;
private const uint ButtonR3 = 0x0004;
private const uint ButtonOptions = 0x0008;
private const uint ButtonUp = 0x0010;
private const uint ButtonRight = 0x0020;
private const uint ButtonDown = 0x0040;
private const uint ButtonLeft = 0x0080;
private const uint ButtonL2 = 0x0100;
private const uint ButtonR2 = 0x0200;
private const uint ButtonL1 = 0x0400;
private const uint ButtonR1 = 0x0800;
private const uint ButtonTriangle = 0x1000;
private const uint ButtonCircle = 0x2000;
private const uint ButtonCross = 0x4000;
private const uint ButtonSquare = 0x8000;
private const uint ButtonTouchPad = 0x100000;
private static readonly object Gate = new();
private static DualSenseState _state;
private static PadState _state;
private static bool _started;
// Output (rumble/lightbar) state, all guarded by Gate.
@@ -92,7 +59,7 @@ internal static class DualSenseReader
}
}
internal static bool TryGetState(out DualSenseState state)
internal static bool TryGetState(out PadState state)
{
lock (Gate)
{
@@ -102,7 +69,7 @@ internal static class DualSenseReader
return state.Connected;
}
private static void SetState(in DualSenseState state)
private static void SetState(in PadState state)
{
lock (Gate)
{
@@ -399,7 +366,7 @@ internal static class DualSenseReader
return null;
}
private static bool TryParseReport(ReadOnlySpan<byte> report, out DualSenseState state)
private static bool TryParseReport(ReadOnlySpan<byte> report, out PadState state)
{
// USB: report id 0x01, payload starts at [1].
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
@@ -429,21 +396,21 @@ internal static class DualSenseReader
var buttons2 = report[offset + 9];
uint buttons = 0;
buttons |= (buttons0 & 0x10) != 0 ? ButtonSquare : 0;
buttons |= (buttons0 & 0x20) != 0 ? ButtonCross : 0;
buttons |= (buttons0 & 0x40) != 0 ? ButtonCircle : 0;
buttons |= (buttons0 & 0x80) != 0 ? ButtonTriangle : 0;
buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0;
buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0;
buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0;
buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0;
buttons |= HatToButtons(buttons0 & 0x0F);
buttons |= (buttons1 & 0x01) != 0 ? ButtonL1 : 0;
buttons |= (buttons1 & 0x02) != 0 ? ButtonR1 : 0;
buttons |= (buttons1 & 0x04) != 0 ? ButtonL2 : 0;
buttons |= (buttons1 & 0x08) != 0 ? ButtonR2 : 0;
buttons |= (buttons1 & 0x20) != 0 ? ButtonOptions : 0;
buttons |= (buttons1 & 0x40) != 0 ? ButtonL3 : 0;
buttons |= (buttons1 & 0x80) != 0 ? ButtonR3 : 0;
buttons |= (buttons2 & 0x02) != 0 ? ButtonTouchPad : 0;
buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0;
buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0;
buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0;
buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0;
buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0;
buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0;
buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0;
buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0;
state = new DualSenseState(
state = new PadState(
Connected: true,
Buttons: buttons,
LeftX: leftX,
@@ -457,14 +424,14 @@ internal static class DualSenseReader
private static uint HatToButtons(int hat) => hat switch
{
0 => ButtonUp,
1 => ButtonUp | ButtonRight,
2 => ButtonRight,
3 => ButtonRight | ButtonDown,
4 => ButtonDown,
5 => ButtonDown | ButtonLeft,
6 => ButtonLeft,
7 => ButtonLeft | ButtonUp,
0 => OrbisPadButton.Up,
1 => OrbisPadButton.Up | OrbisPadButton.Right,
2 => OrbisPadButton.Right,
3 => OrbisPadButton.Right | OrbisPadButton.Down,
4 => OrbisPadButton.Down,
5 => OrbisPadButton.Down | OrbisPadButton.Left,
6 => OrbisPadButton.Left,
7 => OrbisPadButton.Left | OrbisPadButton.Up,
_ => 0,
};
}
+17 -1
View File
@@ -31,6 +31,7 @@ public static class PadExports
{
_initialized = true;
DualSenseReader.EnsureStarted();
XInputReader.EnsureStarted();
return ctx.SetReturn(0);
}
@@ -61,9 +62,12 @@ public static class PadExports
}
DualSenseReader.EnsureStarted();
XInputReader.EnsureStarted();
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense will be used automatically when plugged in.");
: XInputReader.TryGetState(out _)
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
return ctx.SetReturn(PrimaryPadHandle);
}
@@ -201,6 +205,7 @@ public static class PadExports
}
DualSenseReader.SetRumble(parameter[0], parameter[1]);
XInputReader.SetRumble(parameter[0], parameter[1]);
return ctx.SetReturn(0);
}
@@ -277,6 +282,17 @@ public static class PadExports
r2 = Math.Max(r2, pad.R2);
}
if (XInputReader.TryGetState(out var xpad))
{
buttons |= xpad.Buttons;
leftX = MergeAxis(xpad.LeftX, leftX);
leftY = MergeAxis(xpad.LeftY, leftY);
rightX = MergeAxis(xpad.RightX, rightX);
rightY = MergeAxis(xpad.RightY, rightY);
l2 = Math.Max(l2, xpad.L2);
r2 = Math.Max(r2, xpad.R2);
}
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
data[0x04] = leftX;
data[0x05] = leftY;
+40
View File
@@ -0,0 +1,40 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Pad;
/// <summary>
/// Snapshot of a physical controller's state, already translated to ORBIS
/// pad conventions (SCE_PAD_BUTTON bits; sticks 0..255 with 128 centered
/// and Y growing downward; triggers 0..255).
/// </summary>
internal readonly record struct PadState(
bool Connected,
uint Buttons,
byte LeftX,
byte LeftY,
byte RightX,
byte RightY,
byte L2,
byte R2);
/// <summary>SCE_PAD_BUTTON bit values.</summary>
internal static class OrbisPadButton
{
internal const uint L3 = 0x0002;
internal const uint R3 = 0x0004;
internal const uint Options = 0x0008;
internal const uint Up = 0x0010;
internal const uint Right = 0x0020;
internal const uint Down = 0x0040;
internal const uint Left = 0x0080;
internal const uint L2 = 0x0100;
internal const uint R2 = 0x0200;
internal const uint L1 = 0x0400;
internal const uint R1 = 0x0800;
internal const uint Triangle = 0x1000;
internal const uint Circle = 0x2000;
internal const uint Cross = 0x4000;
internal const uint Square = 0x8000;
internal const uint TouchPad = 0x100000;
}
+246
View File
@@ -0,0 +1,246 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Pad;
/// <summary>
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
/// the Windows XInput API on a background thread, translated to the same
/// ORBIS pad conventions as <see cref="DualSenseReader"/>. Supports rumble
/// and hot-plug retry; the first connected slot (of four) is used.
/// </summary>
internal static class XInputReader
{
private const uint ErrorSuccess = 0;
private const int SlotCount = 4;
private const byte TriggerThreshold = 30; // XINPUT_GAMEPAD_TRIGGER_THRESHOLD
// XINPUT_GAMEPAD wButtons bit values.
private const ushort XinputDpadUp = 0x0001;
private const ushort XinputDpadDown = 0x0002;
private const ushort XinputDpadLeft = 0x0004;
private const ushort XinputDpadRight = 0x0008;
private const ushort XinputStart = 0x0010;
private const ushort XinputBack = 0x0020;
private const ushort XinputLeftThumb = 0x0040;
private const ushort XinputRightThumb = 0x0080;
private const ushort XinputLeftShoulder = 0x0100;
private const ushort XinputRightShoulder = 0x0200;
private const ushort XinputA = 0x1000;
private const ushort XinputB = 0x2000;
private const ushort XinputX = 0x4000;
private const ushort XinputY = 0x8000;
private static readonly object Gate = new();
private static PadState _state;
private static bool _started;
private static int _slot = -1; // connected XInput user index, -1 when none
private static byte _motorLeft;
private static byte _motorRight;
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
internal static void EnsureStarted()
{
if (!OperatingSystem.IsWindows())
{
return;
}
lock (Gate)
{
if (_started)
{
return;
}
_started = true;
var thread = new Thread(ReadLoop)
{
IsBackground = true,
Name = "XInputReader",
};
thread.Start();
}
}
internal static bool TryGetState(out PadState state)
{
lock (Gate)
{
state = _state;
}
return state.Connected;
}
private static void SetState(in PadState state)
{
lock (Gate)
{
_state = state;
}
}
/// <summary>Sets rumble; large = left/strong motor, small = right/weak.</summary>
internal static void SetRumble(byte largeMotor, byte smallMotor)
{
lock (Gate)
{
if (_motorLeft == largeMotor && _motorRight == smallMotor)
{
return;
}
_motorLeft = largeMotor;
_motorRight = smallMotor;
SendRumbleLocked();
}
}
private static void SendRumbleLocked()
{
if (_slot < 0)
{
return; // resent on connect
}
var vibration = new XInputVibration
{
LeftMotorSpeed = (ushort)(_motorLeft * 257), // 0..255 -> 0..65535
RightMotorSpeed = (ushort)(_motorRight * 257),
};
_ = XInputSetState((uint)_slot, ref vibration);
}
private static void ReadLoop()
{
try
{
while (true)
{
var slot = FindConnectedSlot();
if (slot < 0)
{
SetState(default);
Thread.Sleep(1000);
continue;
}
lock (Gate)
{
_slot = slot;
SendRumbleLocked();
}
Console.Error.WriteLine("[LOADER][INFO] XInput (Xbox) controller connected.");
while (XInputGetState((uint)slot, out var state) == ErrorSuccess)
{
SetState(Translate(state.Gamepad));
Thread.Sleep(8);
}
Console.Error.WriteLine("[LOADER][INFO] XInput (Xbox) controller disconnected.");
lock (Gate)
{
_slot = -1;
_motorLeft = 0;
_motorRight = 0;
_state = default;
}
Thread.Sleep(1000);
}
}
catch (DllNotFoundException)
{
// XInput unavailable on this system; leave the reader disconnected.
}
catch (EntryPointNotFoundException)
{
}
}
private static int FindConnectedSlot()
{
for (var index = 0; index < SlotCount; index++)
{
if (XInputGetState((uint)index, out _) == ErrorSuccess)
{
return index;
}
}
return -1;
}
private static PadState Translate(in XInputGamepad pad)
{
uint buttons = 0;
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? OrbisPadButton.Up : 0;
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? OrbisPadButton.Down : 0;
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? OrbisPadButton.Left : 0;
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? OrbisPadButton.Right : 0;
buttons |= (pad.Buttons & XinputStart) != 0 ? OrbisPadButton.Options : 0;
buttons |= (pad.Buttons & XinputBack) != 0 ? OrbisPadButton.TouchPad : 0;
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? OrbisPadButton.L3 : 0;
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? OrbisPadButton.R3 : 0;
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? OrbisPadButton.L1 : 0;
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? OrbisPadButton.R1 : 0;
buttons |= (pad.Buttons & XinputA) != 0 ? OrbisPadButton.Cross : 0;
buttons |= (pad.Buttons & XinputB) != 0 ? OrbisPadButton.Circle : 0;
buttons |= (pad.Buttons & XinputX) != 0 ? OrbisPadButton.Square : 0;
buttons |= (pad.Buttons & XinputY) != 0 ? OrbisPadButton.Triangle : 0;
buttons |= pad.LeftTrigger > TriggerThreshold ? OrbisPadButton.L2 : 0;
buttons |= pad.RightTrigger > TriggerThreshold ? OrbisPadButton.R2 : 0;
return new PadState(
Connected: true,
Buttons: buttons,
LeftX: AxisToByte(pad.ThumbLX),
LeftY: AxisToByteInverted(pad.ThumbLY),
RightX: AxisToByte(pad.ThumbRX),
RightY: AxisToByteInverted(pad.ThumbRY),
L2: pad.LeftTrigger,
R2: pad.RightTrigger);
}
private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8);
// XInput Y grows upward, ORBIS pads report Y growing downward.
private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8));
[StructLayout(LayoutKind.Sequential)]
private struct XInputGamepad
{
public ushort Buttons;
public byte LeftTrigger;
public byte RightTrigger;
public short ThumbLX;
public short ThumbLY;
public short ThumbRX;
public short ThumbRY;
}
[StructLayout(LayoutKind.Sequential)]
private struct XInputState
{
public uint PacketNumber;
public XInputGamepad Gamepad;
}
[StructLayout(LayoutKind.Sequential)]
private struct XInputVibration
{
public ushort LeftMotorSpeed;
public ushort RightMotorSpeed;
}
// xinput1_4.dll ships with Windows 8 and later.
[DllImport("xinput1_4.dll")]
private static extern uint XInputGetState(uint userIndex, out XInputState state);
[DllImport("xinput1_4.dll")]
private static extern uint XInputSetState(uint userIndex, ref XInputVibration vibration);
}