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 nativeController: GCController
|
||||
private let controllerHaptics: CHHapticEngine?
|
||||
private let rumbleController: RumbleController?
|
||||
|
||||
public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" }
|
||||
|
||||
init(_ controller: GCController) {
|
||||
nativeController = controller
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -55,7 +71,7 @@ class NativeController: Hashable {
|
|||
// print("Rumble with \(lowFreq), \(highFreq)")
|
||||
guard let userdata else { return 0 }
|
||||
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
|
||||
},
|
||||
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) {
|
||||
guard controller != nil else { return }
|
||||
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 {
|
||||
private var instanceID: SDL_JoystickID = -1
|
||||
private var controller: OpaquePointer?
|
||||
private let hapticEngine: CHHapticEngine?
|
||||
private let rumbleController: RumbleController?
|
||||
|
||||
public let controllername = "MeloNX Touch Controller"
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -46,7 +64,9 @@ class VirtualController {
|
|||
Rumble: { userdata, lowFreq, highFreq in
|
||||
// print("Rumble with \(lowFreq), \(highFreq)")
|
||||
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
|
||||
},
|
||||
|
@ -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) {
|
||||
guard controller != nil else { return }
|
||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
||||
|
|
|
@ -1070,7 +1070,7 @@ namespace Ryujinx.Headless.SDL2
|
|||
{
|
||||
StrongRumble = 1f,
|
||||
WeakRumble = 1f,
|
||||
EnableRumble = false,
|
||||
EnableRumble = true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue