From dc486214c4813b073b5519f646676758c1c5c875 Mon Sep 17 00:00:00 2001 From: qiin <414382190@qq.com> Date: Fri, 21 Aug 2026 16:02:03 +0800 Subject: [PATCH 1/3] fix(haptics): gate complementary shaping on live USB output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Body band-weighting now applies only in 协同 mode with a USB controller actually rendering; solo-actuator scenarios (Bluetooth-only pad, mid-session USB detach) keep full authored energy instead of dropping pure-high rumble from 100 to 28. - Audio haptics gets the same complementary shaping (sharpness- interpolated 0.72/0.28 gain) instead of double full-energy output in 协同 mode. - Auto-mode body fallback unified to !hasUsbControllers(): Bluetooth/GCK pads expose no rumble backend, so game rumble is no longer silently dropped with them attached (matches the audio path policy from #114). - Rename vibration mode 同时 → 协同 with normalizeVibrationMode migration on all read paths. Co-Authored-By: Claude Sonnet 5 --- entry/src/main/ets/model/StreamConfig.ets | 7 ++- entry/src/main/ets/pages/SettingsPageV2.ets | 10 ++-- .../ets/service/AudioVibrationService.ets | 4 +- .../src/main/ets/service/SettingsService.ets | 13 ++-- .../main/ets/service/input/GamepadManager.ets | 12 +--- .../service/input/GamepadVibrationService.ets | 60 +++++++++++++------ 6 files changed, 65 insertions(+), 41 deletions(-) diff --git a/entry/src/main/ets/model/StreamConfig.ets b/entry/src/main/ets/model/StreamConfig.ets index de97705..f67d384 100644 --- a/entry/src/main/ets/model/StreamConfig.ets +++ b/entry/src/main/ets/model/StreamConfig.ets @@ -27,6 +27,11 @@ export function normalizeGameVibrationStrength(value: number): number { return Math.max(GAME_VIBRATION_STRENGTH_MIN, Math.min(GAME_VIBRATION_STRENGTH_MAX, roundedValue)); } +/** 兼容历史设置值:震动模式「同时」已更名为「协同」。 */ +export function normalizeVibrationMode(mode: string): string { + return mode === '同时' ? '协同' : mode; +} + export interface StreamConfig { // 视频设置 width: number; @@ -61,7 +66,7 @@ export interface StreamConfig { multiController: boolean; attachedGamepadMask: number; enableVibration: boolean; - vibrationMode: string; // 震动模式:自动/仅手柄/仅设备/同时 + vibrationMode: string; // 震动模式:自动/仅手柄/仅设备/协同 gameVibrationStrength: number; // 串流游戏震动强度百分比(100=原始强度,100 以上为 app 侧放大) enableAudioVibration: boolean; // 音频振动(低频驱动) audioVibrationStrength: number; // 音频振动强度 (0-100) diff --git a/entry/src/main/ets/pages/SettingsPageV2.ets b/entry/src/main/ets/pages/SettingsPageV2.ets index 1446a86..ac1653a 100644 --- a/entry/src/main/ets/pages/SettingsPageV2.ets +++ b/entry/src/main/ets/pages/SettingsPageV2.ets @@ -23,7 +23,8 @@ import { GAME_VIBRATION_STRENGTH_MIN, GAME_VIBRATION_STRENGTH_ORIGINAL, GAME_VIBRATION_STRENGTH_STEP, - normalizeGameVibrationStrength + normalizeGameVibrationStrength, + normalizeVibrationMode } from '../model/StreamConfig'; import { PerfLabels } from '../components/PerformanceOverlayManager'; import { SettingSlider, SettingSliderConfig, LogarithmicSlider } from '../components/SettingSlider'; @@ -539,7 +540,8 @@ struct SettingsPageV2 { // 输入 - 手柄设置 this.enableVibration = await this.loadBoolean(SettingsKeys.ENABLE_VIBRATION, true); - this.vibrationMode = await PreferencesUtil.get(SettingsKeys.VIBRATION_MODE, '自动'); + this.vibrationMode = normalizeVibrationMode( + await PreferencesUtil.get(SettingsKeys.VIBRATION_MODE, '自动')); this.gameVibrationStrength = await this.loadGameVibrationStrength(); this.enableAudioVibration = await this.loadBoolean(SettingsKeys.ENABLE_AUDIO_VIBRATION, false); this.audioVibrationStrength = await PreferencesUtil.get(SettingsKeys.AUDIO_VIBRATION_STRENGTH, 60); @@ -1481,7 +1483,7 @@ struct SettingsPageV2 { type: 'segment', value: this.vibrationMode, visible: this.enableVibration, - segmentLabels: ['自动', '仅手柄', '仅设备', '同时'], + segmentLabels: ['自动', '仅手柄', '仅设备', '协同'], onSegmentChange: (value: string) => { this.vibrationMode = value; this.saveSetting(SettingsKeys.VIBRATION_MODE, value); @@ -3683,7 +3685,7 @@ struct SettingsPageV2 { { title: '自动', subtitle: '有手柄用手柄震动,无手柄用设备震动', value: '自动' } as PickerOption, { title: '仅手柄', subtitle: '只使用手柄震动', value: '仅手柄' } as PickerOption, { title: '仅设备', subtitle: '只使用设备震动', value: '仅设备' } as PickerOption, - { title: '同时', subtitle: '手柄和设备同时震动', value: '同时' } as PickerOption + { title: '协同', subtitle: '手柄承载高频细节,机身补充低频量感', value: '协同' } as PickerOption ]; const config: PickerConfig = { title: '选择震动模式', diff --git a/entry/src/main/ets/service/AudioVibrationService.ets b/entry/src/main/ets/service/AudioVibrationService.ets index b7fbca7..c045df2 100644 --- a/entry/src/main/ets/service/AudioVibrationService.ets +++ b/entry/src/main/ets/service/AudioVibrationService.ets @@ -235,7 +235,7 @@ export class AudioVibrationService { switch (this.vibrationMode) { case '仅手柄': return false; case '仅设备': - case '同时': return true; + case '协同': return true; case '自动': default: // Automatic audio haptics may use the gamepad path only when this @@ -249,7 +249,7 @@ export class AudioVibrationService { private shouldVibrateGamepad(): boolean { switch (this.vibrationMode) { case '仅手柄': - case '同时': return true; + case '协同': return true; case '仅设备': return false; case '自动': default: diff --git a/entry/src/main/ets/service/SettingsService.ets b/entry/src/main/ets/service/SettingsService.ets index 42de276..74db68d 100644 --- a/entry/src/main/ets/service/SettingsService.ets +++ b/entry/src/main/ets/service/SettingsService.ets @@ -27,7 +27,8 @@ import { PerfOverlayPosition, getRecommendedBitrate, GAME_VIBRATION_STRENGTH_DEFAULT, - normalizeGameVibrationStrength + normalizeGameVibrationStrength, + normalizeVibrationMode } from '../model/StreamConfig'; /** @@ -72,7 +73,7 @@ export class SettingsKeys { // 输入 - 手柄设置 static readonly ENABLE_VIBRATION: string = 'settings_enable_vibration'; - static readonly VIBRATION_MODE: string = 'settings_vibration_mode'; // 震动模式:自动/仅手柄/仅设备/同时 + static readonly VIBRATION_MODE: string = 'settings_vibration_mode'; // 震动模式:自动/仅手柄/仅设备/协同 static readonly VIBRATE_FALLBACK_STRENGTH: string = 'settings_vibrate_fallback_strength'; // 历史存储键,现用于游戏震动强度 static readonly ENABLE_AUDIO_VIBRATION: string = 'settings_enable_audio_vibration'; // 音频振动(低频驱动) static readonly AUDIO_VIBRATION_STRENGTH: string = 'settings_audio_vibration_strength'; // 音频振动强度 (0-100) @@ -193,7 +194,7 @@ export class SettingsKeys { export interface InputSettings { // 手柄 enableVibration: boolean; - vibrationMode: string; // 自动/仅手柄/仅设备/同时 + vibrationMode: string; // 自动/仅手柄/仅设备/协同 gameVibrationStrength: number; enableAudioVibration: boolean; // 音频振动(低频驱动) audioVibrationStrength: number; // 音频振动强度 (0-100) @@ -622,7 +623,8 @@ export class SettingsService { // 获取输入设置 const enableVibration = await this.getBoolean(SettingsKeys.ENABLE_VIBRATION, true); - const vibrationMode = await this.getString(SettingsKeys.VIBRATION_MODE, '自动'); + const vibrationMode = normalizeVibrationMode( + await this.getString(SettingsKeys.VIBRATION_MODE, '自动')); const gameVibrationStrength = normalizeGameVibrationStrength( await this.getNumber(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, GAME_VIBRATION_STRENGTH_DEFAULT) ); @@ -777,7 +779,8 @@ export class SettingsService { return { // 手柄 enableVibration: await this.getBoolean(SettingsKeys.ENABLE_VIBRATION, true), - vibrationMode: await this.getString(SettingsKeys.VIBRATION_MODE, '自动'), + vibrationMode: normalizeVibrationMode( + await this.getString(SettingsKeys.VIBRATION_MODE, '自动')), gameVibrationStrength: normalizeGameVibrationStrength( await this.getNumber(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, GAME_VIBRATION_STRENGTH_DEFAULT) ), diff --git a/entry/src/main/ets/service/input/GamepadManager.ets b/entry/src/main/ets/service/input/GamepadManager.ets index c49faf9..2c4eb3a 100644 --- a/entry/src/main/ets/service/input/GamepadManager.ets +++ b/entry/src/main/ets/service/input/GamepadManager.ets @@ -176,8 +176,7 @@ export class GamepadManager implements UsbDriverListener { this.mouseEmulationService = new MouseEmulationService(); this.vibrationService = new GamepadVibrationService( this.usbDriverService, - this.deviceKeyToSlot, - (): boolean => this.hasConnectedGamepadOutsideUsbDriver() + this.deviceKeyToSlot ); this.loadGCButtonMappingSettings(); this.loadSensorSettings(); @@ -495,15 +494,6 @@ export class GamepadManager implements UsbDriverListener { return this.useGameControllerKit && this.gcDeviceIdToSlot.size > 0; } - private hasConnectedGamepadOutsideUsbDriver(): boolean { - if (this.hasActiveGcGamepad()) return true; - let connected = false; - this.deviceInfoMap.forEach((info: GamepadInfo): void => { - if (!info.isUsb) connected = true; - }); - return connected; - } - // ==================== 鼠标模拟(委托 MouseEmulationService) ==================== /** 设置鼠标模拟回调(由 StreamPage 调用) */ diff --git a/entry/src/main/ets/service/input/GamepadVibrationService.ets b/entry/src/main/ets/service/input/GamepadVibrationService.ets index 7b52f32..c380114 100644 --- a/entry/src/main/ets/service/input/GamepadVibrationService.ets +++ b/entry/src/main/ets/service/input/GamepadVibrationService.ets @@ -75,9 +75,9 @@ export class GamepadVibrationService { /** Device vibrator APIs are relatively expensive; coalesce authored IR frames. */ private static readonly DEVICE_RENDER_INTERVAL_MS = 40; /** - * The body actuator is a single, broad-band motor. In simultaneous mode it - * should add weight rather than mirror the controller's high-frequency - * detail. Keep these gains separate from the controller output path. + * The body actuator is a single, broad-band motor. In 协同 mode it should + * add weight rather than mirror the controller's high-frequency detail. + * Keep these gains separate from the controller output path. */ private static readonly BODY_LOW_BAND_GAIN = 0.72; private static readonly BODY_HIGH_BAND_GAIN = 0.28; @@ -88,7 +88,6 @@ export class GamepadVibrationService { private static readonly IR_PRIORITY_OVER_GAME = true; private usbDriverService: UsbDriverService; private deviceVibrationCoordinator: DeviceVibrationCoordinator; - private hasExternalGamepad: () => boolean; // 设置 private vibrationFeedbackEnabled = true; @@ -134,11 +133,9 @@ export class GamepadVibrationService { private pendingDeviceRenderTimer: number = -1; private pendingDeviceRenderTransient = false; - constructor(usbDriverService: UsbDriverService, deviceKeyToSlot: Map, - hasExternalGamepad: () => boolean) { + constructor(usbDriverService: UsbDriverService, deviceKeyToSlot: Map) { this.usbDriverService = usbDriverService; this.deviceKeyToSlotLookup = deviceKeyToSlot; - this.hasExternalGamepad = hasExternalGamepad; this.deviceVibrationCoordinator = DeviceVibrationCoordinator.getInstance(); this.deviceVibrationCoordinator.setSourceResumeCallback((): void => { this.renderMixedDevice(false); @@ -796,7 +793,7 @@ export class GamepadVibrationService { case '仅设备': return false; case '仅手柄': - case '同时': + case '协同': return true; case '自动': default: @@ -809,12 +806,14 @@ export class GamepadVibrationService { case '仅手柄': return false; case '仅设备': - case '同时': + case '协同': return true; case '自动': default: - return this.usbDriverService.getControllers().length === 0 && - !this.hasExternalGamepad(); + // Bluetooth/GCK gamepads expose no rumble backend, so the body is the + // only usable actuator whenever no USB-driven controller exists — + // same policy as the audio haptics path. + return this.usbDriverService.getControllers().length === 0; } } @@ -1034,18 +1033,21 @@ export class GamepadVibrationService { const highIntensity = this.mixWithHeadroom(gameHighIntensity, irHighIntensity, 100); // A controller can express two motor bands independently. The body motor // cannot, so its continuous channel deliberately favors the low band and - // attenuates high-frequency detail. This makes "同时" complementary: + // attenuates high-frequency detail. This makes "协同" complementary: // the controller carries detail while the body adds physical weight. const gameIntensity = this.getBodyContinuousIntensity(lowIntensity, highIntensity); const audioContinuous = !irActive && Date.now() <= this.audioDeviceContinuousUntilMs ? this.audioDeviceContinuous : 0; + // Audio haptics gets the same complementary treatment while a controller + // destination is live: the body adds weight instead of mirroring what the + // controller already plays. const mixedContinuous = this.mixWithHeadroom( - gameIntensity, audioContinuous, 100); + gameIntensity, this.shapeAudioForBody(audioContinuous), 100); const mixedTransient = effectiveAudioTransient ? this.mixWithHeadroom( - gameIntensity, this.audioDeviceTransient, 100) + gameIntensity, this.shapeAudioForBody(this.audioDeviceTransient), 100) : 0; if (this.deviceVibrationCoordinator.isTouchHapticActive()) return; @@ -1085,10 +1087,11 @@ export class GamepadVibrationService { private getBodyContinuousIntensity(lowIntensity: number, highIntensity: number): number { - // With no controller destination, the body is the only actuator and must - // retain the full authored energy. Attenuation is reserved for simultaneous - // mode, where the controller carries the high-frequency detail. - if (this.vibrationMode !== '同时') { + // With no live controller destination, the body is the only actuator and + // must retain the full authored energy. Attenuation is reserved for 协同 + // mode with a USB controller attached, where the controller carries the + // high-frequency detail. + if (!this.isComplementaryBodyShapingActive()) { return Math.max(lowIntensity, highIntensity); } return this.clampIntensity( @@ -1097,6 +1100,27 @@ export class GamepadVibrationService { ); } + /** 互补衰减只在「协同」且确实有 USB 手柄承载细节时生效。 */ + private isComplementaryBodyShapingActive(): boolean { + return this.vibrationMode === '协同' && this.hasUsbControllers(); + } + + /** + * 协同模式下的音频互塑:sharpness 反映音频频谱倾向,低频内容保留接近 + * BODY_LOW_BAND_GAIN 的量感,尖锐内容衰减到 BODY_HIGH_BAND_GAIN,与游戏 + * rumble 的双频段权重保持同一套互补语义。 + */ + private shapeAudioForBody(intensity: number): number { + if (intensity <= 0 || !this.isComplementaryBodyShapingActive()) { + return intensity; + } + const gain = GamepadVibrationService.BODY_HIGH_BAND_GAIN + + (1 - this.audioDeviceSharpness) * + (GamepadVibrationService.BODY_LOW_BAND_GAIN - + GamepadVibrationService.BODY_HIGH_BAND_GAIN); + return this.clampIntensity(intensity * gain); + } + /** * 立即停止震动(同步方式) */ From 75df2ef1a25781dfa38dd871287350ff5362764e Mon Sep 17 00:00:00 2001 From: qiin <414382190@qq.com> Date: Fri, 21 Aug 2026 16:05:31 +0800 Subject: [PATCH 2/3] chore(haptics): drop vibration mode legacy migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app has no external users yet, so persisted 同时 values can simply fall back to the 自动 default instead of being mapped. Co-Authored-By: Claude Sonnet 5 --- entry/src/main/ets/model/StreamConfig.ets | 5 ----- entry/src/main/ets/pages/SettingsPageV2.ets | 6 ++---- entry/src/main/ets/service/SettingsService.ets | 9 +++------ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/entry/src/main/ets/model/StreamConfig.ets b/entry/src/main/ets/model/StreamConfig.ets index f67d384..fc09a05 100644 --- a/entry/src/main/ets/model/StreamConfig.ets +++ b/entry/src/main/ets/model/StreamConfig.ets @@ -27,11 +27,6 @@ export function normalizeGameVibrationStrength(value: number): number { return Math.max(GAME_VIBRATION_STRENGTH_MIN, Math.min(GAME_VIBRATION_STRENGTH_MAX, roundedValue)); } -/** 兼容历史设置值:震动模式「同时」已更名为「协同」。 */ -export function normalizeVibrationMode(mode: string): string { - return mode === '同时' ? '协同' : mode; -} - export interface StreamConfig { // 视频设置 width: number; diff --git a/entry/src/main/ets/pages/SettingsPageV2.ets b/entry/src/main/ets/pages/SettingsPageV2.ets index ac1653a..f9ab8fa 100644 --- a/entry/src/main/ets/pages/SettingsPageV2.ets +++ b/entry/src/main/ets/pages/SettingsPageV2.ets @@ -23,8 +23,7 @@ import { GAME_VIBRATION_STRENGTH_MIN, GAME_VIBRATION_STRENGTH_ORIGINAL, GAME_VIBRATION_STRENGTH_STEP, - normalizeGameVibrationStrength, - normalizeVibrationMode + normalizeGameVibrationStrength } from '../model/StreamConfig'; import { PerfLabels } from '../components/PerformanceOverlayManager'; import { SettingSlider, SettingSliderConfig, LogarithmicSlider } from '../components/SettingSlider'; @@ -540,8 +539,7 @@ struct SettingsPageV2 { // 输入 - 手柄设置 this.enableVibration = await this.loadBoolean(SettingsKeys.ENABLE_VIBRATION, true); - this.vibrationMode = normalizeVibrationMode( - await PreferencesUtil.get(SettingsKeys.VIBRATION_MODE, '自动')); + this.vibrationMode = await PreferencesUtil.get(SettingsKeys.VIBRATION_MODE, '自动'); this.gameVibrationStrength = await this.loadGameVibrationStrength(); this.enableAudioVibration = await this.loadBoolean(SettingsKeys.ENABLE_AUDIO_VIBRATION, false); this.audioVibrationStrength = await PreferencesUtil.get(SettingsKeys.AUDIO_VIBRATION_STRENGTH, 60); diff --git a/entry/src/main/ets/service/SettingsService.ets b/entry/src/main/ets/service/SettingsService.ets index 74db68d..6379ca6 100644 --- a/entry/src/main/ets/service/SettingsService.ets +++ b/entry/src/main/ets/service/SettingsService.ets @@ -27,8 +27,7 @@ import { PerfOverlayPosition, getRecommendedBitrate, GAME_VIBRATION_STRENGTH_DEFAULT, - normalizeGameVibrationStrength, - normalizeVibrationMode + normalizeGameVibrationStrength } from '../model/StreamConfig'; /** @@ -623,8 +622,7 @@ export class SettingsService { // 获取输入设置 const enableVibration = await this.getBoolean(SettingsKeys.ENABLE_VIBRATION, true); - const vibrationMode = normalizeVibrationMode( - await this.getString(SettingsKeys.VIBRATION_MODE, '自动')); + const vibrationMode = await this.getString(SettingsKeys.VIBRATION_MODE, '自动'); const gameVibrationStrength = normalizeGameVibrationStrength( await this.getNumber(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, GAME_VIBRATION_STRENGTH_DEFAULT) ); @@ -779,8 +777,7 @@ export class SettingsService { return { // 手柄 enableVibration: await this.getBoolean(SettingsKeys.ENABLE_VIBRATION, true), - vibrationMode: normalizeVibrationMode( - await this.getString(SettingsKeys.VIBRATION_MODE, '自动')), + vibrationMode: await this.getString(SettingsKeys.VIBRATION_MODE, '自动'), gameVibrationStrength: normalizeGameVibrationStrength( await this.getNumber(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, GAME_VIBRATION_STRENGTH_DEFAULT) ), From 56f3d81732d59b3e03b2c6f9497058e4705058cf Mon Sep 17 00:00:00 2001 From: qiin <414382190@qq.com> Date: Fri, 21 Aug 2026 16:14:48 +0800 Subject: [PATCH 3/3] feat(haptics): show mode description on vibration segment setting Move the per-mode descriptions onto the live segment item's subtitle (footnote below the buttons) instead of only the dead picker method, and delete that unused picker. Co-Authored-By: Claude Sonnet 5 --- entry/src/main/ets/pages/SettingsPageV2.ets | 31 +++++++-------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/entry/src/main/ets/pages/SettingsPageV2.ets b/entry/src/main/ets/pages/SettingsPageV2.ets index f9ab8fa..dbd6d76 100644 --- a/entry/src/main/ets/pages/SettingsPageV2.ets +++ b/entry/src/main/ets/pages/SettingsPageV2.ets @@ -1480,6 +1480,7 @@ struct SettingsPageV2 { title: '震动模式', type: 'segment', value: this.vibrationMode, + subtitle: this.getVibrationModeSubtitle(), visible: this.enableVibration, segmentLabels: ['自动', '仅手柄', '仅设备', '协同'], onSegmentChange: (value: string) => { @@ -3675,27 +3676,15 @@ struct SettingsPageV2 { private showEnhancedTouchZoneDividerPicker(): void { } private showPointerVelocityFactorPicker(): void { } - /** - * 显示震动模式选择器 - */ - private showVibrationModePicker(): void { - const options: PickerOption[] = [ - { title: '自动', subtitle: '有手柄用手柄震动,无手柄用设备震动', value: '自动' } as PickerOption, - { title: '仅手柄', subtitle: '只使用手柄震动', value: '仅手柄' } as PickerOption, - { title: '仅设备', subtitle: '只使用设备震动', value: '仅设备' } as PickerOption, - { title: '协同', subtitle: '手柄承载高频细节,机身补充低频量感', value: '协同' } as PickerOption - ]; - const config: PickerConfig = { - title: '选择震动模式', - subtitle: '选择震动反馈的来源', - selectedValue: this.vibrationMode, - options: options, - onSelect: (option: PickerOption) => { - this.vibrationMode = option.value as string; - this.saveSetting(SettingsKeys.VIBRATION_MODE, option.value as string); - } - }; - this.showPicker(config); + /** 震动模式分段项下方的说明文字,随所选模式变化。 */ + private getVibrationModeSubtitle(): string { + switch (this.vibrationMode) { + case '仅手柄': return '只使用手柄震动'; + case '仅设备': return '只使用设备震动'; + case '协同': return '手柄承载高频细节,机身补充低频量感'; + case '自动': + default: return '有手柄用手柄震动,无手柄用设备震动'; + } } private showAudioVibrationSceneModePicker(): void { const options: PickerOption[] = [