EQ tmp fixes
This commit is contained in:
1 parent
f1a4a519c3
commit
cacf2b1951
3 files changed
+338
-12
No files matched your search
@@ -14,6 +14,10 @@ import Foundation
|
|||||||
#if !VOIDLINK_PREVIEW
|
#if !VOIDLINK_PREVIEW
|
||||||
import Collections
|
import Collections
|
||||||
#endif
|
#endif
|
||||||
|
#if os(iOS) && !VOIDLINK_PREVIEW
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
@objc enum ControllerElementType: Int {
|
@objc enum ControllerElementType: Int {
|
||||||
case button
|
case button
|
||||||
@@ -311,11 +315,108 @@ import Collections
|
|||||||
qos: .userInteractive
|
qos: .userInteractive
|
||||||
)
|
)
|
||||||
private static let maxQueuedDualSensePCMChunks = 8
|
private static let maxQueuedDualSensePCMChunks = 8
|
||||||
|
private static let dualSenseHapticsEQDefaultsKey = "voidlink.dualsense.haptics.eq.v0"
|
||||||
|
private static let defaultDualSenseHapticsEQ: [Float] = [0.3, 1, 1, 0.3, 1]
|
||||||
|
private static let dualSenseHapticsEQLimits: [ClosedRange<Float>] = [0...1, 0...2, 0...2, 0...1, 0...2]
|
||||||
|
private static let dualSenseHapticsEQLock = NSLock()
|
||||||
|
private static var dualSenseHapticsEQ = loadDualSenseHapticsEQ()
|
||||||
private static var dualSensePCMQueue = Deque<DualSensePCMChunk>()
|
private static var dualSensePCMQueue = Deque<DualSensePCMChunk>()
|
||||||
private static var dualSenseWorkerScheduled = false
|
private static var dualSenseWorkerScheduled = false
|
||||||
private static var dualSenseForcedDiscontinuities = Set<UInt16>()
|
private static var dualSenseForcedDiscontinuities = Set<UInt16>()
|
||||||
private static var dualSenseAuthoredEngines = [UInt16: AuthoredEngineState]()
|
private static var dualSenseAuthoredEngines = [UInt16: AuthoredEngineState]()
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
private static var dualSenseHapticsEQOverlayWindow: UIWindow?
|
||||||
|
private static weak var dualSenseHapticsEQPreviousKeyWindow: UIWindow?
|
||||||
|
private static var didPresentDualSenseHapticsEQ = false
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private static func loadDualSenseHapticsEQ() -> [Float] {
|
||||||
|
guard let storedValues = UserDefaults.standard.array(forKey: dualSenseHapticsEQDefaultsKey),
|
||||||
|
storedValues.count == defaultDualSenseHapticsEQ.count else {
|
||||||
|
return defaultDualSenseHapticsEQ
|
||||||
|
}
|
||||||
|
|
||||||
|
let values = storedValues.compactMap { ($0 as? NSNumber)?.floatValue }
|
||||||
|
return sanitizedDualSenseHapticsEQ(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func sanitizedDualSenseHapticsEQ(_ values: [Float]) -> [Float] {
|
||||||
|
guard values.count == defaultDualSenseHapticsEQ.count,
|
||||||
|
values.allSatisfy(\.isFinite) else {
|
||||||
|
return defaultDualSenseHapticsEQ
|
||||||
|
}
|
||||||
|
return zip(values, dualSenseHapticsEQLimits).map { value, limits in
|
||||||
|
min(max(value, limits.lowerBound), limits.upperBound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate static func dualSenseHapticsEQSnapshot() -> [Float] {
|
||||||
|
dualSenseHapticsEQLock.lock()
|
||||||
|
let values = dualSenseHapticsEQ
|
||||||
|
dualSenseHapticsEQLock.unlock()
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate static func defaultDualSenseHapticsEQSnapshot() -> [Float] {
|
||||||
|
defaultDualSenseHapticsEQ
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate static func previewDualSenseHapticsEQ(_ values: [Float]) {
|
||||||
|
let sanitizedValues = sanitizedDualSenseHapticsEQ(values)
|
||||||
|
dualSenseHapticsEQLock.lock()
|
||||||
|
dualSenseHapticsEQ = sanitizedValues
|
||||||
|
dualSenseHapticsEQLock.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate static func saveDualSenseHapticsEQ(_ values: [Float]) {
|
||||||
|
let sanitizedValues = sanitizedDualSenseHapticsEQ(values)
|
||||||
|
previewDualSenseHapticsEQ(sanitizedValues)
|
||||||
|
UserDefaults.standard.set(sanitizedValues.map(Double.init), forKey: dualSenseHapticsEQDefaultsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
@objc static func presentDualSenseHapticsEQIfNeeded() {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
guard !didPresentDualSenseHapticsEQ,
|
||||||
|
dualSenseHapticsEQOverlayWindow == nil,
|
||||||
|
let keyWindow = UIApplication.shared.connectedScenes
|
||||||
|
.compactMap({ $0 as? UIWindowScene })
|
||||||
|
.flatMap(\.windows)
|
||||||
|
.first(where: { $0.isKeyWindow }),
|
||||||
|
let windowScene = keyWindow.windowScene else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
didPresentDualSenseHapticsEQ = true
|
||||||
|
dualSenseHapticsEQPreviousKeyWindow = keyWindow
|
||||||
|
|
||||||
|
let store = DualSenseHapticsEQStore(values: dualSenseHapticsEQSnapshot())
|
||||||
|
let rootView = DualSenseHapticsEQView(store: store) {
|
||||||
|
closeDualSenseHapticsEQ()
|
||||||
|
}
|
||||||
|
let hostingController = UIHostingController(rootView: rootView)
|
||||||
|
hostingController.view.backgroundColor = .clear
|
||||||
|
|
||||||
|
let overlayWindow = UIWindow(windowScene: windowScene)
|
||||||
|
overlayWindow.frame = windowScene.screen.bounds
|
||||||
|
overlayWindow.windowLevel = .alert + 1
|
||||||
|
overlayWindow.backgroundColor = .clear
|
||||||
|
overlayWindow.rootViewController = hostingController
|
||||||
|
dualSenseHapticsEQOverlayWindow = overlayWindow
|
||||||
|
overlayWindow.makeKeyAndVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate static func closeDualSenseHapticsEQ() {
|
||||||
|
dualSenseHapticsEQOverlayWindow?.isHidden = true
|
||||||
|
dualSenseHapticsEQOverlayWindow?.rootViewController = nil
|
||||||
|
dualSenseHapticsEQOverlayWindow = nil
|
||||||
|
dualSenseHapticsEQPreviousKeyWindow?.makeKey()
|
||||||
|
dualSenseHapticsEQPreviousKeyWindow = nil
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
@objc(enqueueDualSenseHapticsPCMWithControllerNumber:flags:sequenceNumber:presentationTimeUs:frameCount:pcmData:)
|
@objc(enqueueDualSenseHapticsPCMWithControllerNumber:flags:sequenceNumber:presentationTimeUs:frameCount:pcmData:)
|
||||||
static func enqueueDualSenseHapticsPCM(
|
static func enqueueDualSenseHapticsPCM(
|
||||||
controllerNumber: UInt16,
|
controllerNumber: UInt16,
|
||||||
@@ -499,21 +600,38 @@ import Collections
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let leftAmplitude = min(max(left?.rms_amplitude ?? 0, 0), 1)
|
let eq = dualSenseHapticsEQSnapshot()
|
||||||
let rightAmplitude = min(max(right?.rms_amplitude ?? 0, 0), 1)
|
let masterGain = eq[0]
|
||||||
let leftSharpness = min(max(1 - (left?.low_band_ratio ?? 1), 0), 1)
|
let lowBandGain = eq[1]
|
||||||
let rightSharpness = min(max(1 - (right?.low_band_ratio ?? 1), 0), 1)
|
let highBandGain = eq[2]
|
||||||
let leftTransient = min(max(left?.transient_strength ?? 0, 0), 1)
|
let transientGain = eq[3]
|
||||||
let rightTransient = min(max(right?.transient_strength ?? 0, 0), 1)
|
let sharpnessGain = eq[4]
|
||||||
|
|
||||||
|
func renderValues(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 bandGain = (
|
||||||
|
lowBandRatio * lowBandGain * lowBandGain +
|
||||||
|
highBandRatio * highBandGain * highBandGain
|
||||||
|
).squareRoot()
|
||||||
|
return (
|
||||||
|
min(max((lane?.rms_amplitude ?? 0) * masterGain * bandGain, 0), 1),
|
||||||
|
min(max(highBandRatio * sharpnessGain, 0), 1),
|
||||||
|
min(max((lane?.transient_strength ?? 0) * masterGain * transientGain * bandGain, 0), 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let leftValues = renderValues(for: left)
|
||||||
|
let rightValues = renderValues(for: right)
|
||||||
|
|
||||||
controllerSupport.renderDualSenseHaptics(
|
controllerSupport.renderDualSenseHaptics(
|
||||||
controllerNumber,
|
controllerNumber,
|
||||||
leftAmplitude: leftAmplitude,
|
leftAmplitude: leftValues.amplitude,
|
||||||
leftSharpness: leftSharpness,
|
leftSharpness: leftValues.sharpness,
|
||||||
leftTransient: leftTransient,
|
leftTransient: leftValues.transient,
|
||||||
rightAmplitude: rightAmplitude,
|
rightAmplitude: rightValues.amplitude,
|
||||||
rightSharpness: rightSharpness,
|
rightSharpness: rightValues.sharpness,
|
||||||
rightTransient: rightTransient,
|
rightTransient: rightValues.transient,
|
||||||
delaySeconds: delay
|
delaySeconds: delay
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -857,6 +975,200 @@ import Collections
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS) && !VOIDLINK_PREVIEW
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
private final class DualSenseHapticsEQStore: ObservableObject {
|
||||||
|
let originalValues: [Double]
|
||||||
|
@Published private(set) var values: [Double]
|
||||||
|
|
||||||
|
init(values: [Float]) {
|
||||||
|
let doubles = values.map(Double.init)
|
||||||
|
originalValues = doubles
|
||||||
|
self.values = doubles
|
||||||
|
}
|
||||||
|
|
||||||
|
func binding(for index: Int) -> Binding<Double> {
|
||||||
|
Binding(
|
||||||
|
get: { self.values[index] },
|
||||||
|
set: { self.updateValue($0, at: index) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
values = ControllerUtil.defaultDualSenseHapticsEQSnapshot().map(Double.init)
|
||||||
|
preview()
|
||||||
|
}
|
||||||
|
|
||||||
|
func save() {
|
||||||
|
ControllerUtil.saveDualSenseHapticsEQ(values.map(Float.init))
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
ControllerUtil.previewDualSenseHapticsEQ(originalValues.map(Float.init))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateValue(_ value: Double, at index: Int) {
|
||||||
|
values[index] = value
|
||||||
|
preview()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func preview() {
|
||||||
|
ControllerUtil.previewDualSenseHapticsEQ(values.map(Float.init))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
private struct DualSenseHapticsEQView: View {
|
||||||
|
private struct Parameter: Identifiable {
|
||||||
|
let id: Int
|
||||||
|
let title: String
|
||||||
|
let range: ClosedRange<Double>
|
||||||
|
let step: Double
|
||||||
|
let usesMultiplier: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObservedObject var store: DualSenseHapticsEQStore
|
||||||
|
let onClose: () -> Void
|
||||||
|
|
||||||
|
private let parameters = [
|
||||||
|
Parameter(id: 0, title: "Master Gain", range: 0...1, step: 0.01, usesMultiplier: false),
|
||||||
|
Parameter(id: 1, title: "Low Band (<200 Hz)", range: 0...2, step: 0.01, usesMultiplier: true),
|
||||||
|
Parameter(id: 2, title: "High Band (>200 Hz)", range: 0...2, step: 0.01, usesMultiplier: true),
|
||||||
|
Parameter(id: 3, title: "Transient Gain", range: 0...1, step: 0.01, usesMultiplier: false),
|
||||||
|
Parameter(id: 4, title: "Sharpness", range: 0...2, step: 0.01, usesMultiplier: true),
|
||||||
|
]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack {
|
||||||
|
Color.black.opacity(0.28)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture { cancelAndClose() }
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Text("DualSense Haptic EQ")
|
||||||
|
.font(.system(size: 19, weight: .bold, design: .rounded))
|
||||||
|
.foregroundColor(Color.black.opacity(0.76))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: store.reset) {
|
||||||
|
Image(systemName: "arrow.counterclockwise")
|
||||||
|
.font(.system(size: 15, weight: .semibold))
|
||||||
|
.foregroundColor(Color.black.opacity(0.62))
|
||||||
|
.frame(width: 34, height: 34)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||||
|
.fill(Color.white.opacity(0.9))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.accessibility(label: Text("Reset EQ"))
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
ForEach(parameters) { parameter in
|
||||||
|
parameterRow(parameter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
actionButton(title: "Cancel", isPrimary: false) {
|
||||||
|
cancelAndClose()
|
||||||
|
}
|
||||||
|
actionButton(title: "Save", isPrimary: true) {
|
||||||
|
store.save()
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(UIDevice.current.userInterfaceIdiom == .phone ? 12 : 18)
|
||||||
|
.frame(maxWidth: UIDevice.current.userInterfaceIdiom == .phone ? 380 : 460)
|
||||||
|
.frame(maxHeight: max(280, geometry.size.height - 28))
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: UIDevice.current.userInterfaceIdiom == .phone ? 22 : 28, style: .continuous)
|
||||||
|
.fill(Color(red: 0.94, green: 0.97, blue: 0.98).opacity(0.98))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: UIDevice.current.userInterfaceIdiom == .phone ? 22 : 28, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.96), lineWidth: 1.1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.shadow(color: Color.black.opacity(0.12), radius: 24, x: 0, y: 12)
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func parameterRow(_ parameter: Parameter) -> some View {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Text(parameter.title)
|
||||||
|
.font(.system(size: 12.5, weight: .bold, design: .rounded))
|
||||||
|
.foregroundColor(Color.black.opacity(0.64))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.8)
|
||||||
|
.frame(width: 132, alignment: .leading)
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value: store.binding(for: parameter.id),
|
||||||
|
in: parameter.range,
|
||||||
|
step: parameter.step
|
||||||
|
)
|
||||||
|
.accentColor(Color(red: 0.18, green: 0.61, blue: 0.67))
|
||||||
|
.environment(\.colorScheme, .light)
|
||||||
|
|
||||||
|
Text(formattedValue(store.values[parameter.id], multiplier: parameter.usesMultiplier))
|
||||||
|
.font(.system(size: 12, weight: .semibold, design: .monospaced))
|
||||||
|
.foregroundColor(Color(red: 0.14, green: 0.46, blue: 0.52))
|
||||||
|
.frame(width: 48, alignment: .trailing)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.frame(height: 44)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.fill(Color.white.opacity(0.9))
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.94), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func actionButton(title: String, isPrimary: Bool, action: @escaping () -> Void) -> some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 14, weight: .bold, design: .rounded))
|
||||||
|
.foregroundColor(isPrimary ? .white : Color.black.opacity(0.66))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 38)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.fill(isPrimary
|
||||||
|
? Color(red: 0.16, green: 0.53, blue: 0.59)
|
||||||
|
: Color.white.opacity(0.82))
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(isPrimary ? 0.22 : 0.76), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formattedValue(_ value: Double, multiplier: Bool) -> String {
|
||||||
|
String(format: multiplier ? "x%.2f" : "%.2f", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelAndClose() {
|
||||||
|
store.cancel()
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// MARK: - realtime overlay
|
// MARK: - realtime overlay
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
@available(iOS 13.0, *)
|
||||||
|
|||||||
@@ -7977,6 +7977,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"DualSense Haptic EQ" : {
|
||||||
|
"comment" : "The title of the view that allows users to adjust the haptic feedback intensity of their DualSense controllers.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"dummyPadTip" : {
|
"dummyPadTip" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -15809,6 +15813,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Reset EQ" : {
|
||||||
|
"comment" : "A button label that resets the haptic feedback equalizer to its default settings.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Reset the pen pressure curve by starting from the pen pressure test?" : {
|
"Reset the pen pressure curve by starting from the pen pressure test?" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -2325,6 +2325,12 @@ static NSMutableSet* hostList;
|
|||||||
ControllerNavigator.controllerMouseRightButton = (ControllerElement)settings.controllerMouseRightButton.intValue;
|
ControllerNavigator.controllerMouseRightButton = (ControllerElement)settings.controllerMouseRightButton.intValue;
|
||||||
ControllerNavigator.controllerMouseExpo = settings.controllerMouseExpo.floatValue;
|
ControllerNavigator.controllerMouseExpo = settings.controllerMouseExpo.floatValue;
|
||||||
if(ControllerNavigator.enabled) [ControllerNavigator start];
|
if(ControllerNavigator.enabled) [ControllerNavigator start];
|
||||||
|
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
if (@available(iOS 14.0, *)) {
|
||||||
|
[ControllerUtil presentDualSenseHapticsEQIfNeeded];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user