rumble implementation

This commit is contained in:
MediaMoots 2025-05-24 15:56:38 +08:00
parent bff023563b
commit d7dad1b848
4 changed files with 172 additions and 86 deletions

View file

@ -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)

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,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)

View file

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