Merge pull request 'Rumble Implementation' (#34) from MediaMoots/MeloNX:rumbleDev into XC-ios-ht

Reviewed-on: https://git.743378673.xyz/MeloNX/MeloNX/pulls/34
This commit is contained in:
stossy11 2025-05-31 04:14:42 +00:00
commit 056bcb6736
4 changed files with 173 additions and 87 deletions

View file

@ -14,13 +14,29 @@ class NativeController: Hashable, BaseController {
private var nativeController: GCController private var nativeController: GCController
private var controllerMotionProvider: ControllerMotionProvider? private var controllerMotionProvider: ControllerMotionProvider?
private let controllerHaptics: CHHapticEngine? private let controllerHaptics: CHHapticEngine?
private let rumbleController: RumbleController?
public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" } public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" }
init(_ controller: GCController) { init(_ controller: GCController) {
nativeController = controller nativeController = controller
controllerHaptics = nativeController.haptics?.createEngine(withLocality: .default) controllerHaptics = nativeController.haptics?.createEngine(withLocality: .default)
try? controllerHaptics?.start()
// Make sure the haptic engine exists before attempting to start it or initialize the controller.
if let hapticsEngine = controllerHaptics {
do {
try hapticsEngine.start()
rumbleController = RumbleController(engine: hapticsEngine, rumbleMultiplier: 2.5)
// print("CHHapticEngine started and RumbleController initialized.")
} catch {
// print("Error starting CHHapticEngine: \(error.localizedDescription)")
rumbleController = nil
}
} else {
// print("CHHapticEngine is nil. Cannot initialize RumbleController.")
rumbleController = nil
}
setupHandheldController() setupHandheldController()
} }
@ -70,7 +86,7 @@ class NativeController: Hashable, BaseController {
// print("Rumble with \(lowFreq), \(highFreq)") // print("Rumble with \(lowFreq), \(highFreq)")
guard let userdata else { return 0 } guard let userdata else { return 0 }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue() let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics) _self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
return 0 return 0
}, },
RumbleTriggers: { userdata, leftRumble, rightRumble in RumbleTriggers: { userdata, leftRumble, rightRumble in
@ -161,42 +177,6 @@ class NativeController: Hashable, BaseController {
} }
} }
static func rumble(lowFreq: Float, highFreq: Float) {
do {
// Low-frequency haptic pattern
let lowFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
], relativeTime: 0, duration: 0.2)
], parameters: [])
// High-frequency haptic pattern
let highFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
], relativeTime: 0.2, duration: 0.2)
], parameters: [])
// Create and start the haptic engine
let engine = try CHHapticEngine()
try engine.start()
// Create and play the low-frequency player
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
try lowFreqPlayer.start(atTime: 0)
// Create and play the high-frequency player after a short delay
let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
try highFreqPlayer.start(atTime: 0.2)
} catch {
// print("Error creating haptic patterns: \(error)")
}
}
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) { func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return } guard controller != nil else { return }
let joystick = SDL_JoystickFromInstanceID(instanceID) let joystick = SDL_JoystickFromInstanceID(instanceID)

View file

@ -0,0 +1,132 @@
//
// RumbleController.swift
// MeloNX
//
// Created by MediaMoots on 2025/5/24.
//
import CoreHaptics
import Foundation
class RumbleController {
private var engine: CHHapticEngine?
private var lowHapticPlayer: CHHapticPatternPlayer?
private var highHapticPlayer: CHHapticPatternPlayer?
private var rumbleMultiplier: Float = 1.0
// The duration of each continuous haptic event.
// We'll restart the players before this duration expires.
private let hapticEventDuration: TimeInterval = 7200
// Timer to schedule player restarts
private var playerRestartTimer: Timer?
// Interval before the haptic event duration runs out to restart
private let restartGracePeriod: TimeInterval = 1.0
init (engine: CHHapticEngine?, rumbleMultiplier: Float) {
self.engine = engine
self.rumbleMultiplier = rumbleMultiplier
createPlayers()
setupPlayerRestartTimer()
}
// Deinitializer to clean up the timer and stop players when the controller is deallocated
deinit {
playerRestartTimer?.invalidate() // Stop the timer
playerRestartTimer = nil
// Optionally stop the haptic players immediately
try? lowHapticPlayer?.stop(atTime: CHHapticTimeImmediate)
try? highHapticPlayer?.stop(atTime: CHHapticTimeImmediate)
// print("RumbleController deinitialized.")
}
// MARK: - Private Methods for Player Management
private func createPlayers() {
// Ensure the engine is available before proceeding
guard let engine = self.engine else {
// print("CHHapticEngine is nil. Cannot initialize RumbleController.")
return
}
do {
let baseIntensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
let lowSharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.0)
let highSharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1)
// Create continuous haptic events with the defined duration
let lowContinuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [baseIntensity, lowSharpness], relativeTime: 0, duration: hapticEventDuration)
let highContinuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [baseIntensity, highSharpness], relativeTime: 0, duration: hapticEventDuration)
// Create patterns from the continuous haptic events.
let lowPattern = try CHHapticPattern(events: [lowContinuousEvent], parameters: [])
let highPattern = try CHHapticPattern(events: [highContinuousEvent], parameters: [])
// Make players from the patterns
lowHapticPlayer = try engine.makePlayer(with: lowPattern)
highHapticPlayer = try engine.makePlayer(with: highPattern)
rumble(lowFreq: 0, highFreq: 0)
// Start players initially
try lowHapticPlayer?.start(atTime: 0)
try highHapticPlayer?.start(atTime: 0)
} catch {
// print("Error initializing RumbleController or setting up haptic player: \(error.localizedDescription)")
// Clean up if setup fails
lowHapticPlayer = nil
highHapticPlayer = nil
playerRestartTimer?.invalidate()
playerRestartTimer = nil
}
}
private func setupPlayerRestartTimer() {
// Invalidate any existing timer to prevent multiple timers if init is called multiple times
playerRestartTimer?.invalidate()
// Calculate the interval for restarting: 1 second before the haptic event duration ends
let restartInterval = hapticEventDuration - restartGracePeriod
guard restartInterval > 0 else {
// print("Warning: hapticEventDuration (\(hapticEventDuration)s) is too short for scheduled restart with grace period (\(restartGracePeriod)s). Timer will not be set.")
return
}
// Schedule a repeating timer that calls restartPlayers()
playerRestartTimer = Timer.scheduledTimer(withTimeInterval: restartInterval, repeats: true) { [weak self] _ in
self?.createPlayers()
}
// Ensure the timer is added to the current run loop in its default mode
RunLoop.current.add(playerRestartTimer!, forMode: .default)
// print("Haptic Players restart timer scheduled to fire every \(restartInterval) seconds.")
}
// MARK: - Public Rumble Control
public func rumble(lowFreq: Float, highFreq: Float) {
// Normalize SDL values (0-65535) to CoreHaptics range (0.0-1.0)
let normalizedLow = min(1.0, max(0.0, lowFreq * rumbleMultiplier / 65535.0))
let normalizedHigh = min(1.0, max(0.0, highFreq * rumbleMultiplier / 65535.0))
// Create dynamic parameters to control intensity
let lowIntensityParameter = CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: normalizedLow, relativeTime: 0)
let highIntensityParameter = CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: normalizedHigh, relativeTime: 0)
// Send parameters to the players
do {
try lowHapticPlayer?.sendParameters([lowIntensityParameter], atTime: 0)
try highHapticPlayer?.sendParameters([highIntensityParameter], atTime: 0)
} catch {
// print("Error sending haptic parameters: \(error.localizedDescription)")
}
}
}

View file

@ -12,11 +12,29 @@ import UIKit
class VirtualController : BaseController { class VirtualController : BaseController {
private var instanceID: SDL_JoystickID = -1 private var instanceID: SDL_JoystickID = -1
private var controller: OpaquePointer? private var controller: OpaquePointer?
private let hapticEngine: CHHapticEngine?
private let rumbleController: RumbleController?
private var deviceMotionProvider: DeviceMotionProvider? private var deviceMotionProvider: DeviceMotionProvider?
public let controllername = "MeloNX Touch Controller" public let controllername = "MeloNX Touch Controller"
init() { init() {
// Setup Haptics
hapticEngine = try? CHHapticEngine()
if let hapticsEngine = hapticEngine {
do {
try hapticsEngine.start()
rumbleController = RumbleController(engine: hapticsEngine, rumbleMultiplier: 2.0)
// print("CHHapticEngine started and RumbleController initialized.")
} catch {
// print("Error starting CHHapticEngine: \(error.localizedDescription)")
rumbleController = nil
}
} else {
// print("CHHapticEngine is nil. Cannot initialize RumbleController.")
rumbleController = nil
}
setupVirtualController() setupVirtualController()
} }
@ -51,7 +69,7 @@ class VirtualController : BaseController {
button_mask: 0, button_mask: 0,
axis_mask: 0, axis_mask: 0,
name: controllername.withCString { $0 }, name: controllername.withCString { $0 },
userdata: nil, userdata: Unmanaged.passUnretained(self).toOpaque(),
Update: { userdata in Update: { userdata in
// Update joystick state here // Update joystick state here
}, },
@ -61,7 +79,9 @@ class VirtualController : BaseController {
Rumble: { userdata, lowFreq, highFreq in Rumble: { userdata, lowFreq, highFreq in
// print("Rumble with \(lowFreq), \(highFreq)") // print("Rumble with \(lowFreq), \(highFreq)")
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone {
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq)) guard let userdata else { return 0 }
let _self = Unmanaged<VirtualController>.fromOpaque(userdata).takeUnretainedValue()
_self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
} }
return 0 return 0
}, },
@ -93,52 +113,6 @@ class VirtualController : BaseController {
} }
} }
static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
do {
let lowFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
], relativeTime: 0, duration: 0.2)
], parameters: [])
let highFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
], relativeTime: 0.2, duration: 0.2)
], parameters: [])
var engine = engine
if engine == nil {
if hapticEngine == nil {
hapticEngine = try CHHapticEngine()
try hapticEngine?.start()
}
engine = hapticEngine
}
guard let engine else {
return // print("Error creating haptic patterns: hapticEngine is nil")
}
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
try lowFreqPlayer.start(atTime: 0)
let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
try highFreqPlayer.start(atTime: 0)
} catch {
// print("Error creating haptic patterns: \(error)")
}
}
private static var hapticEngine: CHHapticEngine?
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) { func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return } guard controller != nil else { return }
let joystick = SDL_JoystickFromInstanceID(instanceID) let joystick = SDL_JoystickFromInstanceID(instanceID)

View file

@ -1071,7 +1071,7 @@ namespace Ryujinx.Headless.SDL2
{ {
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
EnableRumble = false, EnableRumble = true,
}, },
}; };