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"]
|
||||
path = third_party/imgui
|
||||
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 = {
|
||||
|
||||
/* 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 */; };
|
||||
180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */; };
|
||||
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1817B61D2E6FD04D000D938D /* SafeTimer.swift */; };
|
||||
@@ -187,6 +188,7 @@
|
||||
/* End PBXContainerItemProxy 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>"; };
|
||||
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>"; };
|
||||
@@ -1139,6 +1141,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A1D500022FFC000100000001 /* authored_haptics_engine.cpp in Sources */,
|
||||
FB290D0719B2C406004C83CF /* Limelight.xcdatamodeld in Sources */,
|
||||
9819CC22254F180F008A7C8E /* AbsoluteTouchHandler.m in Sources */,
|
||||
9819CC14254F107A008A7C8E /* RelativeTouchHandler.m in Sources */,
|
||||
@@ -1345,7 +1348,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.trzj.VoidLinkPreview;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG VOIDLINK_PREVIEW $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -1395,6 +1398,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.trzj.VoidLinkPreview;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "VOIDLINK_PREVIEW $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
@@ -1529,10 +1533,13 @@
|
||||
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/third_party/moonlight-audio-haptics/include",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
|
||||
"$(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/ViewControllers/CustomOSCViewControl",
|
||||
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
|
||||
@@ -1597,10 +1604,13 @@
|
||||
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/third_party/moonlight-audio-haptics/include",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
|
||||
"$(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/ViewControllers/CustomOSCViewControl",
|
||||
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
|
||||
|
||||
@@ -360,7 +360,7 @@ final class ControllerNavigator: NSObject {
|
||||
guard uiNavigationDelegate === delegate else { return }
|
||||
/*
|
||||
if previousUINavigationDelegate is StreamFrameViewController {
|
||||
ControllerSupport.sharedInstance().reinitiatePrimaryController()
|
||||
ControllerSupport.sharedInstance()?.reinitiatePrimaryController()
|
||||
return
|
||||
} */
|
||||
|
||||
@@ -378,7 +378,7 @@ final class ControllerNavigator: NSObject {
|
||||
// GamepadNavigationIllustrationHud.updateNavigationElements(controllerMouseNavigationElements)
|
||||
return
|
||||
}
|
||||
ControllerSupport.sharedInstance().reinitiatePrimaryController()
|
||||
ControllerSupport.sharedInstance()?.reinitiatePrimaryController()
|
||||
GamepadNavigationIllustrationHud.updateHud()
|
||||
return;
|
||||
}
|
||||
@@ -769,7 +769,7 @@ final class ControllerNavigator: NSObject {
|
||||
radialMenuDelayTimer?.clean()
|
||||
stickReleasedInRadialMenu = false
|
||||
if radialMenuView == nil, uiNavigationDelegate is StreamFrameViewController {
|
||||
ControllerSupport.sharedInstance().sendNavigationButtonPress()
|
||||
ControllerSupport.sharedInstance()?.sendNavigationButtonPress()
|
||||
return
|
||||
}
|
||||
radialMenuView?.dismiss()
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
@property (readonly) bool shallDisableGyroHotSwitch;
|
||||
|
||||
+(ControllerSupport*) sharedInstance;
|
||||
+(nullable ControllerSupport*) sharedInstance;
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
||||
-(void) connectionEstablished;
|
||||
@@ -55,6 +55,15 @@
|
||||
-(void) setAdaptiveTriggers:(uint16_t)controllerNumber eventFlags:(uint8_t)eventFlags
|
||||
typeLeft:(uint8_t)typeLeft typeRight:(uint8_t)typeRight
|
||||
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;
|
||||
|
||||
-(uint16_t) getActiveGamepadMask;
|
||||
|
||||
@@ -139,6 +139,8 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger,
|
||||
bool _controllerGyroSwitchHoldPressed;
|
||||
ControllerGyroSwitchMode _gyroSwitchMode;
|
||||
|
||||
uint64_t _authoredHapticsGeneration;
|
||||
|
||||
__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
|
||||
{
|
||||
@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
|
||||
if (controller.haptics) {
|
||||
@@ -2063,6 +2120,8 @@ double rc_expo(double x, double expo) {
|
||||
|
||||
-(void) cleanup
|
||||
{
|
||||
[ControllerUtil stopAllDualSenseHaptics];
|
||||
|
||||
if (VLSharedControllerSupport == self) {
|
||||
VLSharedControllerSupport = nil;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import GameController
|
||||
import Combine
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
#if !VOIDLINK_PREVIEW
|
||||
import Collections
|
||||
#endif
|
||||
|
||||
@objc enum ControllerElementType: Int {
|
||||
case button
|
||||
@@ -260,6 +263,261 @@ import Foundation
|
||||
|
||||
@objc class ControllerUtil: NSObject {
|
||||
@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
|
||||
@objc static var navigationActionTriggered:Bool = false
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
@interface HapticContext : NSObject
|
||||
|
||||
-(void)setMotorAmplitude:(unsigned short)amplitude;
|
||||
-(void)setAuthoredAmplitude:(float)amplitude
|
||||
sharpness:(float)sharpness
|
||||
transientStrength:(float)transientStrength;
|
||||
-(void)cleanup;
|
||||
|
||||
+(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)) {
|
||||
NSError *error = nil;
|
||||
_hapticEngine = [[CHHapticEngine alloc] initAndReturnError:&error];
|
||||
|
||||
@@ -588,6 +588,22 @@ void ClSetAdaptiveTriggers(uint16_t controllerNumber, uint8_t eventFlags,
|
||||
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
|
||||
{
|
||||
// Interrupt any action blocking LiStartConnection(). This is
|
||||
@@ -597,6 +613,7 @@ void ClSetAdaptiveTriggers(uint16_t controllerNumber, uint8_t eventFlags,
|
||||
LiInterruptConnection();
|
||||
[audioPlayerNode stop];
|
||||
[audioEngine stop];
|
||||
[ControllerUtil stopAllDualSenseHaptics];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
|
||||
// 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.setControllerLED = ClSetControllerLED;
|
||||
_clCallbacks.setAdaptiveTriggers = ClSetAdaptiveTriggers;
|
||||
_clCallbacks.ds5HapticsPcm = ClDs5HapticsPcm;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleAudioSessionInterruption:)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <moonlight_haptics/authored_haptics.h>
|
||||
#import "SettingsViewController.h"
|
||||
#import "MainFrameViewController.h"
|
||||
#import "LoadingFrameViewController.h"
|
||||
|
||||
Reference in New Issue
Block a user