Skip to content

Commit 88e6de7

Browse files
committed
Add smart assessment game mode dashboard
1 parent c2f9109 commit 88e6de7

31 files changed

Lines changed: 1156 additions & 7 deletions

electron/main.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
isTrustedCueForgeUrl,
1212
validateIpcSender
1313
} from '../src/security/electronPolicy.js';
14+
import { detectRunningGame } from '../src/native/runningGameDetector.js';
1415

1516
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1617

@@ -127,6 +128,11 @@ ipcMain.handle('cueforge:desktop-info', trustedIpc(() => ({
127128

128129
ipcMain.handle('cueforge:read-bridge-report', trustedIpc(async () => readReport()));
129130

131+
ipcMain.handle('cueforge:detect-game', trustedIpc(async () => {
132+
const game = await detectRunningGame();
133+
return game ? { id: game.id, exe: game.exe } : null;
134+
}));
135+
130136
ipcMain.handle('cueforge:scan-audio-setup', trustedIpc(async () => {
131137
const script = scriptPath();
132138
const output = reportPath();

electron/preload.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ contextBridge.exposeInMainWorld('cueforgeDesktop', {
44
isDesktop: true,
55
info: () => ipcRenderer.invoke('cueforge:desktop-info'),
66
getDesktopInfo: () => ipcRenderer.invoke('cueforge:desktop-info'),
7+
detectGame: () => ipcRenderer.invoke('cueforge:detect-game'),
78
scanAudioSetup: () => ipcRenderer.invoke('cueforge:scan-audio-setup'),
89
readBridgeReport: () => ipcRenderer.invoke('cueforge:read-bridge-report'),
910
openBridgeFolder: () => ipcRenderer.invoke('cueforge:open-bridge-folder'),

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@
8383
},
8484
"overrides": {
8585
"form-data": "4.0.6",
86+
"@electron/get": {
87+
"undici": "7.28.0"
88+
},
8689
"js-yaml": "4.2.0",
8790
"tar": "7.5.16",
8891
"tmp": "0.2.7"

qa/playwright/electron/desktop-bridge-smoke.spec.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ test('CueForge desktop bridge smoke', async () => {
3030
expect(String(desktopInfo?.reportPath || '').toLowerCase()).toContain('cueforge');
3131
expect(String(desktopInfo?.scriptPath || '')).toContain('Scan-AudioSetup.ps1');
3232
expect(bridgeKeys).toEqual([
33+
'detectGame',
3334
'getDesktopInfo',
3435
'info',
3536
'isDesktop',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { extractor, extractWavFeatures, WavFeatureExtractor } from '../wavFeatureExtractor.js';

src/core/bottleneckDiagnosis.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
const highConflictIds = new Set([
2+
'double-spatial-risk',
3+
'native-spatial-stack-blocked',
4+
'double-eq-risk',
5+
'stacked-noise-suppression',
6+
'missing-stream-limiter',
7+
'stream-clipping-risk',
8+
'bass-masking-footsteps',
9+
'latency-budget-fail'
10+
]);
11+
12+
export function detectBottleneck(detectData = {}) {
13+
const findings = [];
14+
const conflicts = Array.isArray(detectData.conflicts) ? detectData.conflicts : [];
15+
const metrics = detectData.metrics || {};
16+
17+
if (detectData?.sonar && detectData?.discord) {
18+
findings.push({
19+
id: 'sonar-discord-conflict',
20+
severity: 'high',
21+
title: 'Sonar + Discord likely conflicting',
22+
msg: 'Sonar + Discord likely conflicting',
23+
fix: 'Pick one processing path for voice while testing, then confirm Discord input/output.'
24+
});
25+
}
26+
27+
if (Number(detectData?.micClip) > 0.6) {
28+
findings.push({
29+
id: 'mic-clipping-risk',
30+
severity: 'high',
31+
title: 'High mic clipping risk',
32+
msg: 'High mic clipping risk - lower gain',
33+
fix: 'Lower mic gain before trusting EQ, Sound Match, or Discord feedback.'
34+
});
35+
}
36+
37+
if (detectData?.iEm && !detectData?.eqActive) {
38+
findings.push({
39+
id: 'iem-eq-needed',
40+
severity: 'medium',
41+
title: 'IEMs usually need EQ',
42+
msg: 'IEMs usually need EQ',
43+
fix: 'Start from a safe IEM profile and verify comfort before boosting cue bands.'
44+
});
45+
}
46+
47+
conflicts.forEach((conflict) => {
48+
findings.push({
49+
id: conflict.id || 'conflict',
50+
severity: conflict.severity || (highConflictIds.has(conflict.id) ? 'high' : 'medium'),
51+
title: conflict.title || conflict.message || conflict.id || 'Audio conflict',
52+
fix: conflict.fix || 'Resolve this before applying a profile.'
53+
});
54+
});
55+
56+
if (Number(metrics.cpuLoadPercent) >= 75) {
57+
findings.push({
58+
id: 'cpu-load-high',
59+
severity: 'medium',
60+
title: 'CPU load is high for live monitoring',
61+
fix: 'Use Game Mode or close extra analyzers while playing.'
62+
});
63+
}
64+
65+
if (Number(metrics.audioLatencyMs) > 30) {
66+
findings.push({
67+
id: 'latency-budget-fail',
68+
severity: 'high',
69+
title: 'Latency budget is too high',
70+
fix: 'Use one spatial layer, one EQ path, and a lower-latency output route.'
71+
});
72+
}
73+
74+
if (Number(metrics.droppedFrames) > 0 || Number(metrics.monitorDrops) > 0) {
75+
findings.push({
76+
id: 'monitor-drops',
77+
severity: 'medium',
78+
title: 'Monitoring loop dropped updates',
79+
fix: 'Switch to Game Mode or reduce analyzer size.'
80+
});
81+
}
82+
83+
const sorted = findings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));
84+
const primary = sorted[0] || {
85+
id: 'none',
86+
severity: 'low',
87+
title: 'No bottleneck detected',
88+
fix: 'Keep the current profile and run one controlled match test.'
89+
};
90+
91+
return {
92+
schema: 'cueforge.bottleneck-diagnosis.v1',
93+
status: primary.severity === 'high' ? 'blocked' : sorted.length ? 'watch' : 'clear',
94+
primary,
95+
primaryBottleneck: {
96+
severity: primary.severity,
97+
msg: primary.msg || primary.title,
98+
fix: primary.fix
99+
},
100+
issues: sorted.map((item) => ({
101+
severity: item.severity,
102+
msg: item.msg || item.title,
103+
id: item.id,
104+
fix: item.fix
105+
})),
106+
findings: sorted,
107+
metrics: {
108+
cpuLoadPercent: Number(metrics.cpuLoadPercent) || 0,
109+
audioLatencyMs: Number(metrics.audioLatencyMs) || 0,
110+
droppedFrames: Number(metrics.droppedFrames || metrics.monitorDrops) || 0
111+
}
112+
};
113+
}
114+
115+
function severityRank(severity) {
116+
return { high: 3, medium: 2, low: 1 }[severity] || 0;
117+
}

src/core/smartAssessment.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { detectBottleneck } from './bottleneckDiagnosis.js';
2+
import { getPerformanceProfileConfig } from '../settings/performanceSettings.js';
3+
4+
export async function runSmartAssessment(mode = 'quick', {
5+
detectData = {},
6+
performanceProfile = 'balanced',
7+
runningGame = null
8+
} = {}) {
9+
const bottleneck = detectBottleneck(detectData);
10+
const performance = getPerformanceProfileConfig(performanceProfile);
11+
const ready = bottleneck.status === 'clear' && mode === 'full';
12+
13+
return {
14+
schema: 'cueforge.smart-assessment.v1',
15+
mode,
16+
game: runningGame || detectData.runningGame || null,
17+
performance: {
18+
profile: performanceProfile,
19+
...performance
20+
},
21+
bottleneck,
22+
apply: {
23+
safeToApply: ready,
24+
reason: ready
25+
? 'Full assessment is clear enough to stage a reversible profile.'
26+
: 'Preview only until the full scan, game settings, and Sound Match evidence agree.'
27+
},
28+
nextAction: bottleneck.primary.fix
29+
};
30+
}

src/detection/quickDetect.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export async function runQuickDetect({
2+
desktop = globalThis.window?.cueforgeDesktop,
3+
navigatorRef = globalThis.navigator
4+
} = {}) {
5+
const devices = await safeEnumerateDevices(navigatorRef);
6+
const labels = devices.map((device) => String(device.label || '')).join(' ');
7+
const desktopGame = await safeDetectGame(desktop);
8+
9+
return {
10+
schema: 'cueforge.quick-detect.v1',
11+
completeChain: devices.some((device) => device.kind === 'audioinput') && devices.some((device) => device.kind === 'audiooutput'),
12+
sonar: /sonar|steelseries/i.test(labels),
13+
discord: /discord/i.test(labels),
14+
iEm: /iem|in-ear|earbud/i.test(labels),
15+
eqActive: /equalizer apo|peace|eq/i.test(labels),
16+
runningGame: desktopGame?.id || desktopGame || null,
17+
deviceCounts: {
18+
inputs: devices.filter((device) => device.kind === 'audioinput').length,
19+
outputs: devices.filter((device) => device.kind === 'audiooutput').length
20+
}
21+
};
22+
}
23+
24+
async function safeEnumerateDevices(navigatorRef) {
25+
try {
26+
if (!navigatorRef?.mediaDevices?.enumerateDevices) return [];
27+
return await navigatorRef.mediaDevices.enumerateDevices();
28+
} catch {
29+
return [];
30+
}
31+
}
32+
33+
async function safeDetectGame(desktop) {
34+
try {
35+
if (!desktop?.detectGame) return null;
36+
return await desktop.detectGame();
37+
} catch {
38+
return null;
39+
}
40+
}

src/main.jsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ import { buildGameAudioSettingsCheck } from './core/gameAudioSettingsCheck.js';
155155
import { buildMicPlan } from './engines/micPlan.js';
156156
import { honestSpatialModes, spatialTruthWarning } from './engines/spatialPlan.js';
157157
import { GuidedSetupRunPanel, SetupCommandCenter } from './ui/SetupCommandCenter.jsx';
158+
import GameModeToggle from './ui/GameModeToggle.jsx';
159+
import MoonDashboard from './ui/MoonDashboard.jsx';
160+
import { createLowLatencyAudioContext } from './shared/audio/LowLatencyAudioContext.js';
158161
import './styles.css';
159162

160163
const headsetProfiles = [
@@ -588,6 +591,7 @@ function buildEqFromSourceProfile(profile, fallbackEq = baseEq) {
588591

589592
const SIMPLE_NAV_ITEMS = [
590593
['dashboard', Gauge, 'Command Center'],
594+
['moon', Sparkles, 'Smart'],
591595
['detect', Search, 'Auto Setup'],
592596
['mic', Mic, 'Mic Check'],
593597
['eq', SlidersHorizontal, 'Tune'],
@@ -599,6 +603,7 @@ const SIMPLE_NAV_ITEMS = [
599603

600604
const EXPERT_NAV_ITEMS = [
601605
['hub', Radio, 'Community Hub'],
606+
['moon', Sparkles, 'Moon'],
602607
['dashboard', Gauge, 'Control'],
603608
['selftest', TestTube2, 'Self Test'],
604609
['dna', BrainCircuit, 'Audio DNA'],
@@ -1131,6 +1136,8 @@ function App() {
11311136
/>
11321137
))}
11331138

1139+
{active === 'moon' && <MoonDashboard />}
1140+
11341141
{active === 'selftest' && <SelfTestRunner />}
11351142

11361143
{active === 'dna' && <AudioDnaPage eq={eq} cueforgeState={cueforgeState.stateV2} />}
@@ -1482,9 +1489,7 @@ function UiNotePopover({ draft, onChange, onCancel, onSave }) {
14821489
}
14831490

14841491
function createBrowserAudioContext() {
1485-
const BrowserAudioContext = window.AudioContext || window.webkitAudioContext;
1486-
if (!BrowserAudioContext) throw new Error('Web Audio is unavailable');
1487-
return new BrowserAudioContext();
1492+
return createLowLatencyAudioContext({ target: window });
14881493
}
14891494

14901495
function pickEvidenceMimeType() {
@@ -3846,6 +3851,9 @@ function SettingsPage({ settings, onUpdate, onReset, onRerunSetup, onOpen, uiNot
38463851

38473852
<Panel title="Player Mode" icon={Gauge}>
38483853
<p>Simple Mode is the default player path. Expert Mode brings back the full lab, raw proof, system info, and developer repair tools.</p>
3854+
<div className="settings-stack compact-settings">
3855+
<GameModeToggle />
3856+
</div>
38493857
<div className="mode-choice">
38503858
<button
38513859
className={normalized.interfaceMode === 'simple' ? 'selected' : ''}

0 commit comments

Comments
 (0)