mirror of
https://git.743378673.xyz/MeloNX/MeloNX.git
synced 2025-06-27 19:06:23 +02:00
rumble implementation
This commit is contained in:
parent
bff023563b
commit
d7dad1b848
4 changed files with 172 additions and 86 deletions
|
@ -13,13 +13,29 @@ class NativeController: Hashable {
|
||||||
private var controller: OpaquePointer?
|
private var controller: OpaquePointer?
|
||||||
private var nativeController: GCController
|
private var nativeController: GCController
|
||||||
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: 3.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
|
||||||
|
}
|
||||||
setupHandheldController()
|
setupHandheldController()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +71,7 @@ class NativeController: Hashable {
|
||||||
// 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
|
||||||
|
@ -146,42 +162,6 @@ class NativeController: Hashable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,10 +12,28 @@ import UIKit
|
||||||
class VirtualController {
|
class VirtualController {
|
||||||
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?
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +64,9 @@ class VirtualController {
|
||||||
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
|
||||||
},
|
},
|
||||||
|
@ -78,52 +98,6 @@ class VirtualController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -1070,7 +1070,7 @@ namespace Ryujinx.Headless.SDL2
|
||||||
{
|
{
|
||||||
StrongRumble = 1f,
|
StrongRumble = 1f,
|
||||||
WeakRumble = 1f,
|
WeakRumble = 1f,
|
||||||
EnableRumble = false,
|
EnableRumble = true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue