diff --git a/entry/src/main/ets/model/StreamConfig.ets b/entry/src/main/ets/model/StreamConfig.ets index ed0144f..b0d8d30 100644 --- a/entry/src/main/ets/model/StreamConfig.ets +++ b/entry/src/main/ets/model/StreamConfig.ets @@ -13,6 +13,20 @@ * * 参考 Android 版本 StreamConfiguration.java */ +export const GAME_VIBRATION_STRENGTH_MIN = 0; +export const GAME_VIBRATION_STRENGTH_MAX = 200; +export const GAME_VIBRATION_STRENGTH_ORIGINAL = 100; +export const GAME_VIBRATION_STRENGTH_DEFAULT = GAME_VIBRATION_STRENGTH_ORIGINAL; +export const GAME_VIBRATION_STRENGTH_STEP = 5; + +export function normalizeGameVibrationStrength(value: number): number { + if (!Number.isFinite(value)) { + return GAME_VIBRATION_STRENGTH_DEFAULT; + } + const roundedValue = Math.round(value); + return Math.max(GAME_VIBRATION_STRENGTH_MIN, Math.min(GAME_VIBRATION_STRENGTH_MAX, roundedValue)); +} + export interface StreamConfig { // 视频设置 width: number; @@ -46,7 +60,7 @@ export interface StreamConfig { attachedGamepadMask: number; enableVibration: boolean; vibrationMode: string; // 震动模式:自动/仅手柄/仅设备/同时 - vibrateFallbackStrength: number; + gameVibrationStrength: number; // 串流游戏震动强度百分比(100=原始强度,100 以上为 app 侧放大) enableAudioVibration: boolean; // 音频振动(低频驱动) audioVibrationStrength: number; // 音频振动强度 (0-100) audioVibrationSceneMode: string; // 音频振动场景模式: 游戏/电影, 音乐/节奏, 自动 @@ -229,7 +243,7 @@ export function getDefaultStreamConfig(): StreamConfig { attachedGamepadMask: 0, enableVibration: true, vibrationMode: '自动', - vibrateFallbackStrength: 100, + gameVibrationStrength: GAME_VIBRATION_STRENGTH_DEFAULT, enableAudioVibration: false, audioVibrationStrength: 60, audioVibrationSceneMode: '游戏/电影', diff --git a/entry/src/main/ets/pages/SettingsPageV2.ets b/entry/src/main/ets/pages/SettingsPageV2.ets index 9cb9d72..2c2310d 100644 --- a/entry/src/main/ets/pages/SettingsPageV2.ets +++ b/entry/src/main/ets/pages/SettingsPageV2.ets @@ -15,7 +15,15 @@ import { pasteboard } from '@kit.BasicServicesKit'; import { PreferencesUtil } from '../utils/PreferencesUtil'; import { AppColors, AppSizes, AppSpacing, AppAnimation, AppShadows } from '../common/Theme'; import { StreamingSession, DecoderCapabilities } from '../service/streaming/StreamingSession'; -import { getRecommendedBitrate } from '../model/StreamConfig'; +import { + getRecommendedBitrate, + GAME_VIBRATION_STRENGTH_DEFAULT, + GAME_VIBRATION_STRENGTH_MAX, + GAME_VIBRATION_STRENGTH_MIN, + GAME_VIBRATION_STRENGTH_ORIGINAL, + GAME_VIBRATION_STRENGTH_STEP, + normalizeGameVibrationStrength +} from '../model/StreamConfig'; import { PerfLabels } from '../components/PerformanceOverlayManager'; import { SettingSlider, SettingSliderConfig, LogarithmicSlider } from '../components/SettingSlider'; import { SettingsBackupService } from '../service/SettingsBackupService'; @@ -175,7 +183,7 @@ struct SettingsPageV2 { // 输入 - 手柄设置 @State enableVibration: boolean = true; @State vibrationMode: string = '自动'; // 自动/仅手柄/仅设备/同时 - @State vibrateFallbackStrength: number = 100; + @State gameVibrationStrength: number = GAME_VIBRATION_STRENGTH_DEFAULT; @State enableAudioVibration: boolean = false; @State audioVibrationStrength: number = 60; @State audioVibrationSceneMode: string = '游戏/电影'; @@ -518,7 +526,7 @@ struct SettingsPageV2 { // 输入 - 手柄设置 this.enableVibration = await this.loadBoolean(SettingsKeys.ENABLE_VIBRATION, true); this.vibrationMode = await PreferencesUtil.get(SettingsKeys.VIBRATION_MODE, '自动'); - this.vibrateFallbackStrength = await PreferencesUtil.get(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, 100); + 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); this.audioVibrationSceneMode = await PreferencesUtil.get(SettingsKeys.AUDIO_VIBRATION_SCENE_MODE, '游戏/电影'); @@ -641,6 +649,38 @@ struct SettingsPageV2 { return String(val) === 'true'; } + private async loadGameVibrationStrength(): Promise { + const rawValue = await PreferencesUtil.get( + SettingsKeys.VIBRATE_FALLBACK_STRENGTH, + GAME_VIBRATION_STRENGTH_DEFAULT.toString() + ); + let parsedValue = GAME_VIBRATION_STRENGTH_DEFAULT; + let shouldRepair = false; + + if (typeof rawValue === 'number') { + parsedValue = Number.isFinite(rawValue) ? rawValue : GAME_VIBRATION_STRENGTH_DEFAULT; + shouldRepair = parsedValue !== rawValue; + } else { + const trimmedValue = rawValue.trim(); + if (trimmedValue.length === 0) { + shouldRepair = true; + } else { + const numberValue = Number(trimmedValue); + if (Number.isFinite(numberValue)) { + parsedValue = numberValue; + } else { + shouldRepair = true; + } + } + } + + const normalizedValue = normalizeGameVibrationStrength(parsedValue); + if (shouldRepair || normalizedValue !== parsedValue) { + this.saveSetting(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, normalizedValue); + } + return normalizedValue; + } + private async saveSetting(key: string, value: string | boolean | number): Promise { try { await PreferencesUtil.put(key, value); @@ -1341,7 +1381,7 @@ struct SettingsPageV2 { items: [ { title: '振动反馈', - subtitle: '手柄振动反馈', + subtitle: '串流游戏震动反馈', type: 'toggle', value: this.enableVibration, action: () => { @@ -1361,18 +1401,20 @@ struct SettingsPageV2 { } }, { - title: '设备振动强度', - subtitle: '使用设备震动时的强度', + title: '震动强度', + subtitle: this.gameVibrationStrength > GAME_VIBRATION_STRENGTH_ORIGINAL ? + '超过 100% 为 app 侧放大,不突破系统/硬件上限' : + '串流游戏震动强度,100% 为原始强度', type: 'slider', visible: this.enableVibration, - value: this.vibrateFallbackStrength, - min: 0, - max: 100, - step: 5, + value: this.gameVibrationStrength, + min: GAME_VIBRATION_STRENGTH_MIN, + max: GAME_VIBRATION_STRENGTH_MAX, + step: GAME_VIBRATION_STRENGTH_STEP, unit: '%', onSliderChange: (value: number) => { - this.vibrateFallbackStrength = value; - this.saveSetting(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, value); + this.gameVibrationStrength = normalizeGameVibrationStrength(value); + this.saveSetting(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, this.gameVibrationStrength); } }, { diff --git a/entry/src/main/ets/service/SettingsService.ets b/entry/src/main/ets/service/SettingsService.ets index 352fc9d..f6a6531 100644 --- a/entry/src/main/ets/service/SettingsService.ets +++ b/entry/src/main/ets/service/SettingsService.ets @@ -25,7 +25,9 @@ import { ScreenCombinationMode, PerfOverlayOrientation, PerfOverlayPosition, - getRecommendedBitrate + getRecommendedBitrate, + GAME_VIBRATION_STRENGTH_DEFAULT, + normalizeGameVibrationStrength } from '../model/StreamConfig'; /** @@ -67,7 +69,7 @@ export class SettingsKeys { // 输入 - 手柄设置 static readonly ENABLE_VIBRATION: string = 'settings_enable_vibration'; static readonly VIBRATION_MODE: string = 'settings_vibration_mode'; // 震动模式:自动/仅手柄/仅设备/同时 - static readonly VIBRATE_FALLBACK_STRENGTH: string = 'settings_vibrate_fallback_strength'; + 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) static readonly AUDIO_VIBRATION_SCENE_MODE: string = 'settings_audio_vibration_scene_mode'; // 音频振动场景模式 @@ -183,7 +185,7 @@ export interface InputSettings { // 手柄 enableVibration: boolean; vibrationMode: string; // 自动/仅手柄/仅设备/同时 - vibrateFallbackStrength: number; + gameVibrationStrength: number; enableAudioVibration: boolean; // 音频振动(低频驱动) audioVibrationStrength: number; // 音频振动强度 (0-100) audioVibrationSceneMode: string; // 音频振动场景模式 @@ -527,9 +529,14 @@ export class SettingsService { private async getNumber(key: string, defaultValue: number): Promise { const value = await PreferencesUtil.get(key, defaultValue.toString()); if (typeof value === 'number') { - return value; + return Number.isFinite(value) ? value : defaultValue; + } + const trimmedValue = value.trim(); + if (trimmedValue.length === 0) { + return defaultValue; } - return parseInt(value) || defaultValue; + const parsed = Number(trimmedValue); + return Number.isFinite(parsed) ? parsed : defaultValue; } /** @@ -598,7 +605,9 @@ export class SettingsService { // 获取输入设置 const enableVibration = await this.getBoolean(SettingsKeys.ENABLE_VIBRATION, true); const vibrationMode = await this.getString(SettingsKeys.VIBRATION_MODE, '自动'); - const vibrateFallbackStrength = await this.getNumber(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, 100); + const gameVibrationStrength = normalizeGameVibrationStrength( + await this.getNumber(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, GAME_VIBRATION_STRENGTH_DEFAULT) + ); const enableAudioVibration = await this.getBoolean(SettingsKeys.ENABLE_AUDIO_VIBRATION, false); const audioVibrationStrength = await this.getNumber(SettingsKeys.AUDIO_VIBRATION_STRENGTH, 60); const audioVibrationSceneMode = await this.getString(SettingsKeys.AUDIO_VIBRATION_SCENE_MODE, '游戏/电影'); @@ -679,7 +688,7 @@ export class SettingsService { attachedGamepadMask: 0, enableVibration: enableVibration, vibrationMode: vibrationMode, - vibrateFallbackStrength: vibrateFallbackStrength, + gameVibrationStrength: gameVibrationStrength, enableAudioVibration: enableAudioVibration, audioVibrationStrength: audioVibrationStrength, audioVibrationSceneMode: audioVibrationSceneMode, @@ -741,7 +750,9 @@ export class SettingsService { // 手柄 enableVibration: await this.getBoolean(SettingsKeys.ENABLE_VIBRATION, true), vibrationMode: await this.getString(SettingsKeys.VIBRATION_MODE, '自动'), - vibrateFallbackStrength: await this.getNumber(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, 100), + gameVibrationStrength: normalizeGameVibrationStrength( + await this.getNumber(SettingsKeys.VIBRATE_FALLBACK_STRENGTH, GAME_VIBRATION_STRENGTH_DEFAULT) + ), enableAudioVibration: await this.getBoolean(SettingsKeys.ENABLE_AUDIO_VIBRATION, false), audioVibrationStrength: await this.getNumber(SettingsKeys.AUDIO_VIBRATION_STRENGTH, 60), audioVibrationSceneMode: await this.getString(SettingsKeys.AUDIO_VIBRATION_SCENE_MODE, '游戏/电影'), diff --git a/entry/src/main/ets/service/input/GamepadManager.ets b/entry/src/main/ets/service/input/GamepadManager.ets index 1a91b10..2ba8de3 100644 --- a/entry/src/main/ets/service/input/GamepadManager.ets +++ b/entry/src/main/ets/service/input/GamepadManager.ets @@ -10,7 +10,7 @@ import { inputDevice } from '@kit.InputKit'; import { UsbDriverService, UsbDriverListener, AbstractController, ButtonFlags, KnownUsbGamepadInfo } from '../usbdriver/index'; -import { +import { gameControllerService, AxisType, GameControllerDeviceInfo @@ -645,12 +645,16 @@ export class GamepadManager implements UsbDriverListener { } /** - * 设置设备模拟震动 - * @param enabled 是否启用震动 - * @param strength 设备震动强度 (0-100) + * 设置串流游戏震动 + * @param enabled 是否启用串流游戏震动反馈 + * @param strength 串流游戏震动强度 (0-200, 100=原始强度) * @param mode 震动模式:自动/仅手柄/仅设备/同时 */ - setDeviceVibrate(enabled: boolean, strength: number, mode: string): void { + setGameVibration( + enabled: boolean, + strength: number, + mode: string + ): void { this.vibrationService.setSettings(enabled, strength, mode); } diff --git a/entry/src/main/ets/service/input/GamepadVibrationService.ets b/entry/src/main/ets/service/input/GamepadVibrationService.ets index 310f892..0742082 100644 --- a/entry/src/main/ets/service/input/GamepadVibrationService.ets +++ b/entry/src/main/ets/service/input/GamepadVibrationService.ets @@ -20,6 +20,11 @@ import { vibrator } from '@kit.SensorServiceKit'; import { UsbDriverService, AbstractController } from '../usbdriver/index'; import { RUMBLE_MAX } from './GamepadTypes'; +import { + GAME_VIBRATION_STRENGTH_DEFAULT, + GAME_VIBRATION_STRENGTH_ORIGINAL, + normalizeGameVibrationStrength +} from '../../model/StreamConfig'; // ==================== 震动服务 ==================== @@ -27,8 +32,8 @@ export class GamepadVibrationService { private usbDriverService: UsbDriverService; // 设置 - private deviceVibrateEnabled = false; - private deviceVibrateStrength = 100; + private vibrationFeedbackEnabled = true; + private gameVibrationStrength = GAME_VIBRATION_STRENGTH_DEFAULT; private vibrationMode = '自动'; // 槽位→设备键的查找函数(由 GamepadManager 提供) @@ -58,10 +63,19 @@ export class GamepadVibrationService { /** * 更新震动设置 */ - setSettings(enabled: boolean, strength: number, mode: string): void { - this.deviceVibrateEnabled = enabled; - this.deviceVibrateStrength = strength; + setSettings( + enabled: boolean, + strength: number, + mode: string + ): void { + const wasEnabled = this.vibrationFeedbackEnabled; + const previousStrength = this.gameVibrationStrength; + this.vibrationFeedbackEnabled = enabled; + this.gameVibrationStrength = normalizeGameVibrationStrength(strength); this.vibrationMode = mode; + if ((wasEnabled && !enabled) || (previousStrength > 0 && this.gameVibrationStrength === 0)) { + this.stopAll(); + } } // ==================== 公开 API ==================== @@ -80,6 +94,8 @@ export class GamepadVibrationService { clearTimeout(this.stopConfirmTimer); this.stopConfirmTimer = -1; } + if (!this.vibrationFeedbackEnabled) return; + if (lowFreqMotor !== 0 || highFreqMotor !== 0) { this.lastRumbleControllerNumber = controllerNumber; this.rumbleWatchdogTimer = setTimeout(() => { @@ -122,11 +138,7 @@ export class GamepadVibrationService { // USB 停止确认: 100ms 后重发 stop(0,0),防止首次 USB 传输被覆盖或丢失 if (lowFreqMotor === 0 && highFreqMotor === 0 && hasUsbController) { - this.stopConfirmTimer = setTimeout(() => { - this.stopConfirmTimer = -1; - const ctrls = this.usbDriverService.getControllers(); - this.rumbleUsbControllers(ctrls, controllerNumber, 0, 0); - }, 100); + this.scheduleUsbStopConfirm(controllerNumber); } } @@ -134,9 +146,14 @@ export class GamepadVibrationService { * 处理扳机震动 */ handleRumbleTriggers(controllerNumber: number, leftTrigger: number, rightTrigger: number): void { + if (!this.vibrationFeedbackEnabled) return; + const controllers = this.usbDriverService.getControllers(); const controller = this.findControllerBySlot(controllers, controllerNumber); - controller?.rumbleTriggers(leftTrigger, rightTrigger); + controller?.rumbleTriggers( + this.applyGameVibrationStrength(leftTrigger), + this.applyGameVibrationStrength(rightTrigger) + ); } /** @@ -147,7 +164,7 @@ export class GamepadVibrationService { rumbleUsbControllersOnly(controllerNumber: number, lowFreqMotor: number, highFreqMotor: number): void { const controllers = this.usbDriverService.getControllers(); if (controllers.length > 0) { - this.rumbleUsbControllers(controllers, controllerNumber, lowFreqMotor, highFreqMotor); + this.rumbleUsbControllers(controllers, controllerNumber, lowFreqMotor, highFreqMotor, false); } } @@ -185,6 +202,10 @@ export class GamepadVibrationService { const controllers = this.usbDriverService.getControllers(); for (const controller of controllers) { controller.rumble(0, 0); + controller.rumbleTriggers(0, 0); + } + if (controllers.length > 0) { + this.scheduleUsbStopConfirm(this.lastRumbleControllerNumber); } } catch (_) { /* ignore */ } } @@ -213,9 +234,39 @@ export class GamepadVibrationService { /** * 向 USB 控制器发送震动 */ - private rumbleUsbControllers(controllers: AbstractController[], controllerNumber: number, lowFreqMotor: number, highFreqMotor: number): void { + private rumbleUsbControllers( + controllers: AbstractController[], + controllerNumber: number, + lowFreqMotor: number, + highFreqMotor: number, + applyGain: boolean = true + ): void { const controller = this.findControllerBySlot(controllers, controllerNumber); - controller?.rumble(lowFreqMotor, highFreqMotor); + const lowValue = applyGain ? this.applyGameVibrationStrength(lowFreqMotor) : this.clampRumbleValue(lowFreqMotor); + const highValue = applyGain ? this.applyGameVibrationStrength(highFreqMotor) : this.clampRumbleValue(highFreqMotor); + controller?.rumble(lowValue, highValue); + } + + private applyGameVibrationStrength(value: number): number { + if (value <= 0) return 0; + if (this.gameVibrationStrength === GAME_VIBRATION_STRENGTH_DEFAULT) { + return this.clampRumbleValue(value); + } + const gained = Math.floor(value * this.gameVibrationStrength / GAME_VIBRATION_STRENGTH_ORIGINAL); + return this.clampRumbleValue(gained); + } + + private clampRumbleValue(value: number): number { + if (!Number.isFinite(value)) return 0; + return Math.max(0, Math.min(RUMBLE_MAX, Math.floor(value))); + } + + private scheduleUsbStopConfirm(controllerNumber: number): void { + this.stopConfirmTimer = setTimeout(() => { + this.stopConfirmTimer = -1; + const controllers = this.usbDriverService.getControllers(); + this.rumbleUsbControllers(controllers, controllerNumber, 0, 0); + }, 100); } /** @@ -227,7 +278,7 @@ export class GamepadVibrationService { * 3. 使用 stopVibrationSync 同步停止确保即时性 */ private triggerDeviceVibrate(lowFreqMotor: number, highFreqMotor: number): void { - if (!this.deviceVibrateEnabled) return; + if (!this.vibrationFeedbackEnabled || this.gameVibrationStrength <= 0) return; // 停止震动 if (lowFreqMotor === 0 && highFreqMotor === 0) { @@ -252,7 +303,8 @@ export class GamepadVibrationService { this.lastHighFreq = highFreqMotor; // 计算用户强度系数 - const userStrength = this.deviceVibrateStrength / 100; + const userStrength = Math.min(this.gameVibrationStrength, GAME_VIBRATION_STRENGTH_ORIGINAL) / + GAME_VIBRATION_STRENGTH_ORIGINAL; // 计算低频和高频马达的强度 (0-100) const lowIntensity = Math.floor((lowFreqMotor / RUMBLE_MAX) * userStrength * 100); diff --git a/entry/src/main/ets/service/usbdriver/NativeHidController.ets b/entry/src/main/ets/service/usbdriver/NativeHidController.ets index ae24f65..369f65f 100644 --- a/entry/src/main/ets/service/usbdriver/NativeHidController.ets +++ b/entry/src/main/ets/service/usbdriver/NativeHidController.ets @@ -19,7 +19,7 @@ import { usbManager } from '@kit.BasicServicesKit'; import { AbstractController } from './AbstractController'; import { UsbDriverListener } from './UsbDriverListener'; import { ButtonFlags, UsbClass, ControllerType, ControllerCapabilities } from './ControllerConstants'; -import { AXIS_MAX, TRIGGER_MAX, RUMBLE_MAX } from '../input/GamepadTypes'; +import { AXIS_MAX, TRIGGER_MAX } from '../input/GamepadTypes'; import { NativeHidParserService, NativeGamepadState, NativeGamepadType, NativeButtonFlags } from './NativeHidParserService'; import { createUsbError, UsbError } from './UsbErrorCodes'; @@ -664,12 +664,12 @@ export class NativeHidController extends AbstractController { return; } - // 获取震动命令 + // Native parser expects Moonlight rumble values in the same 0..65535 range as AbstractController. const cmd = this.nativeParser.createRumbleCommand( this.vendorId, this.productId, - Math.round(lowFreqMotor * RUMBLE_MAX), - Math.round(highFreqMotor * RUMBLE_MAX) + this.clampRumbleValue(lowFreqMotor), + this.clampRumbleValue(highFreqMotor) ); if (cmd) { @@ -677,6 +677,11 @@ export class NativeHidController extends AbstractController { } } + private clampRumbleValue(value: number): number { + if (!Number.isFinite(value)) return 0; + return Math.max(0, Math.min(65535, Math.round(value))); + } + private async sendOutputReport(data: Uint8Array): Promise { if (this.outputEndpoint === 0) { return; diff --git a/entry/src/main/ets/viewmodel/StreamViewModel.ets b/entry/src/main/ets/viewmodel/StreamViewModel.ets index 4763d58..fade3d7 100644 --- a/entry/src/main/ets/viewmodel/StreamViewModel.ets +++ b/entry/src/main/ets/viewmodel/StreamViewModel.ets @@ -270,11 +270,10 @@ export class StreamViewModel { console.info(`StreamViewModel: 触摸模式=${this.touchMode}`); console.info(`StreamViewModel: 性能覆盖层=${this.showOverlay}, 虚拟控制器=${this.showController}`); - // 配置震动设置 - const deviceVibrateEnabled = this.streamConfig.enableVibration && this.streamConfig.vibrateFallbackStrength > 0; - GamepadManager.getInstance().setDeviceVibrate( - deviceVibrateEnabled, - this.streamConfig.vibrateFallbackStrength, + // 配置串流游戏震动;100% 以上作为外接手柄 app 侧放大,设备马达仍按 100% 封顶。 + GamepadManager.getInstance().setGameVibration( + this.streamConfig.enableVibration, + this.streamConfig.gameVibrationStrength, this.streamConfig.vibrationMode ); }