dualSense haptic rumbling by qin2333's PCM convertor
This commit is contained in:
@@ -7,3 +7,6 @@ url=https://github.com/TrueZhuangJia/voidlink-c.git
|
|||||||
[submodule "third_party/imgui"]
|
[submodule "third_party/imgui"]
|
||||||
path = third_party/imgui
|
path = third_party/imgui
|
||||||
url = https://github.com/ocornut/imgui.git
|
url = https://github.com/ocornut/imgui.git
|
||||||
|
[submodule "third_party/moonlight-audio-haptics"]
|
||||||
|
path = third_party/moonlight-audio-haptics
|
||||||
|
url = https://github.com/AlkaidLab/moonlight-audio-haptics.git
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
A1D500022FFC000100000001 /* authored_haptics_engine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A1D500012FFC000100000001 /* authored_haptics_engine.cpp */; settings = {COMPILER_FLAGS = "-std=c++17"; }; };
|
||||||
180345E12E819877004AF36F /* libopus-sim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 180345E02E819877004AF36F /* libopus-sim.a */; };
|
180345E12E819877004AF36F /* libopus-sim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 180345E02E819877004AF36F /* libopus-sim.a */; };
|
||||||
180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */; };
|
180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */; };
|
||||||
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1817B61D2E6FD04D000D938D /* SafeTimer.swift */; };
|
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1817B61D2E6FD04D000D938D /* SafeTimer.swift */; };
|
||||||
@@ -187,6 +188,7 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
A1D500012FFC000100000001 /* authored_haptics_engine.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = authored_haptics_engine.cpp; path = "third_party/moonlight-audio-haptics/src/core/authored_haptics_engine.cpp"; sourceTree = SOURCE_ROOT; };
|
||||||
180345E02E819877004AF36F /* libopus-sim.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libopus-sim.a"; sourceTree = "<group>"; };
|
180345E02E819877004AF36F /* libopus-sim.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libopus-sim.a"; sourceTree = "<group>"; };
|
||||||
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertControllerUtil.swift; sourceTree = "<group>"; };
|
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertControllerUtil.swift; sourceTree = "<group>"; };
|
||||||
1817B61D2E6FD04D000D938D /* SafeTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeTimer.swift; sourceTree = "<group>"; };
|
1817B61D2E6FD04D000D938D /* SafeTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeTimer.swift; sourceTree = "<group>"; };
|
||||||
@@ -1139,6 +1141,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A1D500022FFC000100000001 /* authored_haptics_engine.cpp in Sources */,
|
||||||
FB290D0719B2C406004C83CF /* Limelight.xcdatamodeld in Sources */,
|
FB290D0719B2C406004C83CF /* Limelight.xcdatamodeld in Sources */,
|
||||||
9819CC22254F180F008A7C8E /* AbsoluteTouchHandler.m in Sources */,
|
9819CC22254F180F008A7C8E /* AbsoluteTouchHandler.m in Sources */,
|
||||||
9819CC14254F107A008A7C8E /* RelativeTouchHandler.m in Sources */,
|
9819CC14254F107A008A7C8E /* RelativeTouchHandler.m in Sources */,
|
||||||
@@ -1345,7 +1348,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = com.trzj.VoidLinkPreview;
|
PRODUCT_BUNDLE_IDENTIFIER = com.trzj.VoidLinkPreview;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG VOIDLINK_PREVIEW $(inherited)";
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -1395,6 +1398,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = com.trzj.VoidLinkPreview;
|
PRODUCT_BUNDLE_IDENTIFIER = com.trzj.VoidLinkPreview;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "VOIDLINK_PREVIEW $(inherited)";
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
@@ -1529,10 +1533,13 @@
|
|||||||
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
|
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
|
||||||
HEADER_SEARCH_PATHS = (
|
HEADER_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
"$(PROJECT_DIR)/third_party/moonlight-audio-haptics/include",
|
||||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||||
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
|
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
|
||||||
"$(SDKROOT)/usr/include/libxml2/**",
|
"$(SDKROOT)/usr/include/libxml2/**",
|
||||||
"$(PROJECT_DIR)/libs/**",
|
"$(PROJECT_DIR)/libs/FFmpeg/include",
|
||||||
|
"$(PROJECT_DIR)/libs/SDL2/include",
|
||||||
|
"$(PROJECT_DIR)/libs/opus/include/opus",
|
||||||
"$(PROJECT_DIR)/Limelight/Input/CustomOSC",
|
"$(PROJECT_DIR)/Limelight/Input/CustomOSC",
|
||||||
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl",
|
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl",
|
||||||
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
|
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
|
||||||
@@ -1597,10 +1604,13 @@
|
|||||||
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
|
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
|
||||||
HEADER_SEARCH_PATHS = (
|
HEADER_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
"$(PROJECT_DIR)/third_party/moonlight-audio-haptics/include",
|
||||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||||
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
|
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
|
||||||
"$(SDKROOT)/usr/include/libxml2/**",
|
"$(SDKROOT)/usr/include/libxml2/**",
|
||||||
"$(PROJECT_DIR)/libs/**",
|
"$(PROJECT_DIR)/libs/FFmpeg/include",
|
||||||
|
"$(PROJECT_DIR)/libs/SDL2/include",
|
||||||
|
"$(PROJECT_DIR)/libs/opus/include/opus",
|
||||||
"$(PROJECT_DIR)/Limelight/Input/CustomOSC",
|
"$(PROJECT_DIR)/Limelight/Input/CustomOSC",
|
||||||
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl",
|
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl",
|
||||||
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
|
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ final class ControllerNavigator: NSObject {
|
|||||||
guard uiNavigationDelegate === delegate else { return }
|
guard uiNavigationDelegate === delegate else { return }
|
||||||
/*
|
/*
|
||||||
if previousUINavigationDelegate is StreamFrameViewController {
|
if previousUINavigationDelegate is StreamFrameViewController {
|
||||||
ControllerSupport.sharedInstance().reinitiatePrimaryController()
|
ControllerSupport.sharedInstance()?.reinitiatePrimaryController()
|
||||||
return
|
return
|
||||||
} */
|
} */
|
||||||
|
|
||||||
@@ -378,7 +378,7 @@ final class ControllerNavigator: NSObject {
|
|||||||
// GamepadNavigationIllustrationHud.updateNavigationElements(controllerMouseNavigationElements)
|
// GamepadNavigationIllustrationHud.updateNavigationElements(controllerMouseNavigationElements)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ControllerSupport.sharedInstance().reinitiatePrimaryController()
|
ControllerSupport.sharedInstance()?.reinitiatePrimaryController()
|
||||||
GamepadNavigationIllustrationHud.updateHud()
|
GamepadNavigationIllustrationHud.updateHud()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -769,7 +769,7 @@ final class ControllerNavigator: NSObject {
|
|||||||
radialMenuDelayTimer?.clean()
|
radialMenuDelayTimer?.clean()
|
||||||
stickReleasedInRadialMenu = false
|
stickReleasedInRadialMenu = false
|
||||||
if radialMenuView == nil, uiNavigationDelegate is StreamFrameViewController {
|
if radialMenuView == nil, uiNavigationDelegate is StreamFrameViewController {
|
||||||
ControllerSupport.sharedInstance().sendNavigationButtonPress()
|
ControllerSupport.sharedInstance()?.sendNavigationButtonPress()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
radialMenuView?.dismiss()
|
radialMenuView?.dismiss()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
@property (readonly) bool shallDisableGyroHotSwitch;
|
@property (readonly) bool shallDisableGyroHotSwitch;
|
||||||
|
|
||||||
+(ControllerSupport*) sharedInstance;
|
+(nullable ControllerSupport*) sharedInstance;
|
||||||
|
|
||||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
||||||
-(void) connectionEstablished;
|
-(void) connectionEstablished;
|
||||||
@@ -55,6 +55,15 @@
|
|||||||
-(void) setAdaptiveTriggers:(uint16_t)controllerNumber eventFlags:(uint8_t)eventFlags
|
-(void) setAdaptiveTriggers:(uint16_t)controllerNumber eventFlags:(uint8_t)eventFlags
|
||||||
typeLeft:(uint8_t)typeLeft typeRight:(uint8_t)typeRight
|
typeLeft:(uint8_t)typeLeft typeRight:(uint8_t)typeRight
|
||||||
left:(const uint8_t*)left right:(const uint8_t*)right;
|
left:(const uint8_t*)left right:(const uint8_t*)right;
|
||||||
|
-(void) renderDualSenseHaptics:(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;
|
-(void) updateTimerStateForOsc;
|
||||||
|
|
||||||
-(uint16_t) getActiveGamepadMask;
|
-(uint16_t) getActiveGamepadMask;
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger,
|
|||||||
bool _controllerGyroSwitchHoldPressed;
|
bool _controllerGyroSwitchHoldPressed;
|
||||||
ControllerGyroSwitchMode _gyroSwitchMode;
|
ControllerGyroSwitchMode _gyroSwitchMode;
|
||||||
|
|
||||||
|
uint64_t _authoredHapticsGeneration;
|
||||||
|
|
||||||
__weak MotionHandler* motionHandler;
|
__weak MotionHandler* motionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,6 +562,55 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-(void) renderDualSenseHaptics:(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.5, tvOS 14.5, *)) {
|
||||||
|
VoidController* controller = [self->_voidControllers objectForKey:@(controllerNumber)];
|
||||||
|
if (controller == nil ||
|
||||||
|
![controller.gamepad.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[controller.lowFreqMotor setAuthoredAmplitude:leftAmplitude
|
||||||
|
sharpness:leftSharpness
|
||||||
|
transientStrength:leftTransient];
|
||||||
|
[controller.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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
-(void) updateLeftStick:(VoidController*)controller x:(short)x y:(short)y
|
-(void) updateLeftStick:(VoidController*)controller x:(short)x y:(short)y
|
||||||
{
|
{
|
||||||
@synchronized(controller) {
|
@synchronized(controller) {
|
||||||
@@ -990,6 +1041,12 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (@available(iOS 14.5, tvOS 14.5, *)) {
|
||||||
|
if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) {
|
||||||
|
capabilities |= LI_CCAP_DS5_HAPTICS_PCM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Detect supported haptics localities
|
// Detect supported haptics localities
|
||||||
if (controller.haptics) {
|
if (controller.haptics) {
|
||||||
@@ -2063,6 +2120,8 @@ double rc_expo(double x, double expo) {
|
|||||||
|
|
||||||
-(void) cleanup
|
-(void) cleanup
|
||||||
{
|
{
|
||||||
|
[ControllerUtil stopAllDualSenseHaptics];
|
||||||
|
|
||||||
if (VLSharedControllerSupport == self) {
|
if (VLSharedControllerSupport == self) {
|
||||||
VLSharedControllerSupport = nil;
|
VLSharedControllerSupport = nil;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import GameController
|
|||||||
import Combine
|
import Combine
|
||||||
import CoreGraphics
|
import CoreGraphics
|
||||||
import Foundation
|
import Foundation
|
||||||
|
#if !VOIDLINK_PREVIEW
|
||||||
|
import Collections
|
||||||
|
#endif
|
||||||
|
|
||||||
@objc enum ControllerElementType: Int {
|
@objc enum ControllerElementType: Int {
|
||||||
case button
|
case button
|
||||||
@@ -260,6 +263,261 @@ import Foundation
|
|||||||
|
|
||||||
@objc class ControllerUtil: NSObject {
|
@objc class ControllerUtil: NSObject {
|
||||||
@objc static weak var delegate: ControllerUtilDelegate?
|
@objc static weak var delegate: ControllerUtilDelegate?
|
||||||
|
|
||||||
|
#if !VOIDLINK_PREVIEW
|
||||||
|
private struct DualSensePCMChunk {
|
||||||
|
let controllerNumber: UInt16
|
||||||
|
let flags: UInt8
|
||||||
|
let sequenceNumber: UInt32
|
||||||
|
let presentationTimeUs: UInt64
|
||||||
|
let frameCount: UInt16
|
||||||
|
let pcmData: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class AuthoredEngineState {
|
||||||
|
var engine: OpaquePointer?
|
||||||
|
var renderHostAnchorUs: UInt64?
|
||||||
|
var renderLocalAnchor = 0.0
|
||||||
|
|
||||||
|
init?() {
|
||||||
|
var config = AhAuthoredConfig()
|
||||||
|
guard ah_authored_config_init(&config, 48_000) == AH_STATUS_OK else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard ah_authored_create(&config, &engine) == AH_STATUS_OK, engine != nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
ah_authored_destroy(engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderDelay(for timestampUs: UInt64, resetTimeline: Bool) -> TimeInterval {
|
||||||
|
let now = ProcessInfo.processInfo.systemUptime
|
||||||
|
if resetTimeline || renderHostAnchorUs == nil {
|
||||||
|
renderHostAnchorUs = timestampUs
|
||||||
|
renderLocalAnchor = now + 0.01
|
||||||
|
}
|
||||||
|
let anchor = renderHostAnchorUs ?? timestampUs
|
||||||
|
let hostDelta = timestampUs >= anchor ? Double(timestampUs - anchor) / 1_000_000.0 : 0
|
||||||
|
return min(max(renderLocalAnchor + hostDelta - now, 0), 0.08)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 var dualSensePCMQueue = Deque<DualSensePCMChunk>()
|
||||||
|
private static var dualSenseWorkerScheduled = false
|
||||||
|
private static var dualSenseForcedDiscontinuities = Set<UInt16>()
|
||||||
|
private static var dualSenseAuthoredEngines = [UInt16: AuthoredEngineState]()
|
||||||
|
|
||||||
|
@objc(enqueueDualSenseHapticsPCMWithControllerNumber:flags:sequenceNumber:presentationTimeUs:frameCount:pcmData:)
|
||||||
|
static func enqueueDualSenseHapticsPCM(
|
||||||
|
controllerNumber: UInt16,
|
||||||
|
flags: UInt8,
|
||||||
|
sequenceNumber: UInt32,
|
||||||
|
presentationTimeUs: UInt64,
|
||||||
|
frameCount: UInt16,
|
||||||
|
pcmData: Data
|
||||||
|
) {
|
||||||
|
let expectedByteCount = Int(frameCount) * 2 * MemoryLayout<Int16>.size
|
||||||
|
guard frameCount <= 480, pcmData.count == expectedByteCount else {
|
||||||
|
NSLog("DualSense haptics: rejected malformed PCM chunk for controller %u", controllerNumber)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk = DualSensePCMChunk(
|
||||||
|
controllerNumber: controllerNumber,
|
||||||
|
flags: flags,
|
||||||
|
sequenceNumber: sequenceNumber,
|
||||||
|
presentationTimeUs: presentationTimeUs,
|
||||||
|
frameCount: frameCount,
|
||||||
|
pcmData: pcmData
|
||||||
|
)
|
||||||
|
|
||||||
|
dualSenseHapticsLock.lock()
|
||||||
|
if dualSensePCMQueue.count >= maxQueuedDualSensePCMChunks,
|
||||||
|
let dropped = dualSensePCMQueue.popFirst() {
|
||||||
|
dualSenseForcedDiscontinuities.insert(dropped.controllerNumber)
|
||||||
|
}
|
||||||
|
dualSensePCMQueue.append(chunk)
|
||||||
|
let shouldScheduleWorker = !dualSenseWorkerScheduled
|
||||||
|
dualSenseWorkerScheduled = true
|
||||||
|
dualSenseHapticsLock.unlock()
|
||||||
|
|
||||||
|
if shouldScheduleWorker {
|
||||||
|
dualSenseHapticsWorker.async {
|
||||||
|
drainDualSensePCMQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc static func stopAllDualSenseHaptics() {
|
||||||
|
ControllerSupport.sharedInstance()?.cancelScheduledDualSenseHaptics()
|
||||||
|
|
||||||
|
dualSenseHapticsLock.lock()
|
||||||
|
dualSensePCMQueue.removeAll(keepingCapacity: true)
|
||||||
|
dualSenseForcedDiscontinuities.removeAll(keepingCapacity: true)
|
||||||
|
dualSenseHapticsLock.unlock()
|
||||||
|
|
||||||
|
dualSenseHapticsWorker.async {
|
||||||
|
let controllerNumbers = Array(dualSenseAuthoredEngines.keys)
|
||||||
|
dualSenseAuthoredEngines.removeAll(keepingCapacity: false)
|
||||||
|
for controllerNumber in controllerNumbers {
|
||||||
|
renderDualSenseHaptics(controllerNumber: controllerNumber, left: nil, right: nil, delay: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func drainDualSensePCMQueue() {
|
||||||
|
while true {
|
||||||
|
dualSenseHapticsLock.lock()
|
||||||
|
guard var chunk = dualSensePCMQueue.popFirst() else {
|
||||||
|
dualSenseWorkerScheduled = false
|
||||||
|
dualSenseHapticsLock.unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dualSenseForcedDiscontinuities.remove(chunk.controllerNumber) != nil {
|
||||||
|
chunk = DualSensePCMChunk(
|
||||||
|
controllerNumber: chunk.controllerNumber,
|
||||||
|
flags: chunk.flags | 0x04,
|
||||||
|
sequenceNumber: chunk.sequenceNumber,
|
||||||
|
presentationTimeUs: chunk.presentationTimeUs,
|
||||||
|
frameCount: chunk.frameCount,
|
||||||
|
pcmData: chunk.pcmData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dualSenseHapticsLock.unlock()
|
||||||
|
processDualSensePCMChunk(chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func processDualSensePCMChunk(_ chunk: DualSensePCMChunk) {
|
||||||
|
let isStreamStart = (chunk.flags & 0x01) != 0
|
||||||
|
let isStreamEnd = (chunk.flags & 0x02) != 0
|
||||||
|
let isDiscontinuity = (chunk.flags & 0x04) != 0
|
||||||
|
|
||||||
|
if isStreamStart || dualSenseAuthoredEngines[chunk.controllerNumber] == nil {
|
||||||
|
dualSenseAuthoredEngines[chunk.controllerNumber] = AuthoredEngineState()
|
||||||
|
NSLog("DualSense haptics: stream started for controller %u", chunk.controllerNumber)
|
||||||
|
}
|
||||||
|
guard let state = dualSenseAuthoredEngines[chunk.controllerNumber],
|
||||||
|
let engine = state.engine else {
|
||||||
|
NSLog("DualSense haptics: failed to create authored engine for controller %u", chunk.controllerNumber)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputFlags: UInt32 = 0
|
||||||
|
if isStreamStart { inputFlags |= UInt32(AH_AUTHORED_INPUT_STREAM_START) }
|
||||||
|
if isDiscontinuity { inputFlags |= UInt32(AH_AUTHORED_INPUT_DISCONTINUITY) }
|
||||||
|
if isStreamEnd { inputFlags |= UInt32(AH_AUTHORED_INPUT_STREAM_END) }
|
||||||
|
|
||||||
|
var samples = [Int16](repeating: 0, count: Int(chunk.frameCount) * 2)
|
||||||
|
chunk.pcmData.withUnsafeBytes { rawBuffer in
|
||||||
|
let bytes = rawBuffer.bindMemory(to: UInt8.self)
|
||||||
|
for index in samples.indices {
|
||||||
|
let offset = index * 2
|
||||||
|
let value = UInt16(bytes[offset]) | (UInt16(bytes[offset + 1]) << 8)
|
||||||
|
samples[index] = Int16(bitPattern: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputCapacity = ah_authored_get_max_output_frames(
|
||||||
|
engine,
|
||||||
|
UInt32(chunk.frameCount),
|
||||||
|
inputFlags
|
||||||
|
)
|
||||||
|
var outputFrames = [AhAuthoredHapticFrame](
|
||||||
|
repeating: AhAuthoredHapticFrame(),
|
||||||
|
count: Int(outputCapacity)
|
||||||
|
)
|
||||||
|
var outputCount: UInt32 = 0
|
||||||
|
let status = samples.withUnsafeBufferPointer { sampleBuffer -> AhStatus in
|
||||||
|
var input = AhAuthoredProcessInput()
|
||||||
|
input.struct_size = UInt32(MemoryLayout<AhAuthoredProcessInput>.size)
|
||||||
|
input.interleaved_pcm = sampleBuffer.baseAddress
|
||||||
|
input.frame_count = UInt32(chunk.frameCount)
|
||||||
|
input.flags = inputFlags
|
||||||
|
input.first_sample_time_us = chunk.presentationTimeUs
|
||||||
|
input.sequence_number = chunk.sequenceNumber
|
||||||
|
return ah_authored_process_i16(
|
||||||
|
engine,
|
||||||
|
&input,
|
||||||
|
&outputFrames,
|
||||||
|
outputCapacity,
|
||||||
|
&outputCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard status == AH_STATUS_OK || status == AH_STATUS_OUTPUT_AVAILABLE else {
|
||||||
|
NSLog("DualSense haptics: authored conversion failed with status %d", status)
|
||||||
|
ah_authored_reset(engine)
|
||||||
|
renderDualSenseHaptics(controllerNumber: chunk.controllerNumber, left: nil, right: nil, delay: 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalRenderDelay = 0.0
|
||||||
|
for index in 0..<Int(outputCount) {
|
||||||
|
let frame = outputFrames[index]
|
||||||
|
let silent = (frame.flags & UInt32(AH_AUTHORED_FRAME_SILENT)) != 0
|
||||||
|
let resetTimeline = isStreamStart || isDiscontinuity ||
|
||||||
|
(frame.flags & UInt32(AH_AUTHORED_FRAME_DISCONTINUITY)) != 0
|
||||||
|
let delay = state.renderDelay(for: frame.timestamp_us, resetTimeline: resetTimeline && index == 0)
|
||||||
|
finalRenderDelay = max(finalRenderDelay, delay)
|
||||||
|
renderDualSenseHaptics(
|
||||||
|
controllerNumber: chunk.controllerNumber,
|
||||||
|
left: silent ? nil : frame.lanes.0,
|
||||||
|
right: silent ? nil : frame.lanes.1,
|
||||||
|
delay: delay
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isStreamEnd {
|
||||||
|
renderDualSenseHaptics(
|
||||||
|
controllerNumber: chunk.controllerNumber,
|
||||||
|
left: nil,
|
||||||
|
right: nil,
|
||||||
|
delay: finalRenderDelay + 0.005
|
||||||
|
)
|
||||||
|
dualSenseAuthoredEngines.removeValue(forKey: chunk.controllerNumber)
|
||||||
|
NSLog("DualSense haptics: stream ended for controller %u", chunk.controllerNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func renderDualSenseHaptics(
|
||||||
|
controllerNumber: UInt16,
|
||||||
|
left: AhAuthoredLaneFrame?,
|
||||||
|
right: AhAuthoredLaneFrame?,
|
||||||
|
delay: TimeInterval
|
||||||
|
) {
|
||||||
|
guard let controllerSupport = ControllerSupport.sharedInstance() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let leftAmplitude = min(max(left?.rms_amplitude ?? 0, 0), 1)
|
||||||
|
let rightAmplitude = min(max(right?.rms_amplitude ?? 0, 0), 1)
|
||||||
|
let leftSharpness = min(max(1 - (left?.low_band_ratio ?? 1), 0), 1)
|
||||||
|
let rightSharpness = min(max(1 - (right?.low_band_ratio ?? 1), 0), 1)
|
||||||
|
let leftTransient = min(max(left?.transient_strength ?? 0, 0), 1)
|
||||||
|
let rightTransient = min(max(right?.transient_strength ?? 0, 0), 1)
|
||||||
|
|
||||||
|
controllerSupport.renderDualSenseHaptics(
|
||||||
|
controllerNumber,
|
||||||
|
leftAmplitude: leftAmplitude,
|
||||||
|
leftSharpness: leftSharpness,
|
||||||
|
leftTransient: leftTransient,
|
||||||
|
rightAmplitude: rightAmplitude,
|
||||||
|
rightSharpness: rightSharpness,
|
||||||
|
rightTransient: rightTransient,
|
||||||
|
delaySeconds: delay
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static private let stickMaxOffset:CGFloat = 0x7FFE
|
static private let stickMaxOffset:CGFloat = 0x7FFE
|
||||||
@objc static var navigationActionTriggered:Bool = false
|
@objc static var navigationActionTriggered:Bool = false
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
@interface HapticContext : NSObject
|
@interface HapticContext : NSObject
|
||||||
|
|
||||||
-(void)setMotorAmplitude:(unsigned short)amplitude;
|
-(void)setMotorAmplitude:(unsigned short)amplitude;
|
||||||
|
-(void)setAuthoredAmplitude:(float)amplitude
|
||||||
|
sharpness:(float)sharpness
|
||||||
|
transientStrength:(float)transientStrength;
|
||||||
-(void)cleanup;
|
-(void)cleanup;
|
||||||
|
|
||||||
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
|
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
|
||||||
|
|||||||
@@ -87,6 +87,93 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-(void)setAuthoredAmplitude:(float)amplitude
|
||||||
|
sharpness:(float)sharpness
|
||||||
|
transientStrength:(float)transientStrength API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||||
|
if (_hapticEngine == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
amplitude = fmaxf(0.0f, fminf(1.0f, amplitude));
|
||||||
|
sharpness = fmaxf(0.0f, fminf(1.0f, sharpness));
|
||||||
|
transientStrength = fmaxf(0.0f, fminf(1.0f, transientStrength));
|
||||||
|
|
||||||
|
NSError* error = nil;
|
||||||
|
if (amplitude == 0.0f) {
|
||||||
|
if (_playing) {
|
||||||
|
[_hapticPlayer stopAtTime:CHHapticTimeImmediate error:&error];
|
||||||
|
_playing = NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (_hapticPlayer == nil) {
|
||||||
|
CHHapticEventParameter* intensity = [[CHHapticEventParameter alloc]
|
||||||
|
initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
|
||||||
|
CHHapticEventParameter* eventSharpness = [[CHHapticEventParameter alloc]
|
||||||
|
initWithParameterID:CHHapticEventParameterIDHapticSharpness value:0.5f];
|
||||||
|
CHHapticEvent* event = [[CHHapticEvent alloc]
|
||||||
|
initWithEventType:CHHapticEventTypeHapticContinuous
|
||||||
|
parameters:@[intensity, eventSharpness]
|
||||||
|
relativeTime:0
|
||||||
|
duration:GCHapticDurationInfinite];
|
||||||
|
CHHapticPattern* pattern = [[CHHapticPattern alloc]
|
||||||
|
initWithEvents:@[event] parameters:@[] error:&error];
|
||||||
|
if (pattern == nil || error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: Authored haptic pattern creation failed: %@", _playerIndex, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_hapticPlayer = [_hapticEngine createPlayerWithPattern:pattern error:&error];
|
||||||
|
if (_hapticPlayer == nil || error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: Authored haptic player creation failed: %@", _playerIndex, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray* parameters = @[
|
||||||
|
[[CHHapticDynamicParameter alloc]
|
||||||
|
initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl
|
||||||
|
value:amplitude relativeTime:0],
|
||||||
|
[[CHHapticDynamicParameter alloc]
|
||||||
|
initWithParameterID:CHHapticDynamicParameterIDHapticSharpnessControl
|
||||||
|
value:sharpness relativeTime:0]
|
||||||
|
];
|
||||||
|
[_hapticPlayer 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 (error != nil) {
|
||||||
|
_hapticPlayer = nil;
|
||||||
|
Log(LOG_W, @"Controller %d: Authored haptic playback start failed: %@", _playerIndex, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_playing = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit one-shot attacks only when the analyzer found a meaningful transient.
|
||||||
|
if (transientStrength >= 0.05f) {
|
||||||
|
CHHapticEventParameter* intensity = [[CHHapticEventParameter alloc]
|
||||||
|
initWithParameterID:CHHapticEventParameterIDHapticIntensity value:transientStrength];
|
||||||
|
CHHapticEventParameter* eventSharpness = [[CHHapticEventParameter alloc]
|
||||||
|
initWithParameterID:CHHapticEventParameterIDHapticSharpness value:sharpness];
|
||||||
|
CHHapticEvent* event = [[CHHapticEvent alloc]
|
||||||
|
initWithEventType:CHHapticEventTypeHapticTransient
|
||||||
|
parameters:@[intensity, eventSharpness]
|
||||||
|
relativeTime:0];
|
||||||
|
CHHapticPattern* pattern = [[CHHapticPattern alloc]
|
||||||
|
initWithEvents:@[event] parameters:@[] error:&error];
|
||||||
|
id<CHHapticPatternPlayer> transientPlayer = pattern == nil ? nil :
|
||||||
|
[_hapticEngine createPlayerWithPattern:pattern error:&error];
|
||||||
|
[transientPlayer startAtTime:CHHapticTimeImmediate error:&error];
|
||||||
|
if (error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: Authored haptic transient failed: %@", _playerIndex, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
-(id) initDeviceEngineContextWithGamepad:(GCController*)gamepad API_AVAILABLE(ios(13.0), tvos(13.0)) {
|
-(id) initDeviceEngineContextWithGamepad:(GCController*)gamepad API_AVAILABLE(ios(13.0), tvos(13.0)) {
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
_hapticEngine = [[CHHapticEngine alloc] initAndReturnError:&error];
|
_hapticEngine = [[CHHapticEngine alloc] initAndReturnError:&error];
|
||||||
|
|||||||
@@ -588,6 +588,22 @@ void ClSetAdaptiveTriggers(uint16_t controllerNumber, uint8_t eventFlags,
|
|||||||
right:right];
|
right:right];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClDs5HapticsPcm(const LI_DS5_HAPTICS_PCM_FRAME* frame)
|
||||||
|
{
|
||||||
|
if (frame == NULL || frame->pcmData == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// common-c owns pcmData only for this callback, so copy before returning.
|
||||||
|
NSData* pcmData = [NSData dataWithBytes:frame->pcmData length:frame->pcmDataLength];
|
||||||
|
[ControllerUtil enqueueDualSenseHapticsPCMWithControllerNumber:frame->controllerNumber
|
||||||
|
flags:frame->flags
|
||||||
|
sequenceNumber:frame->sequenceNumber
|
||||||
|
presentationTimeUs:frame->presentationTimeUs
|
||||||
|
frameCount:frame->frameCount
|
||||||
|
pcmData:pcmData];
|
||||||
|
}
|
||||||
|
|
||||||
-(void) terminate
|
-(void) terminate
|
||||||
{
|
{
|
||||||
// Interrupt any action blocking LiStartConnection(). This is
|
// Interrupt any action blocking LiStartConnection(). This is
|
||||||
@@ -597,6 +613,7 @@ void ClSetAdaptiveTriggers(uint16_t controllerNumber, uint8_t eventFlags,
|
|||||||
LiInterruptConnection();
|
LiInterruptConnection();
|
||||||
[audioPlayerNode stop];
|
[audioPlayerNode stop];
|
||||||
[audioEngine stop];
|
[audioEngine stop];
|
||||||
|
[ControllerUtil stopAllDualSenseHaptics];
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
|
|
||||||
// We dispatch this async to get out because this can be invoked
|
// We dispatch this async to get out because this can be invoked
|
||||||
@@ -750,6 +767,7 @@ void ClSetAdaptiveTriggers(uint16_t controllerNumber, uint8_t eventFlags,
|
|||||||
_clCallbacks.setMotionEventState = ClSetMotionEventState;
|
_clCallbacks.setMotionEventState = ClSetMotionEventState;
|
||||||
_clCallbacks.setControllerLED = ClSetControllerLED;
|
_clCallbacks.setControllerLED = ClSetControllerLED;
|
||||||
_clCallbacks.setAdaptiveTriggers = ClSetAdaptiveTriggers;
|
_clCallbacks.setAdaptiveTriggers = ClSetAdaptiveTriggers;
|
||||||
|
_clCallbacks.ds5HapticsPcm = ClDs5HapticsPcm;
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
selector:@selector(handleAudioSessionInterruption:)
|
selector:@selector(handleAudioSessionInterruption:)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
#include <moonlight_haptics/authored_haptics.h>
|
||||||
#import "SettingsViewController.h"
|
#import "SettingsViewController.h"
|
||||||
#import "MainFrameViewController.h"
|
#import "MainFrameViewController.h"
|
||||||
#import "LoadingFrameViewController.h"
|
#import "LoadingFrameViewController.h"
|
||||||
|
|||||||
Reference in New Issue
Block a user