mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
iOS: put the audio and pacing numbers on the number row
First screens onto the shared control. Emulator Volume, Buffer Size,
Output Latency, Fast Forward Volume, Queue Size and FPS Target all become
the same row, and IntSliderRow goes since the new one covers what it did.
Three things these rows gain. Bounds now come from the range constant
rather than being written out again as text, so a Text("150%") can no
longer disagree with the slider it sits under. Every row can be typed
into. And they all carry a VoiceOver label and value, where only the
volume slider did.
Queue Size keeps its stepper. Fifteen values and you usually want a
specific one.
Emulator volume gets a named range like the other numeric settings, and
the clamp helper now reads from it rather than repeating 0 and 150.
This commit is contained in:
@@ -5,6 +5,6 @@ import Foundation
|
||||
|
||||
extension SettingsStore {
|
||||
static func clampedEmulatorVolumePercent(_ value: Int) -> Int {
|
||||
min(max(value, 0), 150)
|
||||
clamped(value, to: emulatorVolumeRange)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ final class SettingsStore {
|
||||
static let audioBufferMsRange = 10...200
|
||||
static let audioOutputLatencyMsRange = 5...200
|
||||
static let fastForwardVolumeRange = 0...200
|
||||
static let emulatorVolumeRange = 0...150
|
||||
static let shadeBoostRange = 1...100
|
||||
static let casSharpnessRange = 0...100
|
||||
static let targetFPSRange = Int(minTargetFPS)...Int(maxTargetFPS)
|
||||
|
||||
@@ -9,40 +9,11 @@ struct AudioSettingsView: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text(settings.localized("Emulator Volume"))
|
||||
Spacer()
|
||||
Text(Self.formatPercent(settings.emulatorVolumePercent))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.callout.monospacedDigit())
|
||||
}
|
||||
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(settings.emulatorVolumePercent) },
|
||||
set: { settings.emulatorVolumePercent = Int($0.rounded()) }
|
||||
),
|
||||
in: 0...150,
|
||||
step: 1
|
||||
)
|
||||
.accessibilityLabel(settings.localized("Emulator Volume"))
|
||||
.accessibilityValue(Self.formatPercent(settings.emulatorVolumePercent))
|
||||
.accessibilityHint(settings.localized("Adjusts emulator game audio without changing iOS system volume or other apps."))
|
||||
|
||||
HStack {
|
||||
Text("0%")
|
||||
Spacer()
|
||||
Button(settings.localized("Reset")) {
|
||||
settings.emulatorVolumePercent = SettingsStore.defaultEmulatorVolumePercent
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
Spacer()
|
||||
Text("150%")
|
||||
}
|
||||
.font(.caption.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
NumberRow("Emulator Volume", value: $settings.emulatorVolumePercent,
|
||||
in: SettingsStore.emulatorVolumeRange, format: .percent,
|
||||
default: SettingsStore.defaultEmulatorVolumePercent,
|
||||
hint: "Adjusts emulator game audio without changing iOS system volume or other apps.",
|
||||
settings: settings)
|
||||
|
||||
Text(settings.localized("Controls emulator and game audio only. iOS system volume and other apps stay separate."))
|
||||
.font(.caption)
|
||||
@@ -57,9 +28,15 @@ struct AudioSettingsView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
IntSliderRow("Buffer Size", value: $settings.audioBufferMs, range: SettingsStore.audioBufferMsRange, suffix: " ms", defaultValue: 50, settings: settings)
|
||||
IntSliderRow("Output Latency", value: $settings.audioOutputLatencyMs, range: SettingsStore.audioOutputLatencyMsRange, suffix: " ms", defaultValue: 20, settings: settings)
|
||||
IntSliderRow("Fast-Forward Volume", value: $settings.audioFastForwardVolume, range: SettingsStore.fastForwardVolumeRange, suffix: "%", defaultValue: 100, settings: settings)
|
||||
NumberRow("Buffer Size", value: $settings.audioBufferMs,
|
||||
in: SettingsStore.audioBufferMsRange, format: .milliseconds,
|
||||
default: 50, settings: settings)
|
||||
NumberRow("Output Latency", value: $settings.audioOutputLatencyMs,
|
||||
in: SettingsStore.audioOutputLatencyMsRange, format: .milliseconds,
|
||||
default: 20, settings: settings)
|
||||
NumberRow("Fast-Forward Volume", value: $settings.audioFastForwardVolume,
|
||||
in: SettingsStore.fastForwardVolumeRange, format: .percent,
|
||||
default: 100, settings: settings)
|
||||
|
||||
Text(settings.localized("Lower buffer or latency reduces lag but can cause crackling. Fast-forward volume is a percentage of normal volume used while fast-forwarding."))
|
||||
.font(.caption)
|
||||
@@ -82,8 +59,4 @@ struct AudioSettingsView: View {
|
||||
.navigationTitle(settings.localized("Audio"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private static func formatPercent(_ value: Int) -> String {
|
||||
"\(SettingsStore.clampedEmulatorVolumePercent(value))%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,24 +29,22 @@ struct FramePacingSettingsView: View {
|
||||
Section {
|
||||
frameLimiterRows
|
||||
|
||||
Stepper("\(settings.localized("Queue Size")): \(settings.vsyncQueueSize)",
|
||||
value: $settings.vsyncQueueSize,
|
||||
in: SettingsStore.vsyncQueueRange)
|
||||
NumberRow("Queue Size", value: $settings.vsyncQueueSize,
|
||||
in: SettingsStore.vsyncQueueRange, style: .stepper,
|
||||
settings: settings)
|
||||
|
||||
Toggle(settings.localized("Sync to Host Refresh"), isOn: $settings.syncToHostRefresh)
|
||||
Text(settings.localized("Sync to Host Refresh needs a restart to take effect."))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// Same two keys the Audio screen shows, so use the same control. Stepping
|
||||
// from 10 to 200 a tap at a time was never a serious way to set these.
|
||||
IntSliderRow("Buffer Size", value: $settings.audioBufferMs,
|
||||
range: SettingsStore.audioBufferMsRange,
|
||||
suffix: " ms", defaultValue: 50, settings: settings)
|
||||
NumberRow("Buffer Size", value: $settings.audioBufferMs,
|
||||
in: SettingsStore.audioBufferMsRange, format: .milliseconds,
|
||||
default: 50, settings: settings)
|
||||
|
||||
IntSliderRow("Output Latency", value: $settings.audioOutputLatencyMs,
|
||||
range: SettingsStore.audioOutputLatencyMsRange,
|
||||
suffix: " ms", defaultValue: 20, settings: settings)
|
||||
NumberRow("Output Latency", value: $settings.audioOutputLatencyMs,
|
||||
in: SettingsStore.audioOutputLatencyMsRange, format: .milliseconds,
|
||||
default: 20, settings: settings)
|
||||
} header: {
|
||||
Text(settings.localized("Individual Settings"))
|
||||
}
|
||||
@@ -97,25 +95,14 @@ struct FramePacingSettingsView: View {
|
||||
|
||||
if settings.frameLimiterEnabled {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Text(settings.localized("FPS Target"))
|
||||
Spacer()
|
||||
Text(Self.formatFPS(settings.targetFPS))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.callout.monospacedDigit())
|
||||
}
|
||||
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { settings.targetFPS },
|
||||
set: { value in
|
||||
settings.targetFPS = value
|
||||
enforceHardcoreSpeedFloorIfNeeded()
|
||||
}
|
||||
),
|
||||
in: SettingsStore.minTargetFPS...SettingsStore.maxTargetFPS,
|
||||
step: 1.0
|
||||
)
|
||||
NumberRow("FPS Target", value: Binding(
|
||||
get: { settings.targetFPS },
|
||||
set: { value in
|
||||
settings.targetFPS = value
|
||||
enforceHardcoreSpeedFloorIfNeeded()
|
||||
}
|
||||
), in: SettingsStore.minTargetFPS...SettingsStore.maxTargetFPS,
|
||||
format: .framesPerSecond, step: 1, settings: settings)
|
||||
|
||||
HStack {
|
||||
quickTargetButton(30)
|
||||
@@ -189,10 +176,6 @@ struct FramePacingSettingsView: View {
|
||||
.font(.caption.monospacedDigit())
|
||||
}
|
||||
|
||||
private static func formatFPS(_ value: Float) -> String {
|
||||
String(format: "%.0f FPS", value)
|
||||
}
|
||||
|
||||
private static func formatCompactFPS(_ value: Float) -> String {
|
||||
String(format: "%.0f", value)
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
// IntSliderRow.swift — Labeled integer slider used by the global settings screens.
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Slider with the value in the header, the bounds underneath, and a reset in the middle.
|
||||
/// Lived in AudioSettingsView until Frame Pacing needed the same rows for the same keys.
|
||||
struct IntSliderRow: View {
|
||||
let title: String
|
||||
@Binding var value: Int
|
||||
let range: ClosedRange<Int>
|
||||
var suffix: String = ""
|
||||
let defaultValue: Int
|
||||
let settings: SettingsStore
|
||||
|
||||
init(_ title: String, value: Binding<Int>, range: ClosedRange<Int>,
|
||||
suffix: String = "", defaultValue: Int, settings: SettingsStore) {
|
||||
self.title = title
|
||||
self._value = value
|
||||
self.range = range
|
||||
self.suffix = suffix
|
||||
self.defaultValue = defaultValue
|
||||
self.settings = settings
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text(settings.localized(title))
|
||||
Spacer()
|
||||
Text(formatted(value))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.callout.monospacedDigit())
|
||||
}
|
||||
Slider(value: Binding(
|
||||
get: { Double(value) },
|
||||
set: { value = Int($0.rounded()) }
|
||||
), in: Double(range.lowerBound)...Double(range.upperBound))
|
||||
HStack {
|
||||
Text(formatted(range.lowerBound))
|
||||
Spacer()
|
||||
Button(settings.localized("Reset")) { value = defaultValue }
|
||||
.buttonStyle(.borderless)
|
||||
Spacer()
|
||||
Text(formatted(range.upperBound))
|
||||
}
|
||||
.font(.caption.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private func formatted(_ value: Int) -> String {
|
||||
"\(value)\(suffix)"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user