diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index 4d7da8ef..d0b7a0f9 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -264,7 +264,7 @@ - + - + - - + + + @@ -414,37 +415,37 @@ - - + - - + @@ -1078,48 +1079,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2324,12 +2323,14 @@ + + - + @@ -2338,15 +2339,9 @@ - - - - - - @@ -2367,6 +2362,8 @@ + + @@ -2468,6 +2465,8 @@ + + @@ -3978,19 +3977,19 @@ - + - + - + - + diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard index 93a44458..5710602e 100644 --- a/VoidLink/Base.lproj/iPhone.storyboard +++ b/VoidLink/Base.lproj/iPhone.storyboard @@ -2405,12 +2405,13 @@ - + - - + + + @@ -2551,37 +2552,37 @@ - - + - - + @@ -3127,28 +3128,8 @@ - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - @@ -3551,6 +3512,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3591,6 +3592,8 @@ + + @@ -3605,15 +3608,9 @@ - - - - - - @@ -3634,6 +3631,8 @@ + + @@ -3720,6 +3719,8 @@ + + diff --git a/VoidLink/Input/ControllerSupport.h b/VoidLink/Input/ControllerSupport.h index ae44d42a..14f849f3 100644 --- a/VoidLink/Input/ControllerSupport.h +++ b/VoidLink/Input/ControllerSupport.h @@ -63,6 +63,15 @@ rightSharpness:(float)rightSharpness rightTransient:(float)rightTransient delaySeconds:(double)delaySeconds; +-(BOOL) hasDualSenseController:(uint16_t)controllerNumber; +-(void) renderDeviceDualSenseHaptics:(uint16_t)controllerNumber + leftAmplitude:(float)leftAmplitude + leftSharpness:(float)leftSharpness + leftTransient:(float)leftTransient + rightAmplitude:(float)rightAmplitude + rightSharpness:(float)rightSharpness + rightTransient:(float)rightTransient + delaySeconds:(double)delaySeconds; -(void) cancelScheduledDualSenseHaptics; -(void) updateTimerStateForOsc; diff --git a/VoidLink/Input/ControllerSupport.m b/VoidLink/Input/ControllerSupport.m index 1e6ca84b..b11424d4 100644 --- a/VoidLink/Input/ControllerSupport.m +++ b/VoidLink/Input/ControllerSupport.m @@ -184,9 +184,11 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger, case RumbleDevice: [_oscController.lowFreqMotor setMotorAmplitude:lowFreqMotor]; [_oscController.highFreqMotor setMotorAmplitude:highFreqMotor]; + break; case LeftRightSwapped: [voidController.lowFreqMotor setMotorAmplitude:highFreqMotor]; [voidController.highFreqMotor setMotorAmplitude:lowFreqMotor]; + break; case RumbleOff: break; default: @@ -212,250 +214,317 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger, return CGRectGetWidth([[UIScreen mainScreen]bounds]) > CGRectGetHeight([[UIScreen mainScreen]bounds]); } --(void)updateTimerStateForController:(VoidController* )voidController{ - // if (@available(iOS 14.0, tvOS 14.0, *)) { - if (true) { - if(_gyroMode == GyroModeOff){ - [self stopTimerForController:voidController]; - return; - } - - if(voidController.gamepad.motion.hasAttitudeAndRotationRate) [voidController.motionTypes addObject:@(LI_MOTION_TYPE_ACCEL)]; - if (@available(iOS 14.0, *)) if(voidController.gamepad.motion.hasRotationRate) [voidController.motionTypes addObject:@(LI_MOTION_TYPE_GYRO)]; +#if !TARGET_OS_TV +- (UIInterfaceOrientation)currentInterfaceOrientation +{ + if (@available(iOS 13.0, *)) { + for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { + if (![scene isKindOfClass:UIWindowScene.class]) { + continue; + } - for(NSNumber* motionTypeObj in voidController.motionTypes){ - uint8_t motionType = motionTypeObj.intValue; + UIWindowScene *windowScene = (UIWindowScene *)scene; + if (windowScene.activationState != UISceneActivationStateForegroundActive) { + continue; + } + + if (windowScene.interfaceOrientation != UIInterfaceOrientationUnknown) { + return windowScene.interfaceOrientation; + } + } + + for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { + if (![scene isKindOfClass:UIWindowScene.class]) { + continue; + } + + UIWindowScene *windowScene = (UIWindowScene *)scene; + if (windowScene.activationState == UISceneActivationStateUnattached) { + continue; + } + + if (windowScene.interfaceOrientation != UIInterfaceOrientationUnknown) { + return windowScene.interfaceOrientation; + } + } + } + + return UIApplication.sharedApplication.statusBarOrientation; +} +#endif + +-(void)updateTimerStateForController:(VoidController* )voidController{ + if (![NSThread isMainThread]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateTimerStateForController:voidController]; + }); + return; + } + + if (voidController == nil || voidController.reportRateHz == 0) { + [self stopTimerForController:voidController]; + return; + } + + // if (@available(iOS 14.0, tvOS 14.0, *)) { + if(_gyroMode == GyroModeOff){ + [self stopTimerForController:voidController]; + return; + } + if([ControllerUtil hasControllerAccelerometer:voidController.gamepad]) { + [voidController.motionTypes addObject:@(LI_MOTION_TYPE_ACCEL)]; + } + if (@available(iOS 14.0, *)) if(voidController.gamepad.motion.hasRotationRate){ + [voidController.motionTypes addObject:@(LI_MOTION_TYPE_GYRO)]; + } + + for(NSNumber* motionTypeObj in voidController.motionTypes){ + uint8_t motionType = motionTypeObj.intValue; #if !TARGET_OS_TV //tvOS has no device motion - if(voidController == _oscController){ - //Player has no controller *or* no motion for controller 1 *or* wants to override controller 1 motion with device motion - if(!voidController.motionManager) { - voidController.motionManager = [[CMMotionManager alloc] init]; - } - - switch (motionType) { - case LI_MOTION_TYPE_ACCEL: - [voidController.accelTimer invalidate]; - voidController.accelTimer = nil; - // Reset the last motion sample - CMAcceleration emptyDeviceAccelSample = {}; - voidController.lastDeviceAccelSample = emptyDeviceAccelSample; - - {dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"setup device built-in gyro accelTimer"); + if(voidController == _oscController){ + //Player has no controller *or* no motion for controller 1 *or* wants to override controller 1 motion with device motion + if(!voidController.motionManager) { + voidController.motionManager = [[CMMotionManager alloc] init]; + } + + switch (motionType) { + case LI_MOTION_TYPE_ACCEL: { + [voidController.accelTimer invalidate]; + voidController.accelTimer = nil; + // Reset the last motion sample + CMAcceleration emptyDeviceAccelSample = {}; + voidController.lastDeviceAccelSample = emptyDeviceAccelSample; + voidController.motionManager.deviceMotionUpdateInterval = 1.0 / voidController.reportRateHz; + [voidController.motionManager startDeviceMotionUpdates]; + {NSLog(@"setup device built-in accelTimer"); voidController.hasAccelerometer = YES; voidController.accelTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) { - // Don't send duplicate samples - CMAcceleration lastDeviceAccelSample = voidController.lastDeviceAccelSample; CMAcceleration deviceAccelSample = voidController.motionManager.deviceMotion.userAcceleration; //userAcceleration does not contain gravity, add gravity to x, y and z values: - deviceAccelSample.x += voidController.motionManager.deviceMotion.gravity.x * self->_gyroSensitivity; - deviceAccelSample.y += voidController.motionManager.deviceMotion.gravity.y * self->_gyroSensitivity; - deviceAccelSample.z += voidController.motionManager.deviceMotion.gravity.z * self->_gyroSensitivity; - - if (memcmp(&deviceAccelSample, &lastDeviceAccelSample, sizeof(deviceAccelSample)) == 0) { - return; - } - voidController.lastDeviceAccelSample = deviceAccelSample; - - // Convert g to m/s^2 - if (@available(iOS 13.0, *)) { - if(UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation == 4){ //check for landscape left or landscape right - LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, - LI_MOTION_TYPE_ACCEL, - deviceAccelSample.y * -9.80665f * self->_gyroSensitivity, - deviceAccelSample.z * -9.80665f * self->_gyroSensitivity, - deviceAccelSample.x * -9.80665f * self->_gyroSensitivity); - } - else{ - LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, - LI_MOTION_TYPE_ACCEL, - deviceAccelSample.y * +9.80665f * self->_gyroSensitivity, - deviceAccelSample.z * -9.80665f * self->_gyroSensitivity, - deviceAccelSample.x * +9.80665f * self->_gyroSensitivity); - } - } - else{ - LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, - LI_MOTION_TYPE_ACCEL, - deviceAccelSample.y * -9.80665f * self->_gyroSensitivity, - deviceAccelSample.z * -9.80665f * self->_gyroSensitivity, - deviceAccelSample.x * -9.80665f * self->_gyroSensitivity); - } - }]; - });} - break; - case LI_MOTION_TYPE_GYRO: - [voidController.gyroTimer invalidate]; - voidController.gyroTimer = nil; - - // Reset the last motion sample - CMRotationRate emptyDeviceGyroSample = {}; - voidController.lastDeviceGyroSample = emptyDeviceGyroSample; - [voidController.motionManager startDeviceMotionUpdates]; - - NSLog(@"setup device built-in gyro gyroTimer"); - voidController.hasGyroscope = YES; - voidController.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) { - - // Don't send duplicate samples - CMRotationRate lastDeviceGyroSample = voidController.lastDeviceGyroSample; - CMRotationRate deviceGyroSample = voidController.motionManager.deviceMotion.rotationRate; - if (memcmp(&deviceGyroSample, &lastDeviceGyroSample, sizeof(deviceGyroSample)) == 0) { - return; - } - voidController.lastDeviceGyroSample = deviceGyroSample; - - // Convert rad/s to deg/s - - UIInterfaceOrientation interfaceOrientation = UIInterfaceOrientationUnknown; - if (@available(iOS 13.0, *)) { - interfaceOrientation = UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation; - } else { - interfaceOrientation = UIApplication.sharedApplication.statusBarOrientation; - } - + deviceAccelSample.x += voidController.motionManager.deviceMotion.gravity.x; + deviceAccelSample.y += voidController.motionManager.deviceMotion.gravity.y; + deviceAccelSample.z += voidController.motionManager.deviceMotion.gravity.z; + NSLog(@"sending device accel %f", CACurrentMediaTime()); + + UIInterfaceOrientation interfaceOrientation = [self currentInterfaceOrientation]; + CMAcceleration mappedDeviceAccelSample = {}; switch (interfaceOrientation) { case UIInterfaceOrientationLandscapeLeft: - LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, - LI_MOTION_TYPE_GYRO, - self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0); + mappedDeviceAccelSample.x = deviceAccelSample.y * -9.80665f; + mappedDeviceAccelSample.y = deviceAccelSample.z * -9.80665f; + mappedDeviceAccelSample.z = deviceAccelSample.x * -9.80665f; break; case UIInterfaceOrientationLandscapeRight: - LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, - LI_MOTION_TYPE_GYRO, - self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0); + mappedDeviceAccelSample.x = deviceAccelSample.y * 9.80665f; + mappedDeviceAccelSample.y = deviceAccelSample.z * -9.80665f; + mappedDeviceAccelSample.z = deviceAccelSample.x * 9.80665f; break; case UIInterfaceOrientationPortrait: - LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, - LI_MOTION_TYPE_GYRO, - self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0); + mappedDeviceAccelSample.x = deviceAccelSample.x * -9.80665f; + mappedDeviceAccelSample.y = deviceAccelSample.z * -9.80665f; + mappedDeviceAccelSample.z = deviceAccelSample.y * 9.80665f; break; case UIInterfaceOrientationPortraitUpsideDown: - LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, - LI_MOTION_TYPE_GYRO, - self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0); + mappedDeviceAccelSample.x = deviceAccelSample.x * 9.80665f; + mappedDeviceAccelSample.y = deviceAccelSample.z * -9.80665f; + mappedDeviceAccelSample.z = deviceAccelSample.y * -9.80665f; break; default: - LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, - LI_MOTION_TYPE_GYRO, - self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0); + mappedDeviceAccelSample.x = deviceAccelSample.y * -9.80665f; + mappedDeviceAccelSample.y = deviceAccelSample.z * -9.80665f; + mappedDeviceAccelSample.z = deviceAccelSample.x * -9.80665f; break; } + + // Don't send duplicate samples after orientation remapping. + CMAcceleration lastDeviceAccelSample = voidController.lastDeviceAccelSample; + if (memcmp(&mappedDeviceAccelSample, &lastDeviceAccelSample, sizeof(mappedDeviceAccelSample)) == 0) { + return; + } + voidController.lastDeviceAccelSample = mappedDeviceAccelSample; - /* - if(UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation == 4){//check for landscape left or landscape right + // Convert g to m/s^2 + LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, + LI_MOTION_TYPE_ACCEL, + mappedDeviceAccelSample.x, + mappedDeviceAccelSample.y, + mappedDeviceAccelSample.z); + }];} + break; + } + case LI_MOTION_TYPE_GYRO: { + [voidController.gyroTimer invalidate]; + voidController.gyroTimer = nil; + + // Reset the last motion sample + CMRotationRate emptyDeviceGyroSample = {}; + voidController.lastDeviceGyroSample = emptyDeviceGyroSample; + [voidController.motionManager startDeviceMotionUpdates]; + + NSLog(@"setup device built-in gyro gyroTimer"); + voidController.hasGyroscope = YES; + voidController.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) { + + // Don't send duplicate samples + CMRotationRate lastDeviceGyroSample = voidController.lastDeviceGyroSample; + CMRotationRate deviceGyroSample = voidController.motionManager.deviceMotion.rotationRate; + if (memcmp(&deviceGyroSample, &lastDeviceGyroSample, sizeof(deviceGyroSample)) == 0) { + return; + } + voidController.lastDeviceGyroSample = deviceGyroSample; + + // Convert rad/s to deg/s + + UIInterfaceOrientation interfaceOrientation = [self currentInterfaceOrientation]; + + switch (interfaceOrientation) { + case UIInterfaceOrientationLandscapeLeft: LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_GYRO, self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0); - } - else{ + break; + case UIInterfaceOrientationLandscapeRight: LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_GYRO, self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0); - } - */ - }]; - break; - } - } - -#endif - else{ - NSLog(@"controller obj timer update: controller timer "); - - if (@available(iOS 14.0, *)) { - switch (motionType) { - case LI_MOTION_TYPE_ACCEL: - [voidController.accelTimer invalidate]; - voidController.accelTimer = nil; - - if (voidController.reportRateHz && voidController.gamepad.motion.hasGravityAndUserAcceleration) { - // Reset the last motion sample - GCAcceleration emptyAccelSample = {}; - voidController.lastAccelSample = emptyAccelSample; - NSLog(@"setup controller gyro accelTimer"); - dispatch_sync(dispatch_get_main_queue(), ^{ - voidController.hasAccelerometer = YES; - voidController.accelTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) { - // Don't send duplicate samples - GCAcceleration lastAccelSample = voidController.lastAccelSample; - GCAcceleration accelSample = voidController.gamepad.motion.acceleration; - - if (memcmp(&accelSample, &lastAccelSample, sizeof(accelSample)) == 0) { - return; - } - voidController.lastAccelSample = accelSample; - - // Convert g to m/s^2 - //NSLog(@"sending controller gyro data, accelSample data 00: %f, playerIndex: %ld, obj: %@",accelSample.x, (long)voidController.gamepad.playerIndex, voidController); - LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, - LI_MOTION_TYPE_ACCEL, - accelSample.x * -9.80665f * self->_gyroSensitivity, - accelSample.y * -9.80665f * self->_gyroSensitivity, - accelSample.z * -9.80665f * self->_gyroSensitivity); - }]; - }); - } - break; - - case LI_MOTION_TYPE_GYRO: - [voidController.gyroTimer invalidate]; - voidController.gyroTimer = nil; - - if (voidController.reportRateHz && voidController.gamepad.motion.hasRotationRate) { - // Reset the last motion sample - GCRotationRate emptyGyroSample = {}; - voidController.lastGyroSample = emptyGyroSample; - //dispatch_sync(dispatch_get_main_queue(), ^{ - {dispatch_async(dispatch_get_main_queue(), ^{ - voidController.hasGyroscope = YES; - voidController.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) { - // Don't send duplicate samples - GCRotationRate lastGyroSample = voidController.lastGyroSample; - GCRotationRate gyroSample = voidController.gamepad.motion.rotationRate; - if (memcmp(&gyroSample, &lastGyroSample, sizeof(gyroSample)) == 0) { - return; - } - voidController.lastGyroSample = gyroSample; - - // Convert rad/s to deg/s - // NSLog(@"sending controller gyro data, gyroSample data 00: %f, playerIndex: %ld, obj: %@",gyroSample.x, (long)voidController.gamepad.playerIndex, voidController); - LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, - LI_MOTION_TYPE_GYRO, - self->_gyroEnabledFlag ? gyroSample.x * 57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? gyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, - self->_gyroEnabledFlag ? gyroSample.y * -57.2957795f * self->_gyroSensitivity : 0); - }]; - // }); - });} - } - break; - } + break; + case UIInterfaceOrientationPortrait: + LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, + LI_MOTION_TYPE_GYRO, + self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0); + break; + case UIInterfaceOrientationPortraitUpsideDown: + LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, + LI_MOTION_TYPE_GYRO, + self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0); + break; + default: + LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, + LI_MOTION_TYPE_GYRO, + self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0); + break; + } + + /* + if(UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation == 4){//check for landscape left or landscape right + LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, + LI_MOTION_TYPE_GYRO, + self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0); + } + else{ + LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, + LI_MOTION_TYPE_GYRO, + self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0); + } + */ + }]; + break; } } } - NSLog(@"controller obj timer, motionTypes: %lu", (unsigned long)voidController.motionTypes.count); - - // Set the motion sensor state if they require manual activation - [self updateSensorSateForController:voidController]; - // NSLog(@"sensor active: %d", voidController.gamepad.motion.sensorsActive); +#endif + else{ + // NSLog(@"controller obj timer update: controller timer "); + if (@available(iOS 14.0, *)) { + switch (motionType) { + case LI_MOTION_TYPE_ACCEL: + [voidController.accelTimer invalidate]; + voidController.accelTimer = nil; + + // if (voidController.reportRateHz && voidController.gamepad.motion.hasGravityAndUserAcceleration) { + if (voidController.reportRateHz) { + // Reset the last motion sample + GCAcceleration emptyAccelSample = {}; + voidController.lastAccelSample = emptyAccelSample; + void (^setupAccelTimer)(void) = ^{ + voidController.hasAccelerometer = YES; + voidController.accelTimer = + [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz + repeats:YES + block:^(NSTimer *timer) { + GCAcceleration lastAccelSample = voidController.lastAccelSample; + GCAcceleration accelSample = voidController.gamepad.motion.acceleration; + + if (memcmp(&accelSample, &lastAccelSample, sizeof(accelSample)) == 0) { + return; + } + + voidController.lastAccelSample = accelSample; + + LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, + LI_MOTION_TYPE_ACCEL, + accelSample.x * -9.80665f, + accelSample.y * -9.80665f, + accelSample.z * -9.80665f); + }]; + }; + if ([NSThread isMainThread]) { + setupAccelTimer(); + } else { + dispatch_async(dispatch_get_main_queue(), setupAccelTimer); + } + } + break; + + case LI_MOTION_TYPE_GYRO: + [voidController.gyroTimer invalidate]; + voidController.gyroTimer = nil; + + if (voidController.reportRateHz && voidController.gamepad.motion.hasRotationRate) { + // Reset the last motion sample + GCRotationRate emptyGyroSample = {}; + voidController.lastGyroSample = emptyGyroSample; + //dispatch_sync(dispatch_get_main_queue(), ^{ + void (^setupGyroTimer)(void) = ^{ + voidController.hasGyroscope = YES; + voidController.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) { + // Don't send duplicate samples + GCRotationRate lastGyroSample = voidController.lastGyroSample; + GCRotationRate gyroSample = voidController.gamepad.motion.rotationRate; + if (memcmp(&gyroSample, &lastGyroSample, sizeof(gyroSample)) == 0) { + return; + } + voidController.lastGyroSample = gyroSample; + + // Convert rad/s to deg/s + // NSLog(@"sending controller gyro data, gyroSample data 00: %f, playerIndex: %ld, obj: %@",gyroSample.x, (long)voidController.gamepad.playerIndex, voidController); + LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, + LI_MOTION_TYPE_GYRO, + self->_gyroEnabledFlag ? gyroSample.x * 57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? gyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, + self->_gyroEnabledFlag ? gyroSample.y * -57.2957795f * self->_gyroSensitivity : 0); + }]; + }; + if ([NSThread isMainThread]) setupGyroTimer(); + else dispatch_async(dispatch_get_main_queue(), setupGyroTimer); + } + break; + } + } + } } + + NSLog(@"controller obj timer, motionTypes: %lu", (unsigned long)voidController.motionTypes.count); + + // Set the motion sensor state if they require manual activation + [self updateSensorSateForController:voidController]; + // NSLog(@"sensor active: %d", voidController.gamepad.motion.sensorsActive); } - (void)updateSensorSateForController:(VoidController* )voidController{ @@ -472,6 +541,13 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger, } - (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz { + if (![NSThread isMainThread]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz]; + }); + return; + } + //if (@available(iOS 14.0, tvOS 14.0, *)) { NSLog(@"gyroMode: %ld", (long)_gyroMode); @@ -483,16 +559,26 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger, if(!voidController.motionTypes){ voidController.motionTypes = [[NSMutableSet alloc] init]; } - [voidController.motionTypes addObject:@(motionType)]; + if (reportRateHz == 0) { + [voidController.motionTypes removeObject:@(motionType)]; + } + else { + [voidController.motionTypes addObject:@(motionType)]; + } voidController.hasGyroscope = NO; voidController.hasAccelerometer = NO; - voidController.reportRateHz = reportRateHz; } + voidController.reportRateHz = reportRateHz; voidController.controllerNumber = controllerNumber; - if(voidController == _oscController) [self updateTimerStateForController:voidController]; + if (reportRateHz == 0) { + [self stopTimerForController:voidController]; + } + else if(voidController == _oscController) { + [self updateTimerStateForController:voidController]; + } //} } @@ -605,6 +691,62 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger, } } +-(BOOL) hasDualSenseController:(uint16_t)controllerNumber { + __block BOOL result = NO; + dispatch_block_t checkBlock = ^{ + if (@available(iOS 14.5, tvOS 14.5, *)) { + VoidController* controller = [self->_voidControllers objectForKey:@(controllerNumber)]; + result = controller != nil && + [controller.gamepad.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]; + } + }; + + if ([NSThread isMainThread]) { + checkBlock(); + } + else { + dispatch_sync(dispatch_get_main_queue(), checkBlock); + } + return result; +} + +-(void) renderDeviceDualSenseHaptics:(uint16_t)controllerNumber + leftAmplitude:(float)leftAmplitude + leftSharpness:(float)leftSharpness + leftTransient:(float)leftTransient + rightAmplitude:(float)rightAmplitude + rightSharpness:(float)rightSharpness + rightTransient:(float)rightTransient + delaySeconds:(double)delaySeconds { + __block uint64_t generation; + @synchronized(self) { + generation = _authoredHapticsGeneration; + } + dispatch_block_t renderBlock = ^{ + @synchronized(self) { + if (generation != self->_authoredHapticsGeneration) { + return; + } + } + if (@available(iOS 14.0, tvOS 14.0, *)) { + [self->_oscController.lowFreqMotor setAuthoredAmplitude:leftAmplitude + sharpness:leftSharpness + transientStrength:leftTransient]; + [self->_oscController.highFreqMotor setAuthoredAmplitude:rightAmplitude + sharpness:rightSharpness + transientStrength:rightTransient]; + } + }; + + if (delaySeconds > 0.0) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delaySeconds * NSEC_PER_SEC)), + dispatch_get_main_queue(), renderBlock); + } + else { + dispatch_async(dispatch_get_main_queue(), renderBlock); + } +} + -(void) cancelScheduledDualSenseHaptics { @synchronized(self) { _authoredHapticsGeneration++; @@ -1020,7 +1162,8 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger, // LI_CTYPE_UNKNOWN for option "Both" if(voidController.playerIndex == 0){ - type = _streamConfig.emulatedControllerType == LI_CTYPE_UNKNOWN ? LI_CTYPE_PS : _streamConfig.emulatedControllerType; + type = _streamConfig.emulatedControllerType == ControllerEmulationXboxAndPs ? LI_CTYPE_PS : _streamConfig.emulatedControllerType; + if(type == ControllerEmulationPsEnhancedHaptic) type = ControllerEmulationPs; } if(voidController.playerIndex == 1 && _streamConfig.emulatedControllerType == LI_CTYPE_UNKNOWN){ @@ -1038,15 +1181,10 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger, if (@available(iOS 14.5, tvOS 14.5, *)) { if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) { type = LI_CTYPE_PS; + capabilities |= LI_CCAP_DS5_HAPTICS_PCM; } } } - - if (@available(iOS 14.5, tvOS 14.5, *)) { - if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) { - capabilities |= LI_CCAP_DS5_HAPTICS_PCM; - } - } // Detect supported haptics localities if (controller.haptics) { @@ -1081,7 +1219,8 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger, } bool controllerLacksGyro = (capabilities & LI_CCAP_GYRO) == 0; - if(_streamConfig.emulatedControllerType == LI_CTYPE_PS && (_gyroMode == AlwaysDevice || (_gyroMode == GyroModeAuto && controllerLacksGyro))) + if((_streamConfig.emulatedControllerType == ControllerEmulationPs || _streamConfig.emulatedControllerType == ControllerEmulationPsEnhancedHaptic) + && (_gyroMode == AlwaysDevice || (_gyroMode == GyroModeAuto && controllerLacksGyro))) { type = LI_CTYPE_PS; capabilities |= LI_CCAP_GYRO | LI_CCAP_ACCEL; @@ -1092,7 +1231,7 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger, // This is a virtual controller corresponding to our OSC // set osc to PS to utilize built-in gyro when "both" is selected type = _streamConfig.emulatedControllerType == LI_CTYPE_UNKNOWN ? LI_CTYPE_PS : _streamConfig.emulatedControllerType; - + if(type == ControllerEmulationPsEnhancedHaptic) type = LI_CTYPE_PS; /* if (_streamConfig.gyroMode != GyroModeOff) { type = LI_CTYPE_PS; @@ -1180,8 +1319,7 @@ double rc_expo(double x, double expo) { - (bool)useMotionHandler{ - return self->tempSettings.emulatedControllerType.intValue!=LI_CTYPE_PS - ||self->tempSettings.gyroMode.intValue==GyroModeOff; + return true; } - (void)switchMotionControlOnOffByControllerButton{ @@ -1190,7 +1328,7 @@ double rc_expo(double x, double expo) { if(self->_gyroEnabledFlag) [self->motionHandler startMotionControlByControllerButton]; else [self->motionHandler stopMotionUpdateWithInterruptNoneGyroInput:false]; } - else self->_gyroEnabledFlag = self->_gyroEnabledFlag || !self->_controllerGyroSwitchEnabled; + self->_gyroEnabledFlag = self->_gyroEnabledFlag || !self->_controllerGyroSwitchEnabled; } - (void)sendNavigationButtonPress { @@ -1330,6 +1468,7 @@ double rc_expo(double x, double expo) { */ if([self useMotionHandler] + && self->motionHandler.motionControlStarted && self->oscProfile.mapGyroTo==mapGyroToControllerStick && self->oscProfile.yawPitchToRightStick && self->_gyroEnabledFlag @@ -1339,6 +1478,7 @@ double rc_expo(double x, double expo) { else [self updateRightStick: voidController.playerIndex==0?self->_oscController:voidController x:rightStickX y:rightStickY]; if([self useMotionHandler] + && self->motionHandler.motionControlStarted && self->oscProfile.mapGyroTo==mapGyroToControllerStick && self->oscProfile.rollToLeftStick && self->_gyroEnabledFlag @@ -1611,7 +1751,7 @@ double rc_expo(double x, double expo) { voidController.hasGyroscope = NO; - if(voidController.gamepad.motion.hasAttitudeAndRotationRate){ + if([ControllerUtil hasControllerAccelerometer:voidController.gamepad]){ [voidController.motionTypes addObject:@(LI_MOTION_TYPE_ACCEL)]; voidController.hasAccelerometer = YES; voidController.reportRateHz = 120; @@ -1848,7 +1988,7 @@ double rc_expo(double x, double expo) { NSLog(@"controller obj in dict: %@", controller); } - if([self gamepadGyroEnabledInSetting]) [self updateFinished:_oscController]; + if([self psGyroEnabledInSetting]) [self updateFinished:_oscController]; } -(id)initWithConfig:(StreamConfiguration*)streamConfig delegate:(id)delegate @@ -2038,8 +2178,12 @@ double rc_expo(double x, double expo) { return false; } --(bool)gamepadGyroEnabledInSetting { - return (tempSettings.emulatedControllerType.intValue == LI_CTYPE_PS && tempSettings.gyroMode.intValue != GyroModeOff); +-(bool)psGyroEnabledInSetting { + uint8_t emulation = tempSettings.emulatedControllerType.intValue; + return (emulation == ControllerEmulationPs + || emulation == ControllerEmulationPsEnhancedHaptic + || emulation == ControllerEmulationXboxAndPs) + && tempSettings.gyroMode.intValue != GyroModeOff; } -(void)connectionEstablished { @@ -2049,7 +2193,7 @@ double rc_expo(double x, double expo) { //if (_oscEnabled // || (tempSettings.emulatedControllerType.intValue == LI_CTYPE_PS && tempSettings.gyroMode != GyroModeOff)) { - if ([self gamepadGyroEnabledInSetting]) { + if ([self psGyroEnabledInSetting]) { [self setButtonFlag:self->_oscController flags:A_FLAG]; [self updateFinished:self->_oscController]; [self clearButtonFlag:self->_oscController flags:A_FLAG]; @@ -2068,14 +2212,14 @@ double rc_expo(double x, double expo) { [self resetGyroInputForController:voidController]; if (@available(iOS 14.0, *)) { //NSLog(@"stop controller obj: %@, hasAcc %d, hasGyro %d", voidController, voidController.hasAccelerometer, voidController.hasGyroscope); - if(voidController.hasAccelerometer){ + // if(voidController.hasAccelerometer){ [voidController.accelTimer invalidate]; voidController.accelTimer = nil; - } - if(voidController.hasGyroscope){ + // } + // if(voidController.hasGyroscope){ [voidController.gyroTimer invalidate]; voidController.gyroTimer = nil; - } + // } } } @@ -2083,11 +2227,12 @@ double rc_expo(double x, double expo) { // Stop all timers to ensure a clean slate before applying the new setting. [self stopTimerForAllControllers]; - _gyroEnabledFlag = _gyroMode != GyroModeOff && tempSettings.emulatedControllerType.intValue == LI_CTYPE_PS; + _gyroEnabledFlag = [self psGyroEnabledInSetting]; switch(_gyroMode) { case AlwaysController: // Activate timers only for physical controllers. for (VoidController* voidController in _voidControllers.allValues) { + if(voidController == _oscController) break; [self updateTimerStateForController:voidController]; } break; @@ -2170,7 +2315,8 @@ double rc_expo(double x, double expo) { } } --(void) dealloc + +- (void) dealloc { if (VLSharedControllerSupport == self) { VLSharedControllerSupport = nil; diff --git a/VoidLink/Input/ControllerUtil.swift b/VoidLink/Input/ControllerUtil.swift index 58ec0f77..d0dabe62 100644 --- a/VoidLink/Input/ControllerUtil.swift +++ b/VoidLink/Input/ControllerUtil.swift @@ -260,6 +260,13 @@ import UIKit } } +@objc enum ControllerEmulation: UInt8 { + case xbox = 0x01 // LI_CTYPE_XBOX + case ps = 0x02 // LI_CTYPE_PS + case psEnhancedHaptic = 0xFE + case xboxAndPs = 0x00 // LI_CTYPE_UNKNOWN +} + @objc protocol ControllerUtilDelegate: AnyObject { func isStreaming() -> Bool func isInAppView() -> Bool @@ -269,6 +276,38 @@ import UIKit @objc static weak var delegate: ControllerUtilDelegate? #if !VOIDLINK_PREVIEW + @objc static var hasDualSenseController: Bool { + if #available(iOS 14.5, *) { + for gcController in GCController.controllers() { + if gcController.extendedGamepad is GCDualSenseGamepad {return true} + } + return false + } + else {return false} + } + + @objc static func hasControllerAccelerometer(_ controller: GCController) -> Bool { + guard let motion = controller.motion else { return false } + + if #available(iOS 14.0, tvOS 14.0, *) { + if motion.hasGravityAndUserAcceleration { + return true + } + + if controller.extendedGamepad is GCDualShockGamepad { + return true + } + + if #available(iOS 14.5, tvOS 14.5, *) { + if controller.extendedGamepad is GCDualSenseGamepad { + return true + } + } + } + + return false + } + private struct DualSensePCMChunk { let controllerNumber: UInt16 let flags: UInt8 @@ -310,17 +349,57 @@ import UIKit } } + private final class AuthoredIrRenderState { + var renderHostAnchorUs: UInt64? + var renderLocalAnchor = 0.0 + var diagnosticFrameCounter: UInt32 = 0 + + func renderDelay(for timestampUs: UInt64, resetTimeline: Bool) -> TimeInterval { + let now = ProcessInfo.processInfo.systemUptime + if resetTimeline || renderHostAnchorUs == nil { + renderHostAnchorUs = timestampUs + renderLocalAnchor = now + 0.002 + } + let anchor = renderHostAnchorUs ?? timestampUs + let hostDelta = timestampUs >= anchor ? Double(timestampUs - anchor) / 1_000_000.0 : 0 + return min(max(renderLocalAnchor + hostDelta - now, 0), 0.025) + } + } + private static let dualSenseHapticsLock = NSLock() private static let dualSenseHapticsWorker = DispatchQueue( label: "com.voidlink.dualsense-authored-haptics", qos: .userInteractive ) private static let maxQueuedDualSensePCMChunks = 8 - private static let defaultDualSenseHapticsEQ: [Float] = [0.78, 0.98, 1.59, 0.175, 1.2] + private static let defaultDualSenseControllerHapticsEQ: [Float] = [0.78, 0.98, 1.59, 0.175, 1.2] + private static let defaultDeviceDualSenseHapticsEQ: [Float] = [18, 0.98, 1.59, 0.9, 1.2] + private static let dualSenseControllerAmplitudeScale: Float = 1.15 + private static let dualSenseControllerTransientScale: Float = 1.0 + private static let dualSenseControllerSharpnessScale: Float = 1.15 + private static let dualSenseControllerMirrorMixedHandles = false private static var dualSensePCMQueue = Deque() private static var dualSenseWorkerScheduled = false private static var dualSenseForcedDiscontinuities = Set() private static var dualSenseAuthoredEngines = [UInt16: AuthoredEngineState]() + private static var dualSenseIrRenderStates = [UInt16: AuthoredIrRenderState]() + private static let authoredHapticsRouteLock = NSLock() + private static var authoredHapticsPreference = AuthoredHapticsPreference.auto + private static var authoredHapticsLastRoutedToDualSense = false + private static var authoredHapticsLastRoute = AuthoredHapticsRoute.off + + private enum AuthoredHapticsPreference: Int { + case auto = 0 + case device = 1 + case swapped = 2 + case off = 3 + } + + private enum AuthoredHapticsRoute { + case dualSense + case device + case off + } #if os(iOS) private static var dualSenseHapticsEQOverlayWindow: UIWindow? @@ -329,11 +408,70 @@ import UIKit #endif fileprivate static func dualSenseHapticsEQSnapshot() -> [Float] { - defaultDualSenseHapticsEQ + defaultDeviceDualSenseHapticsEQ } fileprivate static func defaultDualSenseHapticsEQSnapshot() -> [Float] { - defaultDualSenseHapticsEQ + defaultDeviceDualSenseHapticsEQ + } + + private static func dualSenseHapticsEQSnapshot(for route: AuthoredHapticsRoute) -> [Float] { + switch route { + case .dualSense: + return defaultDualSenseControllerHapticsEQ + case .device, .off: + return defaultDeviceDualSenseHapticsEQ + } + } + + private static func compensatedDualSenseControllerValues( + _ values: (amplitude: Float, sharpness: Float, transient: Float), + highBandRatio: Float + ) -> (amplitude: Float, sharpness: Float, transient: Float) { + let lowFrequencyTrim = 0.82 + 0.18 * min(max(highBandRatio, 0), 1) + let amplitude = min(max( + pow(values.amplitude, 0.50) * dualSenseControllerAmplitudeScale * lowFrequencyTrim, + 0 + ), 1) + let sharpness = min(max( + pow(values.sharpness, 0.8) * dualSenseControllerSharpnessScale, + 0 + ), 1) + let transient = min(max( + pow(values.transient, 0.9) * dualSenseControllerTransientScale, + 0 + ), 1) + return (amplitude, sharpness, transient) + } + + private static func authoredHighBandRatio(for lane: AhAuthoredLaneFrame?) -> Float { + 1 - min(max(lane?.low_band_ratio ?? 1, 0), 1) + } + + private static func mixedDualSenseControllerHandleValues( + left: (amplitude: Float, sharpness: Float, transient: Float), + right: (amplitude: Float, sharpness: Float, transient: Float) + ) -> ( + left: (amplitude: Float, sharpness: Float, transient: Float), + right: (amplitude: Float, sharpness: Float, transient: Float) + ) { + guard dualSenseControllerMirrorMixedHandles else { + return (left, right) + } + + let amplitude = min(max(sqrt( + (left.amplitude * left.amplitude + right.amplitude * right.amplitude) * 0.5 + ), 0), 1) + let amplitudeSum = left.amplitude + right.amplitude + let sharpness = amplitudeSum > 0.000_001 + ? min(max( + (left.sharpness * left.amplitude + right.sharpness * right.amplitude) / amplitudeSum, + 0 + ), 1) + : min(max(max(left.sharpness, right.sharpness), 0), 1) + let transient = min(max(max(left.transient, right.transient), 0), 1) + let mixed = (amplitude: amplitude, sharpness: sharpness, transient: transient) + return (mixed, mixed) } fileprivate static func previewDualSenseHapticsEQ(_ values: [Float]) { @@ -342,6 +480,86 @@ import UIKit fileprivate static func saveDualSenseHapticsEQ(_ values: [Float]) { } + private static func refreshAuthoredHapticsPreference() -> AuthoredHapticsPreference { + let rawValue = Int(DataManager().getSettings().hapticEngine.intValue) + let preference = AuthoredHapticsPreference(rawValue: rawValue) ?? .auto + authoredHapticsRouteLock.lock() + authoredHapticsPreference = preference + authoredHapticsRouteLock.unlock() + return preference + } + + private static func selectAuthoredHapticsRoute( + controllerNumber: UInt16, + controllerSupport: ControllerSupport + ) -> AuthoredHapticsRoute { + switch refreshAuthoredHapticsPreference() { + case .off: + return .off + case .device: + return .device + case .auto, .swapped: + let hasDualSense = controllerSupport.hasDualSenseController(controllerNumber) + authoredHapticsRouteLock.lock() + authoredHapticsLastRoutedToDualSense = hasDualSense + authoredHapticsRouteLock.unlock() + return hasDualSense ? .dualSense : .device + } + } + + private static func stopAuthoredHapticsRoute( + _ route: AuthoredHapticsRoute, + controllerNumber: UInt16, + controllerSupport: ControllerSupport + ) { + switch route { + case .dualSense: + controllerSupport.renderDualSenseHaptics( + controllerNumber, + leftAmplitude: 0, + leftSharpness: 0, + leftTransient: 0, + rightAmplitude: 0, + rightSharpness: 0, + rightTransient: 0, + delaySeconds: 0 + ) + case .device: + controllerSupport.renderDeviceDualSenseHaptics( + controllerNumber, + leftAmplitude: 0, + leftSharpness: 0, + leftTransient: 0, + rightAmplitude: 0, + rightSharpness: 0, + rightTransient: 0, + delaySeconds: 0 + ) + case .off: + break + } + } + + private static func updateAuthoredHapticsRouteTransition( + to route: AuthoredHapticsRoute, + controllerNumber: UInt16, + controllerSupport: ControllerSupport + ) { + authoredHapticsRouteLock.lock() + let previousRoute = authoredHapticsLastRoute + authoredHapticsLastRoute = route + authoredHapticsLastRoutedToDualSense = route == .dualSense + authoredHapticsRouteLock.unlock() + + if previousRoute != route { + stopAuthoredHapticsRoute( + previousRoute, + controllerNumber: controllerNumber, + controllerSupport: controllerSupport + ) + } + } + #if os(iOS) @available(iOS 14.0, *) @objc static func presentDualSenseHapticsEQIfNeeded() { @@ -425,6 +643,51 @@ import UIKit } } + @objc(enqueueDualSenseHapticsIRV2WithControllerNumber:flags:sourceSequenceNumber:timestampUs:sourceFrameCount:leftRms:leftPeak:leftTransient:leftLowRatio:leftZeroCrossHz:rightRms:rightPeak:rightTransient:rightLowRatio:rightZeroCrossHz:laneCorrelation:) + static func enqueueDualSenseHapticsIRV2( + controllerNumber: UInt16, + flags: UInt8, + sourceSequenceNumber: UInt32, + timestampUs: UInt64, + sourceFrameCount: UInt32, + leftRms: Float, + leftPeak: Float, + leftTransient: Float, + leftLowRatio: Float, + leftZeroCrossHz: Float, + rightRms: Float, + rightPeak: Float, + rightTransient: Float, + rightLowRatio: Float, + rightZeroCrossHz: Float, + laneCorrelation: Float + ) { + dualSenseHapticsWorker.async { + processDualSenseIRV2Frame( + controllerNumber: controllerNumber, + flags: flags, + sourceSequenceNumber: sourceSequenceNumber, + timestampUs: timestampUs, + sourceFrameCount: sourceFrameCount, + left: makeAuthoredLaneFrame( + rmsAmplitude: leftRms, + peakAmplitude: leftPeak, + transientStrength: leftTransient, + lowBandRatio: leftLowRatio, + zeroCrossingRateHz: leftZeroCrossHz + ), + right: makeAuthoredLaneFrame( + rmsAmplitude: rightRms, + peakAmplitude: rightPeak, + transientStrength: rightTransient, + lowBandRatio: rightLowRatio, + zeroCrossingRateHz: rightZeroCrossHz + ), + laneCorrelation: laneCorrelation + ) + } + } + @objc static func stopAllDualSenseHaptics() { ControllerSupport.sharedInstance()?.cancelScheduledDualSenseHaptics() @@ -434,14 +697,79 @@ import UIKit dualSenseHapticsLock.unlock() dualSenseHapticsWorker.async { - let controllerNumbers = Array(dualSenseAuthoredEngines.keys) + let controllerNumbers = Array( + Set(dualSenseAuthoredEngines.keys).union(dualSenseIrRenderStates.keys) + ) dualSenseAuthoredEngines.removeAll(keepingCapacity: false) + dualSenseIrRenderStates.removeAll(keepingCapacity: false) for controllerNumber in controllerNumbers { renderDualSenseHaptics(controllerNumber: controllerNumber, left: nil, right: nil, delay: 0) } } } + private static func makeAuthoredLaneFrame( + rmsAmplitude: Float, + peakAmplitude: Float, + transientStrength: Float, + lowBandRatio: Float, + zeroCrossingRateHz: Float + ) -> AhAuthoredLaneFrame { + var lane = AhAuthoredLaneFrame() + lane.rms_amplitude = rmsAmplitude + lane.peak_amplitude = peakAmplitude + lane.transient_strength = transientStrength + lane.low_band_ratio = lowBandRatio + lane.zero_crossing_rate_hz = zeroCrossingRateHz + return lane + } + + private static func processDualSenseIRV2Frame( + controllerNumber: UInt16, + flags: UInt8, + sourceSequenceNumber: UInt32, + timestampUs: UInt64, + sourceFrameCount: UInt32, + left: AhAuthoredLaneFrame, + right: AhAuthoredLaneFrame, + laneCorrelation: Float + ) { + let isDiscontinuity = (flags & 0x01) != 0 + let isStreamEnd = (flags & 0x04) != 0 + let isSilent = (flags & 0x08) != 0 + let state = dualSenseIrRenderStates[controllerNumber] ?? AuthoredIrRenderState() + dualSenseIrRenderStates[controllerNumber] = state + let delay = state.renderDelay( + for: timestampUs, + resetTimeline: isDiscontinuity || state.diagnosticFrameCounter == 0 + ) + state.diagnosticFrameCounter &+= 1 + + renderDualSenseHaptics( + controllerNumber: controllerNumber, + left: isSilent ? nil : left, + right: isSilent ? nil : right, + laneCorrelation: laneCorrelation, + logDiagnostics: state.diagnosticFrameCounter % 100 == 0, + appliesEQ: false, + delay: delay + ) + + if isStreamEnd { + renderDualSenseHaptics( + controllerNumber: controllerNumber, + left: nil, + right: nil, + appliesEQ: false, + delay: delay + 0.005 + ) + dualSenseIrRenderStates.removeValue(forKey: controllerNumber) + } + + _ = sourceSequenceNumber + _ = sourceFrameCount + } + private static func drainDualSensePCMQueue() { while true { dualSenseHapticsLock.lock() @@ -544,6 +872,7 @@ import UIKit right: silent ? nil : frame.lanes.1, laneCorrelation: frame.lane_correlation, logDiagnostics: state.diagnosticFrameCounter % 100 == 0, + appliesEQ: false, delay: delay ) } @@ -560,26 +889,52 @@ import UIKit } } + @objc static var dualSenseHapticTransient: Float = 1.0 private static func renderDualSenseHaptics( controllerNumber: UInt16, left: AhAuthoredLaneFrame?, right: AhAuthoredLaneFrame?, laneCorrelation: Float = 0, logDiagnostics: Bool = false, + appliesEQ: Bool = true, delay: TimeInterval ) { guard let controllerSupport = ControllerSupport.sharedInstance() else { return } - let eq = dualSenseHapticsEQSnapshot() + let route = selectAuthoredHapticsRoute( + controllerNumber: controllerNumber, + controllerSupport: controllerSupport + ) + updateAuthoredHapticsRouteTransition( + to: route, + controllerNumber: controllerNumber, + controllerSupport: controllerSupport + ) + + guard route != .off else { + return + } + + let eq = appliesEQ ? dualSenseHapticsEQSnapshot(for: route) : [1, 1, 1, 1, 1] as [Float] let masterGain = eq[0] let lowBandGain = eq[1] let highBandGain = eq[2] let transientGain = eq[3] let sharpnessGain = eq[4] - func renderValues(for lane: AhAuthoredLaneFrame?) -> (amplitude: Float, sharpness: Float, transient: Float) { + func rawIrValues(for lane: AhAuthoredLaneFrame?) -> (amplitude: Float, sharpness: Float, transient: Float) { + guard let lane else { + return (0, 0, 0) + } + let amplitude = min(max(lane.rms_amplitude, 0), 1) + let sharpness = min(max(1 - lane.low_band_ratio, 0), 1) + let transient = min(max(lane.transient_strength, 0), 1) + return (amplitude, sharpness, transient) + } + + func renderValues1(for lane: AhAuthoredLaneFrame?) -> (amplitude: Float, sharpness: Float, transient: Float) { let lowBandRatio = min(max(lane?.low_band_ratio ?? 1, 0), 1) let highBandRatio = 1 - lowBandRatio let effectiveLowBandGain = pow(lowBandGain, 1.6) @@ -604,10 +959,12 @@ import UIKit : immediateSharpness let sourceTransient = min(max(lane?.transient_strength ?? 0, 0), 1) let rmsAmplitude = min(max(lane?.rms_amplitude ?? 0, 0), 1) - let perceptualBandTrim = 0.55 + 0.55 * equalizedHighBandRatio + let transientBandTrim = 0.55 + 0.35 * lowBandRatio + 0.10 * equalizedHighBandRatio + let lowContinuousTrim = 0.78 + 0.22 * lowBandRatio + let highContinuousTrim = 0.62 - 0.30 * equalizedHighBandRatio let renderedTransient = min(max( pow(sourceTransient, 0.45) * sqrt(masterGain) * transientGain * - bandGain * perceptualBandTrim * 1.35, + bandGain * transientBandTrim * 1.35, 0 ), 1) let continuousShare = 1 - 0.85 * min( @@ -617,11 +974,11 @@ import UIKit let lowBody = pow(rmsAmplitude, 1.85) * (1 - highBandRatio) * effectiveLowBandGain * 0.55 let highTexture = pow(rmsAmplitude, 0.55) * - pow(highBandRatio, 1.15) * effectiveHighBandGain * 0.22 + pow(highBandRatio, 1.35) * effectiveHighBandGain * 0.09 let midBodyTrim = 1 - 0.55 * min(max((representativeFrequency - 90) / 140, 0), 1) * (1 - highBandRatio) - let continuousAmplitude = (lowBody + highTexture) * masterGain * - perceptualBandTrim * midBodyTrim * continuousShare + let continuousAmplitude = (lowBody * lowContinuousTrim + highTexture * highContinuousTrim) * + masterGain * midBodyTrim * continuousShare return ( min(max(continuousAmplitude, 0), 1), min(max(synthesizedSharpness * sharpnessGain, 0), 1), @@ -629,36 +986,83 @@ import UIKit ) } - var leftValues = renderValues(for: left) - var rightValues = renderValues(for: right) + func renderValues2(for lane: AhAuthoredLaneFrame?) -> (amplitude: Float, sharpness: Float, transient: Float) { + guard let lane else { + return (0, 0, 0) + } - let positiveCorrelation = min(max(laneCorrelation, 0), 1) - let centerBlend = min(max((positiveCorrelation - 0.1) / 0.9, 0), 1) * 0.85 - if centerBlend > 0 { - let commonAmplitude = sqrt( - (leftValues.amplitude * leftValues.amplitude + - rightValues.amplitude * rightValues.amplitude) * 0.5 + let useBuiltinHaptic = route == .device + + let rmsAmplitude = min(max(lane.rms_amplitude, 0), 1) + + let lowBandRatio = min(max(lane.low_band_ratio, 0), 1) + let highBandRatio = 1 - lowBandRatio + let amplitudeLowBandGain: Float = 1.0 + let amplitudeHighBandGain: Float = 1.0 + let bandGain = lowBandRatio * amplitudeLowBandGain + highBandRatio * amplitudeHighBandGain + let sourceSharpness = min(max(highBandRatio, 0), 1) + let sharpnessLowGain: Float = 0.62 + let sharpnessMidGain: Float = 0.82 + let sharpnessHighGain: Float = 1.2 + let sharpnessLowWeight = min(max((0.50 - sourceSharpness) / 0.50, 0), 1) + let sharpnessHighWeight = min(max((sourceSharpness - 0.50) / 0.50, 0), 1) + let sharpnessMidWeight = 1 - max(sharpnessLowWeight, sharpnessHighWeight) + let sharpnessGain = sharpnessLowWeight * sharpnessLowGain + + sharpnessMidWeight * sharpnessMidGain + + sharpnessHighWeight * sharpnessHighGain + let sharpness = min(max(sourceSharpness * sharpnessGain, 0), 1) + + let transient = min(max(lane.transient_strength, 0), 1) + let amplitudeCeiling: Float = 1.0 + let transientGain: Float = useBuiltinHaptic ? 1.03 : 0.343 + let transientCeiling: Float = useBuiltinHaptic ? 1.03 : 0.6 + let transientBottom: Float = useBuiltinHaptic ? 0.14 : 0.0 + + return ( + min(rmsAmplitude * bandGain, amplitudeCeiling), + sharpness, + min(max(transientBottom, transient * transientGain * 1.6666666667 * dualSenseHapticTransient), transientCeiling) ) - let amplitudeSum = leftValues.amplitude + rightValues.amplitude - let commonSharpness = amplitudeSum > 0.000_001 - ? (leftValues.sharpness * leftValues.amplitude + - rightValues.sharpness * rightValues.amplitude) / amplitudeSum - : max(leftValues.sharpness, rightValues.sharpness) - let commonTransient = max(leftValues.transient, rightValues.transient) - - leftValues.amplitude += (commonAmplitude - leftValues.amplitude) * centerBlend - rightValues.amplitude += (commonAmplitude - rightValues.amplitude) * centerBlend - leftValues.sharpness += (commonSharpness - leftValues.sharpness) * centerBlend - rightValues.sharpness += (commonSharpness - rightValues.sharpness) * centerBlend - leftValues.transient += (commonTransient - leftValues.transient) * centerBlend - rightValues.transient += (commonTransient - rightValues.transient) * centerBlend } + var leftValues = appliesEQ ? renderValues1(for: left) : renderValues2(for: left) + var rightValues = appliesEQ ? renderValues1(for: right) : renderValues2(for: right) + + if appliesEQ { + let positiveCorrelation = min(max(laneCorrelation, 0), 1) + let commonHighBandRatio = ( + authoredHighBandRatio(for: left) + authoredHighBandRatio(for: right) + ) * 0.5 + let dualSenseCenterBlendScale = 0.10 + 0.55 * commonHighBandRatio + let centerBlendScale: Float = route == .dualSense ? dualSenseCenterBlendScale : 0.85 + let centerBlend = min(max((positiveCorrelation - 0.1) / 0.9, 0), 1) * centerBlendScale + if centerBlend > 0 { + let commonAmplitude = sqrt( + (leftValues.amplitude * leftValues.amplitude + + rightValues.amplitude * rightValues.amplitude) * 0.5 + ) + let amplitudeSum = leftValues.amplitude + rightValues.amplitude + let commonSharpness = amplitudeSum > 0.000_001 + ? (leftValues.sharpness * leftValues.amplitude + + rightValues.sharpness * rightValues.amplitude) / amplitudeSum + : max(leftValues.sharpness, rightValues.sharpness) + let commonTransient = max(leftValues.transient, rightValues.transient) + + leftValues.amplitude += (commonAmplitude - leftValues.amplitude) * centerBlend + rightValues.amplitude += (commonAmplitude - rightValues.amplitude) * centerBlend + leftValues.sharpness += (commonSharpness - leftValues.sharpness) * centerBlend + rightValues.sharpness += (commonSharpness - rightValues.sharpness) * centerBlend + leftValues.transient += (commonTransient - leftValues.transient) * centerBlend + rightValues.transient += (commonTransient - rightValues.transient) * centerBlend + } + } + /* if logDiagnostics { NSLog( - "DS5 authored: c=%u srcL[rms=%.3f high=%.3f hz=%.0f] srcR[rms=%.3f high=%.3f hz=%.0f] corr=%.2f outL[a=%.3f s=%.3f t=%.3f] outR[a=%.3f s=%.3f t=%.3f]", + "DS5 authored: c=%u route=%@ srcL[rms=%.3f high=%.3f hz=%.0f] srcR[rms=%.3f high=%.3f hz=%.0f] corr=%.2f outL[a=%.3f s=%.3f t=%.3f] outR[a=%.3f s=%.3f t=%.3f]", controllerNumber, + route == .dualSense ? "dualsense" : "device", Double(left?.rms_amplitude ?? 0), Double(1 - min(max(left?.low_band_ratio ?? 1, 0), 1)), Double(min(max((left?.zero_crossing_rate_hz ?? 0) * 0.5, 0), 400)), @@ -674,18 +1078,47 @@ import UIKit Double(rightValues.transient) ) } - */ + */ - controllerSupport.renderDualSenseHaptics( - controllerNumber, - leftAmplitude: leftValues.amplitude, - leftSharpness: leftValues.sharpness, - leftTransient: leftValues.transient, - rightAmplitude: rightValues.amplitude, - rightSharpness: rightValues.sharpness, - rightTransient: rightValues.transient, - delaySeconds: delay - ) + switch route { + case .dualSense: + if appliesEQ { + leftValues = compensatedDualSenseControllerValues( + leftValues, + highBandRatio: authoredHighBandRatio(for: left) + ) + rightValues = compensatedDualSenseControllerValues( + rightValues, + highBandRatio: authoredHighBandRatio(for: right) + ) + let mixedValues = mixedDualSenseControllerHandleValues(left: leftValues, right: rightValues) + leftValues = mixedValues.left + rightValues = mixedValues.right + } + controllerSupport.renderDualSenseHaptics( + controllerNumber, + leftAmplitude: leftValues.amplitude, + leftSharpness: leftValues.sharpness, + leftTransient: leftValues.transient, + rightAmplitude: rightValues.amplitude, + rightSharpness: rightValues.sharpness, + rightTransient: rightValues.transient, + delaySeconds: delay + ) + case .device: + controllerSupport.renderDeviceDualSenseHaptics( + controllerNumber, + leftAmplitude: leftValues.amplitude, + leftSharpness: leftValues.sharpness, + leftTransient: leftValues.transient, + rightAmplitude: rightValues.amplitude, + rightSharpness: rightValues.sharpness, + rightTransient: rightValues.transient, + delaySeconds: delay + ) + case .off: + break + } } #endif @@ -711,6 +1144,7 @@ import UIKit let elementDict = tempMap as NSDictionary // 单一 gamepad.valueChangedHandler + gamepad.valueChangedHandler = { gamepad, element in handler(elementDict, gamepad, element) if #available(iOS 13.0, *) { diff --git a/VoidLink/Input/CustomOSC/OSCProfile.h b/VoidLink/Input/CustomOSC/OSCProfile.h index 95f147c4..2a48124c 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.h +++ b/VoidLink/Input/CustomOSC/OSCProfile.h @@ -65,6 +65,7 @@ typedef NS_ENUM(NSInteger, PencilAndHoverMode) { @property (nonatomic, assign) CGFloat touchPointerVelocityFactor; @property (nonatomic, assign) CGPoint normalizedStreamViewOffset; @property (nonatomic, assign) CGFloat streamViewScale; +@property (nonatomic, assign) CGFloat dualSenseTransient; @property (nonatomic, assign) bool gamepadOverlayEnabled; diff --git a/VoidLink/Input/CustomOSC/OSCProfile.m b/VoidLink/Input/CustomOSC/OSCProfile.m index 91269a91..ba3f7dc5 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.m +++ b/VoidLink/Input/CustomOSC/OSCProfile.m @@ -58,6 +58,7 @@ [encoder encodeFloat:self.touchPointerVelocityFactor forKey:@"touchPointerVelocityFactor"]; [encoder encodeCGPoint:self.normalizedStreamViewOffset forKey:@"normalizedStreamViewOffset"]; [encoder encodeFloat:self.streamViewScale forKey:@"streamViewScale"]; + [encoder encodeFloat:self.dualSenseTransient forKey:@"dualSenseTransient"]; [encoder encodeBool:self.gamepadOverlayEnabled forKey:@"gamepadOverlayEnabled"]; [encoder encodeObject:self.pressureCurvePoints forKey:@"pressureCurvePoints"]; @@ -132,7 +133,8 @@ self.normalizedStreamViewOffset = [decoder containsValueForKey:@"normalizedStreamViewOffset"] ? [decoder decodeCGPointForKey:@"normalizedStreamViewOffset"] : CGPointZero; self.streamViewScale = [decoder containsValueForKey:@"streamViewScale"] ? [decoder decodeFloatForKey:@"streamViewScale"] : 1.0; - + self.dualSenseTransient = [decoder containsValueForKey:@"dualSenseTransient"] ? [decoder decodeFloatForKey:@"dualSenseTransient"] : 1.0; + self.gamepadOverlayEnabled = [decoder containsValueForKey:@"gamepadOverlayEnabled"] ? [decoder decodeBoolForKey:@"gamepadOverlayEnabled"] : false; self.pressureCurvePoints = diff --git a/VoidLink/Input/HapticContext.m b/VoidLink/Input/HapticContext.m index 2688c7cd..5717169e 100644 --- a/VoidLink/Input/HapticContext.m +++ b/VoidLink/Input/HapticContext.m @@ -18,14 +18,20 @@ @implementation HapticContext { GCControllerPlayerIndex _playerIndex; CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0), tvos(14.0)); - id _hapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0)); - BOOL _playing; + id _motorHapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0)); + id _authoredHapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0)); + BOOL _motorPlaying; + BOOL _authoredPlaying; } -(void)cleanup API_AVAILABLE(ios(14.0), tvos(14.0)) { - if (_hapticPlayer != nil) { - [_hapticPlayer cancelAndReturnError:nil]; - _hapticPlayer = nil; + if (_motorHapticPlayer != nil) { + [_motorHapticPlayer cancelAndReturnError:nil]; + _motorHapticPlayer = nil; + } + if (_authoredHapticPlayer != nil) { + [_authoredHapticPlayer cancelAndReturnError:nil]; + _authoredHapticPlayer = nil; } if (_hapticEngine != nil) { [_hapticEngine stopWithCompletionHandler:nil]; @@ -43,15 +49,15 @@ // Stop the effect entirely if the amplitude is 0 if (amplitude == 0) { - if (_playing) { - [_hapticPlayer stopAtTime:0 error:&error]; - _playing = NO; + if (_motorPlaying) { + [_motorHapticPlayer stopAtTime:0 error:&error]; + _motorPlaying = NO; } return; } - if (_hapticPlayer == nil) { + if (_motorHapticPlayer == nil) { // We must initialize the intensity to 1.0f because the dynamic parameters are multiplied by this value before being applied CHHapticEventParameter* intensityParameter = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f]; CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObject:intensityParameter] relativeTime:0 duration:GCHapticDurationInfinite]; @@ -61,7 +67,7 @@ return; } - _hapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error]; + _motorHapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error]; if (error != nil) { Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error); return; @@ -69,21 +75,21 @@ } CHHapticDynamicParameter* intensityParameter = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:amplitude / 65535.0f relativeTime:0]; - [_hapticPlayer sendParameters:[NSArray arrayWithObject:intensityParameter] atTime:CHHapticTimeImmediate error:&error]; + [_motorHapticPlayer sendParameters:[NSArray arrayWithObject:intensityParameter] atTime:CHHapticTimeImmediate error:&error]; if (error != nil) { Log(LOG_W, @"Controller %d: Haptic player parameter update failed: %@", _playerIndex, error); return; } - if (!_playing) { - [_hapticPlayer startAtTime:0 error:&error]; + if (!_motorPlaying) { + [_motorHapticPlayer startAtTime:0 error:&error]; if (error != nil) { - _hapticPlayer = nil; + _motorHapticPlayer = nil; Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error); return; } - _playing = YES; + _motorPlaying = YES; } } @@ -100,13 +106,13 @@ NSError* error = nil; if (amplitude == 0.0f) { - if (_playing) { - [_hapticPlayer stopAtTime:CHHapticTimeImmediate error:&error]; - _playing = NO; + if (_authoredPlaying) { + [_authoredHapticPlayer stopAtTime:CHHapticTimeImmediate error:&error]; + _authoredPlaying = NO; } } else { - if (_hapticPlayer == nil) { + if (_authoredHapticPlayer == nil) { CHHapticEventParameter* intensity = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f]; CHHapticEventParameter* eventSharpness = [[CHHapticEventParameter alloc] @@ -122,8 +128,8 @@ Log(LOG_W, @"Controller %d: Authored haptic pattern creation failed: %@", _playerIndex, error); return; } - _hapticPlayer = [_hapticEngine createPlayerWithPattern:pattern error:&error]; - if (_hapticPlayer == nil || error != nil) { + _authoredHapticPlayer = [_hapticEngine createPlayerWithPattern:pattern error:&error]; + if (_authoredHapticPlayer == nil || error != nil) { Log(LOG_W, @"Controller %d: Authored haptic player creation failed: %@", _playerIndex, error); return; } @@ -137,19 +143,19 @@ initWithParameterID:CHHapticDynamicParameterIDHapticSharpnessControl value:sharpness relativeTime:0] ]; - [_hapticPlayer sendParameters:parameters atTime:CHHapticTimeImmediate error:&error]; + [_authoredHapticPlayer sendParameters:parameters atTime:CHHapticTimeImmediate error:&error]; if (error != nil) { Log(LOG_W, @"Controller %d: Authored haptic parameter update failed: %@", _playerIndex, error); return; } - if (!_playing) { - [_hapticPlayer startAtTime:CHHapticTimeImmediate error:&error]; + if (!_authoredPlaying) { + [_authoredHapticPlayer startAtTime:CHHapticTimeImmediate error:&error]; if (error != nil) { - _hapticPlayer = nil; + _authoredHapticPlayer = nil; Log(LOG_W, @"Controller %d: Authored haptic playback start failed: %@", _playerIndex, error); return; } - _playing = YES; + _authoredPlaying = YES; } } @@ -197,9 +203,11 @@ } Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason); - me->_hapticPlayer = nil; + me->_motorHapticPlayer = nil; + me->_authoredHapticPlayer = nil; me->_hapticEngine = nil; - me->_playing = NO; + me->_motorPlaying = NO; + me->_authoredPlaying = NO; }; _hapticEngine.resetHandler = ^{ HapticContext* me = weakSelf; @@ -208,8 +216,10 @@ } Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex); - me->_hapticPlayer = nil; - me->_playing = NO; + me->_motorHapticPlayer = nil; + me->_authoredHapticPlayer = nil; + me->_motorPlaying = NO; + me->_authoredPlaying = NO; [me->_hapticEngine startAndReturnError:nil]; }; @@ -258,9 +268,11 @@ } Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason); - me->_hapticPlayer = nil; + me->_motorHapticPlayer = nil; + me->_authoredHapticPlayer = nil; me->_hapticEngine = nil; - me->_playing = NO; + me->_motorPlaying = NO; + me->_authoredPlaying = NO; }; _hapticEngine.resetHandler = ^{ HapticContext* me = weakSelf; @@ -269,8 +281,10 @@ } Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex); - me->_hapticPlayer = nil; - me->_playing = NO; + me->_motorHapticPlayer = nil; + me->_authoredHapticPlayer = nil; + me->_motorPlaying = NO; + me->_authoredPlaying = NO; [me->_hapticEngine startAndReturnError:nil]; }; diff --git a/VoidLink/Input/MotionHandler.swift b/VoidLink/Input/MotionHandler.swift index 7339bb0d..b9f260b9 100644 --- a/VoidLink/Input/MotionHandler.swift +++ b/VoidLink/Input/MotionHandler.swift @@ -179,6 +179,9 @@ import CoreMotion if useBuiltinGyro { if motionManager.isGyroAvailable { + if rollToLeftStick { + leftStickMotion = leftStickTouchInputX + } motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motionData, _ in guard let self = self, let data = motionData else { return } self.handleMotionData(deviceMotion: data) @@ -229,7 +232,7 @@ import CoreMotion } /// 停止更新 - private var interruptNoneGyroInput:Bool = false + // private var interruptNoneGyroInput:Bool = false @objc public func stopMotionUpdate(interruptNoneGyroInput:Bool=false) { motionControlStarted = false @@ -362,7 +365,7 @@ import CoreMotion } if !motionControlStarted { - self.clearGyroInput(interruptNonGyroInput: interruptNoneGyroInput) + self.clearGyroInput(interruptNonGyroInput: false) // print("Gyro: stopped") return } @@ -394,12 +397,12 @@ import CoreMotion if rollToLeftStick { roll = gyroInputToStickInput(input:rollSource*sensitvityRoll*widgetRollFactor*0.2) - leftStickMotion = useBuiltinGyro ? -stickMaxOffset*(gravityYOffset/Double.pi)*3*sensitvityRoll*widgetRollFactor : leftStickMotion + roll + leftStickMotion = self.clampStickInput(input: useBuiltinGyro ? -stickMaxOffset*(gravityYOffset/Double.pi)*3*sensitvityRoll*widgetRollFactor : leftStickMotion + roll) let mixedLeftStickOffsetX = self.clampStickInput(input: leftStickMotion+leftStickTouchInputX+leftStickPhysicalInputX) let mixedLeftStickOffsetY = self.clampStickInput(input: leftStickTouchInputY+leftStickPhysicalInputY) - let offsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: mixedLeftStickOffsetX, dy: mixedLeftStickOffsetY), minOffset: gyroToStickMinOffset) + let offsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: mixedLeftStickOffsetX, dy: mixedLeftStickOffsetY), minOffset: gyroToStickMinOffset, circulate: true) onScreenControls?.sendLeftStickTouchPadEvent(offsetVector.dx, offsetVector.dy) } @@ -410,11 +413,19 @@ import CoreMotion guard ControllerUtil.gamepadArrivalReported else { return } if yawPitchToRightStick{ - onScreenControls?.sendRightStickTouchPadEvent(rightStickPhysicalInputX+rightStickTouchInputX-yawBias, rightStickPhysicalInputY+rightStickTouchInputY-pitchBias) + let clearX = rightStickPhysicalInputX+rightStickTouchInputX-yawBias + let clearY = rightStickPhysicalInputY+rightStickTouchInputY-pitchBias + let clearVector = ControllerUtil.circulated(offsetVector: CGVector(dx: clearX, dy: clearY)) + + onScreenControls?.sendRightStickTouchPadEvent(clearVector.dx, clearVector.dy) } if rollToLeftStick{ - leftStickMotion = 0 - onScreenControls?.sendLeftStickTouchPadEvent(leftStickPhysicalInputX+leftStickTouchInputX-rollBias,leftStickPhysicalInputY+leftStickTouchInputY) + leftStickMotion = leftStickTouchInputX + let clearX = leftStickPhysicalInputX+leftStickTouchInputX-rollBias + let clearY = leftStickPhysicalInputY+leftStickTouchInputY + let clearVector = ControllerUtil.circulated(offsetVector: CGVector(dx: clearX, dy: clearY)) + + onScreenControls?.sendLeftStickTouchPadEvent(clearVector.dx,clearVector.dy) } if(interruptNonGyroInput){ onScreenControls?.clearLeftStickTouchPadFlag() diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index 14e66b8c..f4668253 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -2000,12 +2000,13 @@ import ObjectiveC.runtime let mixRightStickInputToGyro = (oscProfile.mapGyroTo == .mapGyroToControllerStick && oscProfile.yawPitchToRightStick) + + stickOffsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: targetX, dy: targetY), minOffset: minStickOffset, circulate: circulate) + if !mixRightStickInputToGyro || (self.motionHandler?.gyroMixInputStarted() != true) { - - stickOffsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: targetX, dy: targetY), minOffset: minStickOffset, circulate: circulate) self.onScreenControls?.sendRightStickTouchPadEvent(stickOffsetVector.dx, stickOffsetVector.dy) } - self.motionHandler?.mixOnScreenRightStickAndGyroInput(x: targetX, y: targetY) + self.motionHandler?.mixOnScreenRightStickAndGyroInput(x: stickOffsetVector.dx, y: stickOffsetVector.dy) if !ControllerUtil.gamepadArrivalReported {ControllerUtil.gamepadArrivalReported = true} } @@ -2015,12 +2016,13 @@ import ObjectiveC.runtime let mixLeftStickInputToGyro = (oscProfile.mapGyroTo == .mapGyroToControllerStick && oscProfile.rollToLeftStick) + + stickOffsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: targetX, dy: targetY), minOffset: minStickOffset, circulate: circulate) + if !mixLeftStickInputToGyro || (self.motionHandler?.gyroMixInputStarted() != true) { - - stickOffsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: targetX, dy: targetY), minOffset: minStickOffset, circulate: circulate) self.onScreenControls?.sendLeftStickTouchPadEvent(stickOffsetVector.dx, stickOffsetVector.dy) } - self.motionHandler?.mixOnScreenLeftStickAndGyroInput(x: targetX, y: targetY) + self.motionHandler?.mixOnScreenLeftStickAndGyroInput(x: stickOffsetVector.dx, y: stickOffsetVector.dy) if !ControllerUtil.gamepadArrivalReported {ControllerUtil.gamepadArrivalReported = true} } diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index 0ec8350c..e0c34a84 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -714,6 +714,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; OnScreenWidgetView.buttonVisualFeedbackEnabled = self->settings.buttonVisualFeedback; OnScreenWidgetView.gamepadOverlayFLag = profile.gamepadOverlayEnabled; + ControllerUtil.dualSenseHapticTransient = profile.dualSenseTransient; bool hasLegacyWidget = false; if(reloadWidgets && !OnScreenWidgetView.editMode){ diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index 4a0ac3b0..374a8df9 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -8212,25 +8212,25 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Sunshine emulates game controller via VigemBus, which support DS4 & Xbox360. \n\n- Xbox360 does not support gyro.\n\n- When “both” is selected, the app will map built-in gyro as player0 & external game controller as player1" + "value" : "Sunshine emulates game controller via VigemBus, which support DS4 & Xbox360. \n- Xbox360 does not support gyro.\n- When “both” is selected, the app will map built-in gyro as player0 & external game controller as player1\n\nWhen DualSense sidecar is running on Sunshine-Foundation, VigemBus will be taken over, left DualSense to be the emulated Controller on your host. In this case:\n- The option “PS (Enhanced Haptic)” provides better haptic feedback for a real DualSense controller.\n- The option “PS” provides DualSense haptic feedback translation to traditional rumbling for all controllers.\n- Enhanced haptics are also available with iPhone motor by setting “Haptic Engine” to “Device”.\n" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Sunshine 通过 VigemBus 模拟游戏手柄,支持 DS4 和 Xbox 360。\n\n- Xbox 360 不支持陀螺仪。\n\n- 当选择“同时映射”时,app会将内置陀螺仪映射为玩家0,外接手柄映射为玩家1。" + "value" : "Sunshine 通过 VigemBus 模拟游戏手柄,支持 DS4 和 Xbox 360。\n- Xbox 360 不支持陀螺仪。\n- 当选择“同时映射”时,app会将内置陀螺仪映射为玩家0,外接手柄映射为玩家1。\n\n当 DualSense 组件在【Sunshine 基地版】上运行时,它会接管 ViGEmBus,并使 DualSense 成为主机端的模拟手柄。在这种情况下:\n- 选择“PS(触觉增强)”将为真实的DualSense 手柄提供更好的触觉反馈。\n- 选择“PS”:将 DualSense 触觉反馈转换为传统震动,适用于所有手柄。\n- 将 “振动引擎” 设置为 “内置”,也可以通过 iPhone 马达获得增强触觉反馈。\n\nDualSense的自适应板机、触觉反馈仅与Sunshine基地版配合使用时有效。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "Sunshine 透過 VigemBus 模擬遊戲控制器,支援 DS4 和 Xbox 360。\n\n- Xbox 360 不支援陀螺儀。\n\n- 當選擇「同時映射」時,app 會將內建陀螺儀對應為玩家0,外接控制器對應為玩家1。" + "value" : "Sunshine 透過 VigemBus 模擬遊戲控制器,支援 DS4 和 Xbox 360。\n- Xbox 360 不支援陀螺儀。\n- 當選擇「同時映射」時,app 會將內建陀螺儀對應為玩家0,外接控制器對應為玩家1。\n\n當 DualSense 元件在 「Sunshine 基地版」上執行時,它會接管 ViGEmBus,並使 DualSense 成為主機端的模擬手把。在這種情況下:\n- 選擇「PS(觸覺增強)」將為真實的 DualSense 手把提供更好的觸覺回饋。\n- 選擇「PS」:將 DualSense 觸覺回饋轉換為傳統震動,適用於所有手把。\n- 將「振動引擎」設定為「內建」,也可以透過 iPhone 馬達獲得增強觸覺回饋。\n\nDualSense 的自適應扳機、觸覺回饋僅在與 Sunshine 基地版配合使用時有效。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "Sunshine 透過 VigemBus 模擬遊戲控制器,支援 DS4 和 Xbox 360。\n\n- Xbox 360 不支援陀螺儀。\n\n- 當選擇「同時映射」時,app 會將內建陀螺儀對應為玩家0,外接控制器對應為玩家1。" + "value" : "Sunshine 透過 VigemBus 模擬遊戲控制器,支援 DS4 和 Xbox 360。\n- Xbox 360 不支援陀螺儀。\n- 當選擇「同時映射」時,app 會將內建陀螺儀對應為玩家0,外接控制器對應為玩家1。\n\n當 DualSense 元件在 「Sunshine 基地版」上執行時,它會接管 ViGEmBus,並使 DualSense 成為主機端的模擬手把。在這種情況下:\n- 選擇「PS(觸覺增強)」將為真實的 DualSense 手把提供更好的觸覺回饋。\n- 選擇「PS」:將 DualSense 觸覺回饋轉換為傳統震動,適用於所有手把。\n- 將「振動引擎」設定為「內建」,也可以透過 iPhone 馬達獲得增強觸覺回饋。\n\nDualSense 的自適應扳機、觸覺回饋僅在與 Sunshine 基地版配合使用時有效。" } } } @@ -8256,7 +8256,8 @@ "value" : "啟用 Metal 渲染?" } } - } + }, + "shouldTranslate" : false }, "enableOswForNativeTouchSwitchTip" : { "extractionState" : "manual", @@ -9890,25 +9891,25 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "This setting is saved in [Game Profile]. To open game profile selector:\n- Tap the gamepad icon on navigation bar\n- While streaming, swipe inward from the %@ edge of the screen by %@ of screen width, open [Switch Game Profile] in the toolbox menu" + "value" : "This setting is saved in [Game Profile]. To open game profile selector:\n- Tap the gamepad icon on navigation bar\n- While streaming, swipe inward from the %@ edge of the screen by %@ of screen width, open [Switch Game Profile] in the toolbox menu\n- Also available by controller radial menu -> [more options]" } }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "该设置保存在 [游戏配置] 中. 游戏配置切换入口:\n- 点击导航栏的手柄图标\n- 串流时,由屏幕 %@ 边缘向内滑动 %@ 屏宽,在工具箱菜单中打开 [切换游戏配置]" + "state" : "needs_review", + "value" : "该设置保存在 [游戏配置] 中. 游戏配置切换入口:\n- 点击导航栏的手柄图标\n- 串流时,由屏幕 %@ 边缘向内滑动 %@ 屏宽,在工具箱菜单中打开 [切换游戏配置]\n- 手柄用户请在环形菜单 -> [更多] 中打开" } }, "zh-Hant" : { "stringUnit" : { - "state" : "translated", - "value" : "此設定保存在[遊戲配置]中。遊戲配置切換入口:\n- 點擊導覽列的手把圖示\n- 串流時,由螢幕 %@ 邊緣向內滑動 %@ 螢幕寬度,在工具箱選單中開啟[切換遊戲配置]" + "state" : "needs_review", + "value" : "此設定保存在[遊戲配置]中。遊戲配置切換入口:\n- 點擊導覽列的手把圖示\n- 串流時,由螢幕 %@ 邊緣向內滑動 %@ 螢幕寬度,在工具箱選單中開啟[切換遊戲配置]\n- 手把使用者請在環形選單 -> [更多] 中開啟" } }, "zh-HK" : { "stringUnit" : { - "state" : "translated", - "value" : "此設定保存在[遊戲配置]中。遊戲配置切換入口:\n- 點擊導覽列的手把圖示\n- 串流時,由螢幕 %@ 邊緣向內滑動 %@ 螢幕寬度,在工具箱選單中開啟[切換遊戲配置]" + "state" : "needs_review", + "value" : "此設定保存在[遊戲配置]中。遊戲配置切換入口:\n- 點擊導覽列的手把圖示\n- 串流時,由螢幕 %@ 邊緣向內滑動 %@ 螢幕寬度,在工具箱選單中開啟[切換遊戲配置]\n- 手把使用者請在環形選單 -> [更多] 中開啟" } } } diff --git a/VoidLink/Stream/Connection.h b/VoidLink/Stream/Connection.h index 2593ea52..2a52ee68 100644 --- a/VoidLink/Stream/Connection.h +++ b/VoidLink/Stream/Connection.h @@ -21,6 +21,7 @@ @interface Connection : NSOperation @property (class, nonatomic, assign) bool muteInBackground; @property (class, nonatomic, assign) bool useSystemAudioEngine; +@property (class, nonatomic, assign) bool useDualSenseHapticsIrV2; -(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id)callbacks; -(void) terminate; @@ -32,4 +33,8 @@ + (void)setVolume:(float)newVolume; + (void)resetSysAudioPlayback; ++ (bool)useDualSenseAuthoredPCM; ++ (void)setuseDualSenseAuthoredPCM:(bool)use; + + @end diff --git a/VoidLink/Stream/Connection.m b/VoidLink/Stream/Connection.m index 45b64346..2b746e93 100644 --- a/VoidLink/Stream/Connection.m +++ b/VoidLink/Stream/Connection.m @@ -61,6 +61,7 @@ static AVAudioFormat *audioFormat; static bool muteInBackground; static bool fullColorRange; static bool request10BitCodec; +static bool useDualSenseAuthoredPCM = false; static VideoDecoderRenderer* renderer; @@ -357,6 +358,14 @@ void ArCleanup(void) useSystemAudioEngine = useSysAudioEngine; } ++ (bool)useDualSenseAuthoredPCM { + return useDualSenseAuthoredPCM; +} + ++ (void)setuseDualSenseAuthoredPCM:(bool)use { + useDualSenseAuthoredPCM = use; +} + void AudioEngineInit(int sampleRate, int channelCount) { audioEngine = [[AVAudioEngine alloc] init]; @@ -604,6 +613,30 @@ void ClDs5HapticsPcm(const LI_DS5_HAPTICS_PCM_FRAME* frame) pcmData:pcmData]; } +void ClDs5HapticsIrV2(const LI_DS5_HAPTICS_IR_FRAME_V2* frame) +{ + if (frame == NULL) { + return; + } + + [ControllerUtil enqueueDualSenseHapticsIRV2WithControllerNumber:frame->controllerNumber + flags:frame->flags + sourceSequenceNumber:frame->sourceSequenceNumber + timestampUs:frame->timestampUs + sourceFrameCount:frame->sourceFrameCount + leftRms:frame->lanes[0].rmsAmplitude + leftPeak:frame->lanes[0].peakAmplitude + leftTransient:frame->lanes[0].transientStrength + leftLowRatio:frame->lanes[0].lowBandRatio + leftZeroCrossHz:frame->lanes[0].zeroCrossingRateHz + rightRms:frame->lanes[1].rmsAmplitude + rightPeak:frame->lanes[1].peakAmplitude + rightTransient:frame->lanes[1].transientStrength + rightLowRatio:frame->lanes[1].lowBandRatio + rightZeroCrossHz:frame->lanes[1].zeroCrossingRateHz + laneCorrelation:frame->laneCorrelation]; +} + -(void) terminate { // Interrupt any action blocking LiStartConnection(). This is @@ -706,6 +739,7 @@ void ClDs5HapticsPcm(const LI_DS5_HAPTICS_PCM_FRAME* frame) _streamConfig.audioConfiguration = config.audioConfiguration; _streamConfig.redirectMic = config.redirectMic && [MicHandler permissionGranted]; [Connection setVolume:config.localVolume]; + // Since we require iOS 12 or above, we're guaranteed to be running // on a 64-bit device with ARMv8 crypto instructions, so we don't // need to check for that here. @@ -767,8 +801,13 @@ void ClDs5HapticsPcm(const LI_DS5_HAPTICS_PCM_FRAME* frame) _clCallbacks.setMotionEventState = ClSetMotionEventState; _clCallbacks.setControllerLED = ClSetControllerLED; _clCallbacks.setAdaptiveTriggers = ClSetAdaptiveTriggers; - _clCallbacks.ds5HapticsPcm = ClDs5HapticsPcm; - + useDualSenseAuthoredPCM = (config.emulatedControllerType == ControllerEmulationPsEnhancedHaptic + && ControllerUtil.hasDualSenseController) || config.hapticEngine == RumbleDevice; + if (useDualSenseAuthoredPCM) { + _clCallbacks.ds5HapticsPcm = ClDs5HapticsPcm; + // _clCallbacks.ds5HapticsIrV2 = ClDs5HapticsIrV2; + } + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioSessionInterruption:) name:AVAudioSessionInterruptionNotification diff --git a/VoidLink/Stream/StreamConfiguration.h b/VoidLink/Stream/StreamConfiguration.h index ca3ea07c..7e9cfb09 100644 --- a/VoidLink/Stream/StreamConfiguration.h +++ b/VoidLink/Stream/StreamConfiguration.h @@ -40,6 +40,7 @@ @property BOOL asyncNativeTouchPriority; @property int gyroMode; @property int emulatedControllerType; +@property int hapticEngine; @property int audioConfiguration; @property int supportedVideoFormats; @property BOOL multiController; diff --git a/VoidLink/Stream/StreamConfiguration.m b/VoidLink/Stream/StreamConfiguration.m index 42d00fd1..3dd7a102 100644 --- a/VoidLink/Stream/StreamConfiguration.m +++ b/VoidLink/Stream/StreamConfiguration.m @@ -9,5 +9,5 @@ #import "StreamConfiguration.h" @implementation StreamConfiguration -@synthesize host, httpsPort, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, swapABXYButtons, buttonVisualFeedback, gyroMode, emulatedControllerType, audioConfiguration, supportedVideoFormats, multiController, serverCert, rtspSessionUrl, serverCodecModeSupport, enableYUV444, enablePIP, fullColorRange, enableHdr, sdrPerformanceWorkaround, localVolume; +@synthesize host, httpsPort, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, swapABXYButtons, buttonVisualFeedback, gyroMode, emulatedControllerType, hapticEngine, audioConfiguration, supportedVideoFormats, multiController, serverCert, rtspSessionUrl, serverCodecModeSupport, enableYUV444, enablePIP, fullColorRange, enableHdr, sdrPerformanceWorkaround, localVolume; @end diff --git a/VoidLink/Utility/GenericUtils.swift b/VoidLink/Utility/GenericUtils.swift index 0991a940..30f267fa 100644 --- a/VoidLink/Utility/GenericUtils.swift +++ b/VoidLink/Utility/GenericUtils.swift @@ -117,6 +117,24 @@ import UIKit return true } + + @objc public static func handleControllerEmulationTip(in vc: UIViewController?) { + let key = "hasShownControllerEmulationTip2" + guard !UserDefaults.standard.bool(forKey: key) else { + return + } + UserDefaults.standard.set(true, forKey: key) + + AlertControllerUtil.showAlert( + in: vc, + title: LocalizationHelper.localizedString(forKey: "Tips"), + message: LocalizationHelper.localizedString(forKey: "emulatedControllerTypeStackTip"), + withCancel: false, + buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"), + countdown: 7 + ) + } + @objc public static func needUpdateDefaultSettings() -> Bool { // let key = "needUpdateDefaultSettings20260226-1" diff --git a/VoidLink/ViewControllers/MainFrameViewController.m b/VoidLink/ViewControllers/MainFrameViewController.m index b0c0228d..0cd94fa7 100644 --- a/VoidLink/ViewControllers/MainFrameViewController.m +++ b/VoidLink/ViewControllers/MainFrameViewController.m @@ -882,6 +882,7 @@ static NSMutableSet* hostList; _streamConfig.asyncNativeTouchPriority = streamSettings.asyncNativeTouchPriority; // new streamConfig segment _streamConfig.gyroMode = [streamSettings.gyroMode intValue]; _streamConfig.emulatedControllerType = streamSettings.emulatedControllerType.intValue; + _streamConfig.hapticEngine = streamSettings.hapticEngine.intValue; //NSLog(@"gyroMode from settings: %ld", _streamConfig.gyroMode); // multiController must be set before calling getConnectedGamepadMask diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index cfa029ef..82a07bdb 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -268,9 +268,6 @@ @property (strong, nonatomic) IBOutlet UIStackView *pressureCurveStack; @property (strong, nonatomic) IBOutlet UISwitch *pressureCurveSwitch; -@property (weak, nonatomic) IBOutlet UIStackView *frameTimebaseStack; -@property (weak, nonatomic) IBOutlet UISwitch *frameTimebaseSwitch; - @property (weak, nonatomic) IBOutlet UIStackView *asyncFrameDequeueStack; @property (weak, nonatomic) IBOutlet UISwitch *asyncFrameDequeueSwitch; @@ -283,6 +280,9 @@ @property (weak, nonatomic) IBOutlet UIStackView *globeAsEscapeStack; @property (weak, nonatomic) IBOutlet UISwitch *globeAsEscapeSwitch; +@property (weak, nonatomic) IBOutlet UIStackView *dualSenseTransientStack; +@property (weak, nonatomic) IBOutlet UISlider *dualSenseTransientSlider; + @property (strong, nonatomic) IBOutlet UIStackView *testStack; @property (nonatomic, strong) MicHandler *micHandler; diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index c19dfe86..5e0f6bbe 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -211,15 +211,21 @@ CMVideoDimensions resolutionTable[RESOLUTION_TABLE_SIZE]; -(uint16_t)controllerTypeToSegmentIndex:(uint16_t)type{ uint16_t index; switch (type) { - case LI_CTYPE_XBOX: + case ControllerEmulationXbox: index = 0; break; - case LI_CTYPE_PS: + case ControllerEmulationPs: index = 1; break; - default: + case ControllerEmulationPsEnhancedHaptic: index = 2; break; + case ControllerEmulationXboxAndPs: + index = 3; + break; + default: + index = 3; + break; } return index; } @@ -228,10 +234,16 @@ CMVideoDimensions resolutionTable[RESOLUTION_TABLE_SIZE]; uint16_t type; switch (index) { case 0: - type = LI_CTYPE_XBOX; + type = ControllerEmulationXbox; break; case 1: - type = LI_CTYPE_PS; + type = ControllerEmulationPs; + break; + case 2: + type = ControllerEmulationPsEnhancedHaptic; + break; + case 3: + type = ControllerEmulationXboxAndPs; break; default: type = LI_CTYPE_UNKNOWN; @@ -428,6 +440,9 @@ BOOL isCustomResolution(int resolutionSelected) { [self.synthPhysicalInputSwitch setOn:oscProfile.synthesizePhysicalStick]; + [self.dualSenseTransientSlider setValue:oscProfile.dualSenseTransient]; + [self.dualSenseTransientSlider sendActionsForControlEvents:UIControlEventValueChanged]; + [self.pressureCurveSwitch setOn:oscProfile.pressureCurveEnabled]; [self.doubleTapShortcutSwitch setOn:oscProfile.doubleTapShorcutEnabled]; [self.squeezeShortcutSwitch setOn:oscProfile.squeezeShorcutEnabled]; @@ -461,6 +476,7 @@ BOOL isCustomResolution(int resolutionSelected) { && (int16_t)(oscProfile.gyroSensitivityPitch*100) == (int16_t)pitchSensitivityPercent && (int16_t)(oscProfile.gyroSensitivityRoll*100) == (int16_t)rollSensitivityPercent && (int16_t)(oscProfile.gyroToStickMinOffset) == (int16_t)self.gyroToStickMinOffsetSlider.value + && (int16_t)(oscProfile.dualSenseTransient*100) == (int16_t)(self.dualSenseTransientSlider.value*100) && oscProfile.synthesizePhysicalStick == self.synthPhysicalInputSwitch.isOn && oscProfile.controllerGyroSwitchMode == self.controllerGyroSwitchButtonSetter.selectedSegmentIndex && oscProfile.reverseGyroHoldButton == self.reverseHoldButtonSwitch.isOn @@ -488,6 +504,7 @@ BOOL isCustomResolution(int resolutionSelected) { oscProfile.gyroSensitivityPitch = pitchSensitivityPercent/100; oscProfile.gyroSensitivityRoll = rollSensitivityPercent/100; oscProfile.gyroToStickMinOffset = (int16_t)self.gyroToStickMinOffsetSlider.value; + oscProfile.dualSenseTransient = self.dualSenseTransientSlider.value; oscProfile.synthesizePhysicalStick = self.synthPhysicalInputSwitch.isOn; oscProfile.controllerGyroSwitchMode = (int)self.controllerGyroSwitchButtonSetter.selectedSegmentIndex; oscProfile.reverseGyroHoldButton = self.reverseHoldButtonSwitch.isOn; @@ -996,6 +1013,11 @@ BOOL isCustomResolution(int resolutionSelected) { self.emulatedControllerTypeStack.hasInfoTag = YES; // self.emulatedControllerTypeStack.isGameProfileSetting = YES; [self addSetting:self.emulatedControllerTypeStack ofId:@"emulatedControllerTypeStack" to:controllerSection]; + + self.dualSenseTransientStack.hasInfoTag = YES; + self.dualSenseTransientStack.isGameProfileSetting = YES; + self.dualSenseTransientStack.hasDynamicLabel = YES; + [self addSetting:self.dualSenseTransientStack ofId:@"dualSenseTransientStack" to:controllerSection]; self.gyroModeStack.hasInfoTag = YES; // self.gyroModeStack.isGameProfileSetting = YES; @@ -2321,6 +2343,8 @@ BOOL isCustomResolution(int resolutionSelected) { [self.hapticEngineSelector setEnabled:!disableControllerRumble forSegmentAtIndex:LeftRightSwapped]; [self.hapticEngineSelector setEnabled:!disableControllerRumble forSegmentAtIndex:HapticEngineAuto]; [self.hapticEngineSelector setEnabled:[self isIPhone] forSegmentAtIndex:RumbleDevice]; + + [self.dualSenseTransientSlider addTarget:self action:@selector(dualSenseTransientSliderMoved:) forControlEvents:UIControlEventValueChanged]; self.gyroModeSelector.selectedSegmentIndex = self->tempSettings.gyroMode.intValue; [self.gyroSensitivitySlider setValue: (uint16_t)(self->tempSettings.gyroSensitivity.floatValue * 100) animated:NO]; // Load old setting. @@ -3425,6 +3449,11 @@ BOOL isCustomResolution(int resolutionSelected) { } } +- (void)dualSenseTransientSliderMoved:(UISlider* )sender { + [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %.2f ", sender.value]; // Update label display + ControllerUtil.dualSenseHapticTransient = sender.value; +} + - (void) gyroSensitivitySliderMoved:(UISlider* )sender { [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)sender.value]; // Update label display } @@ -3619,6 +3648,12 @@ BOOL isCustomResolution(int resolutionSelected) { - (void)emulatedControllerTypeChanged:(UISegmentedControl* )sender{ [self setHidden:sender.selectedSegmentIndex == 0 forStack:_gyroModeStack]; [self setHidden:sender.selectedSegmentIndex == 0 forStack:_gyroSensitivityStack]; + [self setHidden:sender.selectedSegmentIndex != 2 forStack:_dualSenseTransientStack]; + + if(settingsViewJustExpanded) return; + dispatch_async(dispatch_get_main_queue(), ^{ + [GenericUtils handleControllerEmulationTipIn:self]; + }); } diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index 6fb6d44f..e0ce207e 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -2425,18 +2425,6 @@ } } }, - "cpg-mq-kYj.text" : { - "comment" : "Class = \"UILabel\"; text = \"Enable Timebase\"; ObjectID = \"cpg-mq-kYj\";", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Enable Timebase" - } - } - } - }, "CpY-Sb-BDc.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Import\"; ObjectID = \"CpY-Sb-BDc\";", "extractionState" : "stale", @@ -3999,43 +3987,55 @@ } }, "JaY-XA-1V6.segmentTitles[1]" : { - "comment" : "Class = \"UISegmentedControl\"; JaY-XA-1V6.segmentTitles[1] = \"DS4\"; ObjectID = \"JaY-XA-1V6\";", + "comment" : "Class = \"UISegmentedControl\"; JaY-XA-1V6.segmentTitles[1] = \"PS\"; ObjectID = \"JaY-XA-1V6\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "DS4" + "value" : "PS" } } } }, "JaY-XA-1V6.segmentTitles[2]" : { - "comment" : "Class = \"UISegmentedControl\"; JaY-XA-1V6.segmentTitles[2] = \"Both\"; ObjectID = \"JaY-XA-1V6\";", + "comment" : "Class = \"UISegmentedControl\"; JaY-XA-1V6.segmentTitles[2] = \"PS (Enhanced Haptics)\"; ObjectID = \"JaY-XA-1V6\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "Both" + "value" : "PS (Enhanced Haptics)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "同时映射" + "value" : "PS (触觉增强)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "同時映射" + "value" : "PS(觸覺增強)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "同時映射" + "value" : "PS(觸覺增強)" + } + } + } + }, + "JaY-XA-1V6.segmentTitles[3]" : { + "comment" : "Class = \"UISegmentedControl\"; JaY-XA-1V6.segmentTitles[3] = \"Xbox + PS\"; ObjectID = \"JaY-XA-1V6\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Xbox + PS" } } } @@ -6598,6 +6598,36 @@ } } }, + "UpM-yi-yc9.text" : { + "comment" : "Class = \"UILabel\"; text = \"DualSense Haptic Intensity\"; ObjectID = \"UpM-yi-yc9\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DualSense Haptic Intensity" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "DualSense振动强度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "DualSense振動強度" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "DualSense振動強度" + } + } + } + }, "V1y-Wk-uHM.text" : { "comment" : "Class = \"UILabel\"; text = \"Height\"; ObjectID = \"V1y-Wk-uHM\";", "extractionState" : "extracted_with_value", diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings index 630bd2ca..a1593560 100644 --- a/VoidLink/mul.lproj/iPhone.xcstrings +++ b/VoidLink/mul.lproj/iPhone.xcstrings @@ -4789,18 +4789,6 @@ } } }, - "P4N-i6-1zD.text" : { - "comment" : "Class = \"UILabel\"; text = \"Enable Timebase\"; ObjectID = \"P4N-i6-1zD\";", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Enable Timebase" - } - } - } - }, "P9b-Th-Ybc.text" : { "comment" : "Class = \"UILabel\"; text = \" Batch move \"; ObjectID = \"P9b-Th-Ybc\";", "extractionState" : "extracted_with_value", @@ -5216,43 +5204,55 @@ } }, "q6v-Jb-ysy.segmentTitles[1]" : { - "comment" : "Class = \"UISegmentedControl\"; q6v-Jb-ysy.segmentTitles[1] = \"DS4\"; ObjectID = \"q6v-Jb-ysy\";", + "comment" : "Class = \"UISegmentedControl\"; q6v-Jb-ysy.segmentTitles[1] = \"PS\"; ObjectID = \"q6v-Jb-ysy\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "DS4" + "value" : "PS" } } } }, "q6v-Jb-ysy.segmentTitles[2]" : { - "comment" : "Class = \"UISegmentedControl\"; q6v-Jb-ysy.segmentTitles[2] = \"Both\"; ObjectID = \"q6v-Jb-ysy\";", + "comment" : "Class = \"UISegmentedControl\"; q6v-Jb-ysy.segmentTitles[2] = \"PS (Enhanced Haptics)\"; ObjectID = \"q6v-Jb-ysy\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "Both" + "value" : "PS (Enhanced Haptics)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "同时映射" + "value" : "PS (触觉增强)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "同時映射" + "value" : "PS(觸覺增強)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "同時映射" + "value" : "PS(觸覺增強)" + } + } + } + }, + "q6v-Jb-ysy.segmentTitles[3]" : { + "comment" : "Class = \"UISegmentedControl\"; q6v-Jb-ysy.segmentTitles[3] = \"Xbox + PS\"; ObjectID = \"q6v-Jb-ysy\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Xbox + PS" } } } @@ -6325,6 +6325,36 @@ } } }, + "vak-fg-iAQ.text" : { + "comment" : "Class = \"UILabel\"; text = \"DualSense Haptic Intensity\"; ObjectID = \"vak-fg-iAQ\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DualSense Haptic Intensity" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "DualSense振动强度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "DualSense振動強度" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "DualSense振動強度" + } + } + } + }, "VEc-6n-QcS.text" : { "comment" : "Class = \"UILabel\"; text = \"Lift Stream View for Local Keyboard \"; ObjectID = \"VEc-6n-QcS\";", "extractionState" : "extracted_with_value",