Add rumble support on iOS 14

This commit is contained in:
Cameron Gutman 2020-09-17 20:32:41 -05:00
parent 2d24b0ec7b
commit e8c7eb67c6
5 changed files with 192 additions and 49 deletions

View File

@ -6,7 +6,10 @@
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
//
#import "HapticContext.h"
@import GameController;
@import CoreHaptics;
@interface Controller : NSObject
@ -21,7 +24,8 @@
@property (nonatomic) short lastLeftStickY;
@property (nonatomic) short lastRightStickX;
@property (nonatomic) short lastRightStickY;
@property (nonatomic) unsigned short lowFreqMotor;
@property (nonatomic) unsigned short highFreqMotor;
@property (nonatomic) HapticContext* _Nullable lowFreqMotor;
@property (nonatomic) HapticContext* _Nullable highFreqMotor;
@end

View File

@ -20,7 +20,6 @@
@implementation ControllerSupport {
NSLock *_controllerStreamLock;
NSMutableDictionary *_controllers;
NSTimer *_rumbleTimer;
id<GamepadPresenceDelegate> _presenceDelegate;
OnScreenControls *_osc;
@ -41,20 +40,6 @@
#define UPDATE_BUTTON_FLAG(controller, x, y) \
((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x])
-(void) rumbleController: (Controller*)controller
{
#if 0
// Only vibrate if the amplitude is large enough
if (controller.lowFreqMotor > 0x5000 || controller.highFreqMotor > 0x5000) {
// If the gamepad is nil (on-screen controls) or it's attached to the device,
// then vibrate the device itself
if (controller.gamepad == nil || [controller.gamepad isAttachedToDevice]) {
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
}
}
#endif
}
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
{
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
@ -67,12 +52,8 @@
return;
}
// Update the motor levels for the rumble timer to grab next iteration
controller.lowFreqMotor = lowFreqMotor;
controller.highFreqMotor = highFreqMotor;
// Rumble now to ensure short vibrations aren't missed
[self rumbleController:controller];
[controller.lowFreqMotor setMotorAmplitude:lowFreqMotor];
[controller.highFreqMotor setMotorAmplitude:highFreqMotor];
}
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y
@ -212,6 +193,18 @@
}
}
-(void) initializeControllerHaptics:(Controller*) controller
{
controller.lowFreqMotor = [HapticContext createContextForLowFreqMotor:controller.gamepad];
controller.highFreqMotor = [HapticContext createContextForHighFreqMotor:controller.gamepad];
}
-(void) cleanupControllerHaptics:(Controller*) controller
{
[controller.lowFreqMotor cleanup];
[controller.highFreqMotor cleanup];
}
-(void) registerControllerCallbacks:(GCController*) controller
{
if (controller != NULL) {
@ -402,6 +395,9 @@
}
}
// Prepare controller haptics for use
[self initializeControllerHaptics:limeController];
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]];
Log(LOG_I, @"Assigning controller index: %d", i);
@ -461,23 +457,6 @@
return mask;
}
-(void) rumbleTimer
{
for (int i = 0; i < 4; i++) {
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:i]];
if (controller == nil && i == 0 && _oscEnabled) {
// No physical controller, but we have on-screen controls
controller = _player0osc;
}
if (controller == nil) {
// No connected controller for this player
continue;
}
[self rumbleController:controller];
}
}
-(NSUInteger) getConnectedGamepadCount
{
return _controllers.count;
@ -499,12 +478,6 @@
DataManager* dataMan = [[DataManager alloc] init];
_oscEnabled = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue] != OnScreenControlsLevelOff;
_rumbleTimer = [NSTimer scheduledTimerWithTimeInterval:0.20
target:self
selector:@selector(rumbleTimer)
userInfo:nil
repeats:YES];
Log(LOG_I, @"Number of supported controllers connected: %d", [ControllerSupport getGamepadCount]);
Log(LOG_I, @"Multi-controller: %d", _multiController);
@ -553,6 +526,10 @@
// Unset the GCController on this object (in case it is the OSC, which will persist)
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
// Stop haptics on this controller
[self cleanupControllerHaptics:limeController];
limeController.gamepad = nil;
// Inform the server of the updated active gamepads before removing this controller
@ -570,12 +547,15 @@
-(void) cleanup
{
[_rumbleTimer invalidate];
_rumbleTimer = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
[_controllers removeAllObjects];
_controllerNumbers = 0;
for (Controller* controller in [_controllers allValues]) {
[self cleanupControllerHaptics:controller];
}
[_controllers removeAllObjects];
for (GCController* controller in [GCController controllers]) {
if ([ControllerSupport isSupportedGamepad:controller]) {
[self unregisterControllerCallbacks:controller];

View File

@ -0,0 +1,20 @@
//
// HapticContext.h
// Moonlight
//
// Created by Cameron Gutman on 9/17/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
@import CoreHaptics;
@import GameController;
@interface HapticContext : NSObject
-(void)setMotorAmplitude:(unsigned short)amplitude;
-(void)cleanup;
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad;
@end

View File

@ -0,0 +1,131 @@
//
// HapticContext.m
// Moonlight
//
// Created by Cameron Gutman on 9/17/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
#import "HapticContext.h"
@import CoreHaptics;
@import GameController;
@implementation HapticContext {
GCControllerPlayerIndex _playerIndex;
CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0));
id<CHHapticPatternPlayer> _hapticPlayer API_AVAILABLE(ios(13.0));
}
-(void)cleanup API_AVAILABLE(ios(14.0)) {
if (_hapticPlayer != nil) {
[_hapticPlayer cancelAndReturnError:nil];
_hapticPlayer = nil;
}
if (_hapticEngine != nil) {
[_hapticEngine stopWithCompletionHandler:nil];
_hapticEngine = nil;
}
}
-(void)setMotorAmplitude:(unsigned short)amplitude API_AVAILABLE(ios(14.0)) {
NSError* error;
// Cancel the last haptic effect
if (_hapticPlayer != nil) {
[_hapticPlayer stopAtTime:0 error:&error];
_hapticPlayer = nil;
}
// Check if the haptic engine died
if (_hapticEngine == nil) {
return;
}
// Don't bother queuing a 0 amplitude haptic event
if (amplitude == 0) {
return;
}
CHHapticEventParameter* intensityParameter = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:amplitude / 65536.0f];
CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObjects:intensityParameter, nil] relativeTime:0 duration:GCHapticDurationInfinite];
CHHapticPattern* hapticPattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:hapticEvent] parameters:[[NSArray alloc] init] error:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Haptic pattern creation failed: %@", _playerIndex, error);
return;
}
_hapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error);
return;
}
[_hapticPlayer startAtTime:0 error:&error];
if (error != nil) {
_hapticPlayer = nil;
Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error);
return;
}
}
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0)) {
if (gamepad.haptics == nil) {
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
return nil;
}
_playerIndex = gamepad.playerIndex;
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
NSError* error;
[_hapticEngine startAndReturnError:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
return nil;
}
__weak typeof(self) weakSelf = self;
_hapticEngine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
HapticContext* me = weakSelf;
if (me == nil) {
return;
}
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
me->_hapticPlayer = nil;
me->_hapticEngine = nil;
};
_hapticEngine.resetHandler = ^{
HapticContext* me = weakSelf;
if (me == nil) {
return;
}
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
me->_hapticPlayer = nil;
[me->_hapticEngine startAndReturnError:nil];
};
return self;
}
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad {
if (@available(iOS 14.0, tvOS 14.0, *)) {
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
}
else {
return nil;
}
}
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad {
if (@available(iOS 14.0, tvOS 14.0, *)) {
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
}
else {
return nil;
}
}
@end

View File

@ -8,6 +8,8 @@
/* Begin PBXBuildFile section */
693B3A9B218638CD00982F7B /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 693B3A9A218638CD00982F7B /* Settings.bundle */; };
9827E7A32514366900F25707 /* HapticContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 9827E7A22514366900F25707 /* HapticContext.m */; };
9827E7A42514366900F25707 /* HapticContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 9827E7A22514366900F25707 /* HapticContext.m */; };
9832D1361BBCD5C50036EF48 /* TemporaryApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 9832D1351BBCD5C50036EF48 /* TemporaryApp.m */; };
9865DC30213260B40005B9B9 /* libmoonlight-common-tv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB1A68152132509400507771 /* libmoonlight-common-tv.a */; };
9865DC31213260F10005B9B9 /* mkcert.c in Sources */ = {isa = PBXBuildFile; fileRef = FB89460719F646E200339C8A /* mkcert.c */; };
@ -157,6 +159,8 @@
/* Begin PBXFileReference section */
693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
98132E8C20BC9A62007A053F /* Moonlight v1.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.1.xcdatamodel"; sourceTree = "<group>"; };
9827E7A22514366900F25707 /* HapticContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HapticContext.m; sourceTree = "<group>"; };
9827E7A7251436EA00F25707 /* HapticContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HapticContext.h; sourceTree = "<group>"; };
9832D1341BBCD5C50036EF48 /* TemporaryApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporaryApp.h; path = Database/TemporaryApp.h; sourceTree = "<group>"; };
9832D1351BBCD5C50036EF48 /* TemporaryApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporaryApp.m; path = Database/TemporaryApp.m; sourceTree = "<group>"; };
98517B1B21CE0A9000481377 /* Moonlight v1.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.3.xcdatamodel"; sourceTree = "<group>"; };
@ -533,6 +537,8 @@
FB1A674C2131E65900507771 /* KeyboardSupport.m */,
9897B6A0221260EF00966419 /* Controller.m */,
9897B6A32212610800966419 /* Controller.h */,
9827E7A22514366900F25707 /* HapticContext.m */,
9827E7A7251436EA00F25707 /* HapticContext.h */,
);
path = Input;
sourceTree = "<group>";
@ -945,6 +951,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9827E7A42514366900F25707 /* HapticContext.m in Sources */,
9897B6A62212732C00966419 /* Controller.m in Sources */,
9865DC3E21332D660005B9B9 /* MainFrameViewController.m in Sources */,
9865DC36213287F30005B9B9 /* AppDelegate.m in Sources */,
@ -1023,6 +1030,7 @@
FBD1C8E21A8AD71400C6703C /* Logger.m in Sources */,
FB1D599A1BBCCD7E00F482CA /* AppCollectionView.m in Sources */,
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */,
9827E7A32514366900F25707 /* HapticContext.m in Sources */,
9832D1361BBCD5C50036EF48 /* TemporaryApp.m in Sources */,
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */,
FB89462819F646E200339C8A /* CryptoManager.m in Sources */,