mirror of
https://github.com/encounter/aurora.git
synced 2026-07-09 18:19:33 -07:00
183 lines
5.2 KiB
Plaintext
183 lines
5.2 KiB
Plaintext
#include "device.hpp"
|
|
|
|
#import <CoreHaptics/CoreHaptics.h>
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
|
|
@interface AuroraDeviceHaptics : NSObject
|
|
{
|
|
std::atomic<uint64_t> _generation;
|
|
}
|
|
@property(nonatomic, strong) CHHapticEngine* engine;
|
|
@property(nonatomic, strong) id<CHHapticAdvancedPatternPlayer> player;
|
|
@property(nonatomic) BOOL active;
|
|
- (BOOL)available;
|
|
- (void)rumbleLow:(uint16_t)low high:(uint16_t)high duration:(uint16_t)durationMs;
|
|
- (void)shutdown;
|
|
@end
|
|
|
|
@implementation AuroraDeviceHaptics
|
|
|
|
- (instancetype)init {
|
|
self = [super init];
|
|
if (self != nil) {
|
|
_generation.store(0);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (BOOL)available {
|
|
return CHHapticEngine.capabilitiesForHardware.supportsHaptics;
|
|
}
|
|
|
|
- (BOOL)ensurePlayer {
|
|
if (![self available]) {
|
|
return NO;
|
|
}
|
|
|
|
NSError* error = nil;
|
|
if (self.engine == nil) {
|
|
self.engine = [[CHHapticEngine alloc] initAndReturnError:&error];
|
|
if (error != nil || self.engine == nil) {
|
|
return NO;
|
|
}
|
|
|
|
__weak AuroraDeviceHaptics* weakSelf = self;
|
|
self.engine.stoppedHandler = ^(CHHapticEngineStoppedReason) {
|
|
AuroraDeviceHaptics* strongSelf = weakSelf;
|
|
if (strongSelf == nil) {
|
|
return;
|
|
}
|
|
strongSelf.player = nil;
|
|
strongSelf.engine = nil;
|
|
strongSelf.active = NO;
|
|
};
|
|
self.engine.resetHandler = ^{
|
|
AuroraDeviceHaptics* strongSelf = weakSelf;
|
|
if (strongSelf == nil) {
|
|
return;
|
|
}
|
|
strongSelf.player = nil;
|
|
strongSelf.active = NO;
|
|
[strongSelf.engine startAndReturnError:nil];
|
|
};
|
|
|
|
[self.engine startAndReturnError:&error];
|
|
if (error != nil) {
|
|
self.engine = nil;
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
if (self.player == nil) {
|
|
CHHapticEventParameter* intensity =
|
|
[[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
|
|
CHHapticEvent* event = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous
|
|
parameters:@[ intensity ]
|
|
relativeTime:0
|
|
duration:1.0];
|
|
CHHapticPattern* pattern = [[CHHapticPattern alloc] initWithEvents:@[ event ] parameters:@[] error:&error];
|
|
if (error != nil || pattern == nil) {
|
|
return NO;
|
|
}
|
|
|
|
self.player = [self.engine createAdvancedPlayerWithPattern:pattern error:&error];
|
|
if (error != nil || self.player == nil) {
|
|
return NO;
|
|
}
|
|
self.player.loopEnabled = YES;
|
|
self.player.loopEnd = 1.0;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)stop {
|
|
++_generation;
|
|
if (self.player != nil && self.active) {
|
|
[self.player stopAtTime:CHHapticTimeImmediate error:nil];
|
|
}
|
|
self.active = NO;
|
|
}
|
|
|
|
- (void)rumbleLow:(uint16_t)low high:(uint16_t)high duration:(uint16_t)durationMs {
|
|
const float lowValue = static_cast<float>(low) / 65535.0f;
|
|
const float highValue = static_cast<float>(high) / 65535.0f;
|
|
const float intensity = std::clamp(lowValue * 0.6f + highValue * 0.4f, 0.0f, 1.0f);
|
|
if (intensity <= 0.0f) {
|
|
[self stop];
|
|
return;
|
|
}
|
|
|
|
if (![self ensurePlayer]) {
|
|
return;
|
|
}
|
|
|
|
const float sharpness = std::clamp(highValue * 0.75f + lowValue * 0.25f, 0.0f, 1.0f);
|
|
CHHapticDynamicParameter* intensityParam =
|
|
[[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl
|
|
value:intensity
|
|
relativeTime:0];
|
|
CHHapticDynamicParameter* sharpnessParam =
|
|
[[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticSharpnessControl
|
|
value:sharpness
|
|
relativeTime:0];
|
|
if (![self.player sendParameters:@[ intensityParam, sharpnessParam ] atTime:CHHapticTimeImmediate error:nil]) {
|
|
return;
|
|
}
|
|
|
|
if (!self.active) {
|
|
if (![self.player startAtTime:CHHapticTimeImmediate error:nil]) {
|
|
return;
|
|
}
|
|
self.active = YES;
|
|
}
|
|
|
|
const uint64_t generation = ++_generation;
|
|
if (durationMs != 0) {
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, static_cast<int64_t>(durationMs) * NSEC_PER_MSEC),
|
|
dispatch_get_main_queue(), ^{
|
|
if (_generation.load() == generation) {
|
|
[self stop];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
- (void)shutdown {
|
|
[self stop];
|
|
if (self.player != nil) {
|
|
[self.player cancelAndReturnError:nil];
|
|
self.player = nil;
|
|
}
|
|
if (self.engine != nil) {
|
|
[self.engine stopWithCompletionHandler:nil];
|
|
self.engine = nil;
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
namespace aurora::device {
|
|
namespace {
|
|
AuroraDeviceHaptics* haptics() {
|
|
static AuroraDeviceHaptics* s_haptics = [[AuroraDeviceHaptics alloc] init];
|
|
return s_haptics;
|
|
}
|
|
} // namespace
|
|
|
|
bool rumble_available() noexcept { return [haptics() available]; }
|
|
|
|
void rumble(const uint16_t loqFreq, const uint16_t highFreq, const uint16_t durationMs) noexcept {
|
|
[haptics() rumbleLow:loqFreq high:highFreq duration:durationMs];
|
|
}
|
|
|
|
namespace detail {
|
|
void shutdown_rumble() noexcept {
|
|
[haptics() shutdown];
|
|
}
|
|
} // namespace detail
|
|
} // namespace aurora::device
|