diff --git a/entry/src/main/ets/model/StreamConfig.ets b/entry/src/main/ets/model/StreamConfig.ets index de97705..fc09a05 100644 --- a/entry/src/main/ets/model/StreamConfig.ets +++ b/entry/src/main/ets/model/StreamConfig.ets @@ -61,7 +61,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..dbd6d76 100644 --- a/entry/src/main/ets/pages/SettingsPageV2.ets +++ b/entry/src/main/ets/pages/SettingsPageV2.ets @@ -1480,8 +1480,9 @@ struct SettingsPageV2 { title: '震动模式', type: 'segment', value: this.vibrationMode, + subtitle: this.getVibrationModeSubtitle(), visible: this.enableVibration, - segmentLabels: ['自动', '仅手柄', '仅设备', '同时'], + segmentLabels: ['自动', '仅手柄', '仅设备', '协同'], onSegmentChange: (value: string) => { this.vibrationMode = value; this.saveSetting(SettingsKeys.VIBRATION_MODE, value); @@ -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[] = [ 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..6379ca6 100644 --- a/entry/src/main/ets/service/SettingsService.ets +++ b/entry/src/main/ets/service/SettingsService.ets @@ -72,7 +72,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 +193,7 @@ export class SettingsKeys { export interface InputSettings { // 手柄 enableVibration: boolean; - vibrationMode: string; // 自动/仅手柄/仅设备/同时 + vibrationMode: string; // 自动/仅手柄/仅设备/协同 gameVibrationStrength: number; enableAudioVibration: boolean; // 音频振动(低频驱动) audioVibrationStrength: number; // 音频振动强度 (0-100) 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); + } + /** * 立即停止震动(同步方式) */