Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
74 changes: 74 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
name: Immerse Jam
version: 1
token_ownership: existing-runtime-canonical
register: product
locales: [zh-CN, en]
colors:
ground: '#0d0c11'
surface: '#14131a'
panel: '#1b1a22'
text: '#eeecf3'
secondary: '#aba7b9'
accent: '#ff5c7a'
typography:
body: Noto Sans SC, HarmonyOS Sans SC, PingFang SC, Microsoft YaHei UI, system-ui
display: Bricolage Grotesque, Noto Sans SC, HarmonyOS Sans SC, Microsoft YaHei UI
mono: JetBrains Mono, Cascadia Mono, Consolas, monospace
spacing:
settings_navigation: 220px
settings_gap: 24px
rounded:
control: 7px
panel: 14px
elevation:
page: none
popup: var(--shadow-pop)
motion:
fast: 120ms
medium: 200ms
---

# Immerse Jam design context

A local music practice and recording workspace. Preserve the existing rehearsal-room visual language and its user-selected light/dark theme and accent. Settings are an instrument panel: fixed categories, readable controls, and visible paths/status. SV2 reference screenshots inform grouping and order, not product-specific audio/synthesis features.

The approved 2026-11-05 scope is S1, S2, S4, S5, S6, S7; S3 custom key bindings is excluded.

## Runtime ownership and mapping

Model B: existing CSS is canonical. src/styles/rehearsal-design.css is generated from docs/design/prototype.html; do not hand-edit that generated file. src/styles/themes.css maps original palette tokens. src/styles/rehearsal.css integrates light/dark/accent semantics. New settings styles live in src/styles/settings.css in the integration layer and consume the existing semantic properties.

| Role | Runtime source | Consumers |
| ----------------- | -------------------------------------- | --------------------------------------- |
| Ground and panels | --ground, --surface-1, --surface-2 | settings page, navigation, fields |
| Text hierarchy | --text, --text-2, --text-3 | headings, descriptions, path captions |
| Action/focus | --accent, --accent-soft, --accent-line | selected category, buttons, focus ring |
| Status | --ok, --warn, --danger | runtime state, errors, cleanup feedback |
| Type | --font-ui, --font-display, --font-mono | settings labels, headings, paths |
| Shape | --r-s, --r-l | controls and panels |
| Motion | --t-fast, --t-med | hover and popup transitions |

The exact dark/rose values above mirror the existing runtime seed. Other accents and the light theme retain their existing mappings; settings never hard-code these seed colors.

## Layout and components

Settings use a full-window page with a single Back action in the fixed heading and a persistent category rail. Only the content pane scrolls. At narrow widths, a shared Select replaces the rail. Search results group matched settings by category and keep the clear action available. Storage paths wrap; long model sources are folded behind details.

Use the existing Select owner, ModalFrame/Radix focus management for the settings page and confirmations, app notify status for action results, and Lucide icons. Body text stays at least the existing 14px body role. Paths and byte counts use the mono role. Focus is visible and active categories expose aria-current. Busy and failure states keep their controls in place.

The distinctive element is useful technical information: actual device state, readable data paths and explicit resource categories. No decorative hero, replacement logo, marketing slogan, or telemetry is introduced.

## Follow-up interaction and guide direction

Native desktop drag regions are disabled beneath managed modals and explicitly excluded on their surfaces and controls. Verify actual Windows mouse input after zoom/restore, rather than relying solely on DOM-injected clicks. Runtime preparation logs appear only when non-empty and are folded behind a disclosure.

The onboarding guide uses a compact step rail, persistent footer actions and restrained code-native workflow illustrations (import, stems, loop recording and export). Illustrations are labelled as examples and do not pretend to be live controls. The existing palette/type tokens remain canonical. #66CCFF is the first waveform swatch; existing saved colors and the original default remain intact.

Native playback settings expose real Windows systems and enumerated devices, processing vs device sample rate, explicit unavailability and safe rollback. Capture input remains the Windows recording device selected in the recording section. No shortcut editor is introduced.

## Motion and verification

Honor prefers-reduced-motion. Tooltips appear after two seconds of pointer hover and on keyboard focus; Escape dismisses them. All new text uses the shared Chinese/English dictionary. Preserve the unrelated existing music and recording screen contracts.

Check the built settings page in both themes, English/Chinese, narrow and short windows, and after restart. Storage cleanup needs synthetic filesystem regression tests; test profiles, screenshots and all caches stay on the selected development drive.
39 changes: 39 additions & 0 deletions UX-CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Settings UX contract

Applies to the settings workspace introduced 2026-11-05. Existing product flows remain governed by docs/ARCHITECTURE.md, shared project schema, and their tests.

## Scope and ownership

Approved: S1, S2, S4, S5, S6, S7 and the subsequent Windows native playback follow-up. S3 remains excluded: no shortcut editor or changes to existing key bindings. No path migration, MIDI hardware input or CPU/GPU tuning is introduced.

| Capability | Canonical owner | Source of truth | Allowed variants | Verification |
| ----------------- | ---------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------- |
| Select/Listbox | src/shared/Select.tsx | DESIGN.md and this contract | desktop native where platform geometry is accepted; authored menu=true for app-owned popup | keyboard and popup |
| Form | labelled shared controls in settings | This contract and validated preferences | local validation and inline error; no native validation bubbles | invalid values and persistence |
| Scrollbar | src/styles/settings.css global baseline | DESIGN.md | content-pane geometry | narrow layout and computed styles |
| Toast | useWorkspace.notify / .toast role=status | This contract | success and error | failure/retry result |
| Dialog | ModalFrame / Radix managedFocus | This contract | full-window settings, local cleanup confirmation | Escape, focus trap/restoration |
| Tooltip | src/shared/HelpTooltips.tsx | This contract | delayed pointer / immediate focus | delay, Escape, disabled preference |
| Storage lifecycle | desktop/settings-storage.cjs | Approved cleanup scope and this contract | opaque preview token, bounded cleanup | path, race, busy, expired token tests |

No date picker or table selection is introduced. Settings search is transient local state; it is not a shareable URL.

## Flow decisions

- Open settings from any route without unmounting the player or losing the current project. Model management opens the model category directly.
- General appearance changes take effect immediately and persist. Restoring a category resets only that category, not projects, devices, key bindings or data paths.
- A fresh empty library receives the guide once; it is skippable and may be reopened. Existing preference/profile data migrates without forcing a repeated guide.
- Audio defaults are separate from an explicit project recording override. The user may apply defaults to the current project. Device switching saves only after routing succeeds and is disabled while recording or calibrating. Compatibility capture calibration retains its existing input/output pair key; native playback adds backend, output device and processing rate to the route key. Explicit project corrections retain precedence.
- Settings use one Back action; operating-system window controls retain their normal meaning. Managed modal surfaces and controls are not draggable. Underlying page headers lose their drag regions while a managed modal is open; only the settings title region can drag the window. Native pointer testing must cover zoom changes and maximize/restore.
- Windows playback exposes compatibility output, WASAPI shared/exclusive/low-latency shared, DirectSound and installed x64 ASIO drivers. Unavailable drivers are not selectable. Processing rates are 44.1, 48, 88.2 and 96 kHz; shared-device mix rate is reported separately. Driver support determines whether a selection opens successfully.
- Engine changes preserve the project and playback position, stop output before rebuilding the processing context, and commit preferences only after the new route succeeds. Failure restores the previous engine and saved settings; failed recovery requests output selection. Active playback, recording or calibration prevents switching. Native errors do not clear unrelated analysis tasks.
- Native playback sends only the final stereo mix through a bounded PCM bridge. It silences the browser output and releases the device when idle. Recording input continues to use the selected Windows/browser capture device; native playback does not imply native ASIO input.
- Showing settings, checking environment state, or changing selected installation mode never installs anything. Installation/repair is a separate explicit action. Preparation logs are cleaned and folded behind a disclosure; an empty log never creates a strip.
- Storage statistics include actual locations, incomplete scans and unknown free space. Cache, installed models and original media are different categories.
- Cleanup begins with a preview describing file count, size and age rule. The backend returns an opaque five-minute token, checks current busy state and revalidates every candidate. It never follows links, accepts caller-provided paths, recursively removes unknown content, or deletes resumable downloads. Partial results are reported accurately.
- Diagnostics stay local: open an allowlisted application folder or copy a bounded status summary. No log upload, private music upload or telemetry.
- First-run guide, search, errors, status and confirmations support zh-CN/en, visible focus, keyboard use and theme changes. The settings heading and Back action remain visible throughout scrolling. The guide retains visible step progress, Skip and replay, and labels its illustrations as examples.

## Recovery

Failed saves retain the previous value. Failed storage scans expose retry. Changed/stale cleanup candidates survive and are reported as skipped. An active task prevents maintenance cleanup. Returning from settings restores focus to its trigger; ongoing music playback is preserved.
7 changes: 7 additions & 0 deletions assets/licenses/NAudio-LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2020 Mark Heath

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
6 changes: 6 additions & 0 deletions assets/licenses/THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ git+https://github.com/radix-ui/primitives.git
@radix-ui/react-use-is-hydrated 0.1.3 — MIT
git+https://github.com/radix-ui/primitives.git

NAudio 2.2.1 — MIT (native Windows audio output)
https://github.com/naudio/NAudio/tree/v2.2.1

Microsoft .NET 10 portable runtime — license and third-party notices bundled in native-audio/licenses
https://github.com/dotnet/runtime

Bricolage Grotesque — SIL OFL 1.1
https://github.com/google/fonts/tree/main/ofl/bricolagegrotesque

Expand Down
1 change: 1 addition & 0 deletions backend/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def decode(source, destination, sr=44100):
try:
subprocess.run([ffmpeg,'-nostdin','-v','error','-y','-i',str(source),'-vn','-t','14401','-ac','2','-ar',str(sr),'-c:a','pcm_f32le','-rf64','auto',str(temp)],check=True,creationflags=0x08000000 if os.name=='nt' else 0)
info=sf.info(temp)
if not math.isfinite(info.duration) or info.duration<=0:raise ValueError('音频中没有可读取的采样')
if info.duration>14400:raise ValueError('音频最长四小时,请先分段处理')
os.replace(temp,destination);return info
finally:temp.unlink(missing_ok=True)
Expand Down
99 changes: 97 additions & 2 deletions desktop/application.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
const { audioToolPath, ensureAudioTools } = require('./audio-tools.cjs');
const { text: t, nativeDialogs } = require('./locale.cjs');
const { app, BrowserWindow, ipcMain, dialog, protocol, net, shell } = require('electron');
const {
app,
BrowserWindow,
ipcMain,
dialog,
protocol,
net,
shell,
clipboard,
} = require('electron');
const fs = require('node:fs');
const path = require('node:path');
const { pathToFileURL } = require('node:url');
Expand Down Expand Up @@ -28,7 +37,10 @@ let win,
peerFiles,
takes,
installing = false,
pending = false;
pending = false,
modelTransfers = 0,
maintenance,
nativeAudio;
const base =
process.env.IMMERSE_DATA_DIR ||
(development
Expand Down Expand Up @@ -135,6 +147,24 @@ async function importFiles(files) {
function handle(name, fn) {
ipcMain.handle('jam:' + name, async (event, data) => {
if (event.sender !== win.webContents) throw new Error('无效调用来源');
if (
maintenance?.cleaning &&
![
'settings-storage',
'settings-diagnostics',
'ui-language',
'ui-scale',
'library',
'models',
'runtime-status',
'window',
'audio-output-info',
'audio-output-start',
'audio-output-stop',
'audio-output-write',
].includes(name)
)
throw new Error('正在清理临时内容,请稍后重试');
return fn(data || {});
});
}
Expand Down Expand Up @@ -184,6 +214,32 @@ app
backgroundThrottling: false,
},
});
const {
NativeAudio,
hostCommand,
validate: validateAudioOutput,
} = require('./native-audio.cjs');
nativeAudio = new NativeAudio({
...hostCommand(root, app.isPackaged, process.resourcesPath),
emit,
});
handle('audio-output-info', () => nativeAudio.info());
handle('audio-output-start', (data) => {
if (
takes.sessions.size &&
JSON.stringify(validateAudioOutput(data)) !== JSON.stringify(nativeAudio.lastConfig)
)
throw new Error('请先停止播放、录音或校准再切换音频系统');
return nativeAudio.start(data);
});
handle('audio-output-stop', ({ streamId }) => nativeAudio.stop(streamId));
handle('audio-output-write', (data) => nativeAudio.write(data));
win.webContents.on('render-process-gone', () => {
void nativeAudio.stop();
});
win.on('closed', () => {
void nativeAudio.stop();
});
win.removeMenu();
const emitWindowState = () => {
if (!win.isDestroyed())
Expand Down Expand Up @@ -397,6 +453,38 @@ app
}
});
const updates = new (require('./updates.cjs').Updates)(runtimeBase, app.getVersion());
maintenance = require('./settings-ipc.cjs')({
handle,
app,
shell,
clipboard,
getWindow: () => win,
roots: {
library: base,
runtime: runtimeBase,
profile: app.getPath('userData'),
logs: app.getPath('logs'),
},
isBusy: () =>
!!(
installing ||
pending ||
jobs.active ||
modelTransfers ||
updates.downloading ||
takes.sessions.size
),
getState: () => {
const status = runtimeStatus();
return {
ready: status.ready,
runtimeMode: status.mode,
installing,
processing: !!jobs.active,
modelDownloads: modelTransfers,
};
},
});
handle('update-check', () => updates.check());
handle('update-download', () =>
updates.download((event) => emit({ ...event, scope: 'update' })),
Expand Down Expand Up @@ -447,6 +535,7 @@ app
});
handle('models', () => models.list());
handle('model-download', async ({ id }) => {
modelTransfers++;
try {
emit({
scope: 'model-download',
Expand All @@ -467,6 +556,8 @@ app
} catch (error) {
emit({ scope: 'model-download', modelId: id, type: 'error', message: error.message });
throw error;
} finally {
modelTransfers--;
}
});
handle('model-source-open', ({ id, file }) => {
Expand Down Expand Up @@ -708,7 +799,11 @@ app
dialog.showErrorBox('Immerse Jam', error.message || String(error));
app.exit(1);
});
app.on('before-quit', () => {
void nativeAudio?.stop();
});
app.on('window-all-closed', () => {
void nativeAudio?.stop();
takes?.close();
jobs?.close();
transfer?.stop();
Expand Down
1 change: 1 addition & 0 deletions desktop/installation.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ async function prepare(input, services = {}) {
if (mode === 'cuda' && !services.runtime) {
const result = await require('./gpu-health.cjs').probeGpu(python);
if (!result.ready) {
if (result.detail) log({ message: 'GPU readiness check failed: ' + result.detail });
if (input.mode === 'cuda')
throw new Error('NVIDIA 驱动不兼容,请重新安装并选择 CPU 或自动模式');
mode = 'cpu';
Expand Down
Loading
Loading