Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion entry/src/main/ets/model/StreamConfig.ets
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions entry/src/main/ets/pages/SettingsPageV2.ets
Original file line number Diff line number Diff line change
Expand Up @@ -1481,7 +1481,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);
Expand Down Expand Up @@ -3683,7 +3683,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: '选择震动模式',
Expand Down
4 changes: 2 additions & 2 deletions entry/src/main/ets/service/AudioVibrationService.ets
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions entry/src/main/ets/service/SettingsService.ets
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -193,7 +193,7 @@ export class SettingsKeys {
export interface InputSettings {
// 手柄
enableVibration: boolean;
vibrationMode: string; // 自动/仅手柄/仅设备/同时
vibrationMode: string; // 自动/仅手柄/仅设备/协同
gameVibrationStrength: number;
enableAudioVibration: boolean; // 音频振动(低频驱动)
audioVibrationStrength: number; // 音频振动强度 (0-100)
Expand Down
12 changes: 1 addition & 11 deletions entry/src/main/ets/service/input/GamepadManager.ets
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 调用) */
Expand Down
60 changes: 42 additions & 18 deletions entry/src/main/ets/service/input/GamepadVibrationService.ets
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -134,11 +133,9 @@ export class GamepadVibrationService {
private pendingDeviceRenderTimer: number = -1;
private pendingDeviceRenderTransient = false;

constructor(usbDriverService: UsbDriverService, deviceKeyToSlot: Map<string, number>,
hasExternalGamepad: () => boolean) {
constructor(usbDriverService: UsbDriverService, deviceKeyToSlot: Map<string, number>) {
this.usbDriverService = usbDriverService;
this.deviceKeyToSlotLookup = deviceKeyToSlot;
this.hasExternalGamepad = hasExternalGamepad;
this.deviceVibrationCoordinator = DeviceVibrationCoordinator.getInstance();
this.deviceVibrationCoordinator.setSourceResumeCallback((): void => {
this.renderMixedDevice(false);
Expand Down Expand Up @@ -796,7 +793,7 @@ export class GamepadVibrationService {
case '仅设备':
return false;
case '仅手柄':
case '同时':
case '协同':
return true;
case '自动':
default:
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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);
}

/**
* 立即停止震动(同步方式)
*/
Expand Down
Loading