dualSense haptic rumbling by qin2333's PCM convertor

This commit is contained in:
True Zhuanjia
2026-08-16 18:03:50 +08:00
parent 9da1121cdb
commit 4f0be34947
10 changed files with 455 additions and 7 deletions
+3
View File
@@ -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
+13 -3
View File
@@ -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",
+3 -3
View File
@@ -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()
+10 -1
View File
@@ -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;
+59
View File
@@ -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;
}
+258
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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];
+18
View File
@@ -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:)
+1
View File
@@ -3,6 +3,7 @@
//
#include <Limelight.h>
#include <moonlight_haptics/authored_haptics.h>
#import "SettingsViewController.h"
#import "MainFrameViewController.h"
#import "LoadingFrameViewController.h"