文档用途:PLVS 技术地图——技术栈选型、目录结构、音频管线、前后端通信、主题系统。
面向读者:项目作者 + AI agent。以 main 分支实现 为准;文档与代码冲突时以代码为准。
配套文档:产品范围见prd.md;UI token 细节见design-tokens.md;架构决策见adr/。
Tauri 2 + Rust(后端)+ React 19 / Vite(前端)
| 层 | 技术 | 职责 |
|---|---|---|
| 桌面壳 | Tauri 2 | 系统 WebView、进程管理、平台 API 桥接 |
| 音频引擎 | Rust + cpal / Core Audio | PCM 采集、DSP 计算(peak / LUFS / FFT / vectorscope) |
| 前端 UI | React 19 + Vite + Tailwind CSS v4 | 面板渲染、布局、设置 |
| 组件库 | shadcn/ui (Radix) | 壳与设置控件 |
| 测试 | Vitest | 前端单元测试(与源文件同目录,*.test.js/jsx) |
为什么是 Rust + Tauri: Rust 的无 GC 特性保证音频回调线程 realtime-safe;Tauri 用系统 WebView(无需打包 Chromium,安装包 ~10MB);现有 React 组件完整复用。Electron 与 JUCE 因体积/定位问题被否决(详见原 architecture.md 历史与 ADR)。
┌─────────────────────────────────────────────┐
│ Tauri Application │
│ │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ Frontend │◄──────│ Rust Backend │ │
│ │ React + Vite │Channel│ │ │
│ │ │ ~60Hz │ Audio Engine │ │
│ │ • Panels │ │ ┌───────────┐ │ │
│ │ • Controls │◄──────│ │ cpal / │ │ │
│ │ • Settings │ Event │ │ Core Audio│ │ │
│ │ │ ~2Hz │ └─────┬─────┘ │ │
│ │ │──────►│ ▼ │ │
│ │ │invoke │ DSP Pipeline │ │
│ └──────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────┘
▲
│ WASAPI Loopback (Windows) / Core Audio Tap (macOS 14.2+)
System Audio
数据流:
- 采集:Rust 通过 cpal(Windows WASAPI Loopback + 物理输入)或 macOS Core Audio Tap 获取 PCM。
- DSP:音频线程并行计算 Peak / True Peak / LUFS / FFT spectrum / correlation。
- 推送:高频指标(~60Hz)→ Tauri Channel;低频状态(~2Hz)→ Tauri Event。
- 渲染:React 订阅数据更新面板。
- 控制:前端按钮(START/STOP/设备切换)→
invoke调 Rust command。
PLVS/
├── index.html # Vite entry
├── package.json / vite.config.js / eslint.config.js
├── scripts/
│ ├── generate-theme-fallbacks.mjs # writes src/generated/theme-fallbacks.css
│ └── verify-versions.mjs # checks package.json / Cargo.toml / tauri.conf.json
│
├── src/ # React frontend
│ ├── main.jsx # React mount + first-paint theme apply
│ ├── App.jsx # Shell: workspace layout + panel routing
│ ├── index.css # Global styles + first-paint token fallbacks
│ ├── meterHealth.js # Metering health state
│ ├── uiPreferences.js # Re-export of UI_PREFERENCES
│ │
│ ├── components/
│ │ ├── panels/ # LevelMeterPanel, LoudnessPanel, StatsPanel, SpectrumPanel,
│ │ │ # SpectrogramPanel, VectorscopePanel, WaveformPanel
│ │ └── ui/ # shadcn/ui components (Button, Dialog, Select…)
│ │
│ ├── config/
│ │ ├── scales.js # Scale definitions shared with Rust DSP
│ │ └── loudnessReferenceProfiles.js
│ │
│ ├── hooks/ # useAudioEngine, useSettings, useLayoutDrag,
│ │ # useHistoryInteraction, useSnapshot, useCanvasSize…
│ │
│ ├── ipc/ # ★ single entry point for all frontend↔backend comms
│ │ ├── commands.js # all invoke() calls
│ │ ├── events.js # listen + Channel subscriptions
│ │ ├── capturePrefs.js # device preference (Tauri store + localStorage)
│ │ ├── fileDialog.js # file-mode open dialog (File analysis)
│ │ ├── openExternal.js # open release/update links in system browser
│ │ ├── env.js # isTauri() helper
│ │ └── types.js # shared IPC type definitions
│ │
│ ├── lib/ # engine integration helpers
│ │ ├── FrameIntake.js # high-frequency frame ingestion + ring buffer
│ │ ├── audioEngineCommands.js # start/stop/device abstraction over IPC
│ │ ├── shellLayout.js # layout computation helpers
│ │ ├── tauriFrameApply.js # frame → React state bridge
│ │ └── … # updateCheck, audioDeviceLabels, utils
│ │
│ ├── math/ # pure functions: history paths, format, spectrum math
│ │
│ ├── preferences/
│ │ ├── data.js # UI_PREFERENCES constant (layout, typography, radii)
│ │ ├── themeResolve.js # resolveThemeId, parsePersistedUiStateJson
│ │ ├── applyDocumentTheme.js # applyLayoutToDocument, applyThemeToDocument
│ │ └── layoutPersistence.js # layout localStorage read/write
│ │
│ ├── theme/
│ │ ├── builtinThemes.js # BUILTIN_THEMES, THEME_IDS, semantic + seeds + colormap
│ │ ├── buildThemeTokens.js # derives instrument color CSS vars from theme seeds
│ │ ├── spectrogramColormap.js # spectrogram stop lists + LUT builder
│ │ └── shadcnSemanticPreset.js # shadcn semantic token preset
│ │
│ ├── workspace/ # split-tree workspace layout system
│ │ ├── constants.js # WORKSPACE_STORAGE_KEY, DEFAULT_TREE, BUILTIN_PRESETS
│ │ ├── reducer.js # workspace state reducer
│ │ ├── SplitLayout.jsx # draggable split container
│ │ ├── LeafView.jsx # tab container for panels
│ │ └── … # context, toolbar, registry, treeUtils, types
│ │
│ ├── dock/ # dock mode: meter strip + accessory WebViews
│ │ │ # (spec: docs/superpowers/specs/2026-07-13-dock-accessory-windows-design.md)
│ │ ├── accessories/ # header/editor presentation roots (no runtime ownership)
│ │ ├── editors/ # vertical modules, presets, and settings drill-in
│ │ ├── registry.jsx # compact module catalog and settings metadata
│ │ ├── DockStrip.jsx # reserved 72px meter-only strip
│ │ └── … # protocol, visibility, controls, analysis requests, modules/
│ │
│ └── generated/
│ └── theme-fallbacks.css # auto-generated by npm run theme:generate
│
├── src-tauri/
│ ├── Cargo.toml # package name: plvs
│ ├── tauri.conf.json # productName: PLVS, identifier: com.soundoer.plvs
│ ├── capabilities/default.json # Tauri 2 permission declarations
│ ├── native/macos/tap_bridge.m # Core Audio process tap (macOS build only)
│ └── src/
│ ├── main.rs / lib.rs / state.rs / profile.rs / vad.rs / window_state.rs
│ ├── audio/ # capture layer: cpal_backend, platform_backend, macos/
│ ├── dsp/ # peak, loudness, spectrum, spectrum_bank, vectorscope, speech (VAD), dialogue, filters
│ ├── file_analysis/ # File mode: ffmpeg probe/decode + session history
│ ├── engine/meter_pipeline.rs # PCM → metering frames → Channel/Event push
│ └── ipc/ # commands.rs, events.rs, types.rs
│
└── docs/ # see docs/README.md
- Windows:
cpal_backend.rs通过cpal打开 WASAPI Loopback——无需虚拟声卡,直接读系统输出 PCM。物理输入也走 cpal。 - macOS:系统音频走
macos/(Core Audio process tap,需 macOS 14.2+);物理输入走 cpal。平台分发由platform_backend.rs处理。
| 模块 | 计算 |
|---|---|
peak.rs |
采样峰值 + True Peak(4× 过采样) |
loudness.rs |
K-weighting → gate → M / S / I / LRA(ITU-R BS.1770 / EBU R128) |
spectrum.rs |
rFFT + Hann 窗,hop=N/4,4 帧非相干平均;带内能量按 Hz 连续边界与 bin 分数重叠积分(非整数 bin 截断) |
vectorscope.rs |
L/R → XY + 相关系数 |
PCM 帧 → 并行 DSP → 打包 MeteringFrame → Channel(~60Hz)推前端;慢速响度 / 状态 → Event(~2Hz)广播。
核心契约:源 chunk 大小可以影响 CPU 批处理,但绝不决定 history 时长。 三种节奏各司其职,别混用:
| 节奏 | 周期 | 来源 | 前端契约 |
|---|---|---|---|
| main history | 100 ms(~10Hz) | MeterHistoryEntry,每 100 ms 一行 |
HIST_SAMPLE_SEC = 0.1(index-grid 定位) |
| visual history | 40 ms(~25Hz) | VisualHistEntry / VISUAL_EMIT_MS |
VISUAL_HIST_SAMPLE_SEC = 0.04(spectrogram 按 timestamp 定位) |
| UI frame | 交付节奏 | FRAME_EMIT_MS = 16,仅 UI 投递 + 背压 |
不是分析节奏,不得用来定义 history 行数 |
HIST_EMIT_MS = 95是 live 的 wall-clock 容差门(允许名义 100 ms 块在略低于 100 ms 时也能发),不是语义周期。- File 模式:
FilePcmHistoryChunker(file_analysis/session.rs)把任意大小的 ffmpeg PCM 整流成每次一个 100 ms 块喂给 pipeline,使 history 行数只由媒体时长决定。设计见specs/2026-06-29-sample-clocked-history-cadence-design.md。 - 已知限制:file 模式 visual history 被 100 ms 块卡在 ~10Hz(live 为 ~25Hz)。因 spectrogram 按 timestamp 贴帧,时间轴仍正确,只是分辨率更粗。
- Live 不走 chunker:采集源在 realtime-safe 回调线程,不能做 chunker 的 buffer 分配;live 与 file 共享上述契约,但不共享代码路径。
前端 IPC 的唯一入口是 src/ipc/;绝不在组件或 hook 里直接调 @tauri-apps/api。
| 通道 | 方向 | 频率 | 用途 |
|---|---|---|---|
| Channel | Rust → Frontend | ~60Hz | 高频指标帧(peak、LUFS M/S、spectrum path、vectorscope) |
| Event | Rust → Frontend | ~2Hz | 慢速响度(I/LRA)、设备状态、健康状态 |
| invoke (command) | Frontend → Rust | 用户触发 | START/STOP、设备切换、设置写入 |
Rust command 定义在 src-tauri/src/ipc/commands.rs;前端调用封装在 src/ipc/commands.js。
首屏渲染流程(src/main.jsx):
- 通过
settingsStore读取appearance(system|fixed)与themeId(Tauri 下由 Rust 预注入window.__PLVS_INITIAL_STATE__,浏览器开发环境走localStorage) resolveThemeId(结合prefers-color-scheme)→ 当前themeIdgetBuiltinTheme(themeId)→ 主题对象(含colorScheme)applyLayoutToDocument(UI_PREFERENCES):布局 / 字号 / 几何 / 非调色变量applyThemeToDocument(themeId):data-theme、color-scheme、shadcn semantic tokens、seed 派生的 instrument color tokens、Peak 渐变色
Token 分层(详见 design-tokens.md 与 ADR 0001/0002):
| 层 | CSS 变量前缀 | 定义位置 |
|---|---|---|
| shadcn 语义(表面色) | --background, --foreground, --primary… |
builtinThemes.js → applyThemeToDocument |
| UI 布局 | --ui-* |
data.js → applyLayoutToDocument |
| Instrument 色 | --ui-loudness-*, --ui-spectrum-*, --ui-vectorscope-*, --ui-waveform-*, --ui-signal-*, --ui-meter-gradient-* |
builtinThemes.js seeds → buildThemeTokens.js → applyThemeToDocument |
| Spectrogram colormap | JS LUT, not CSS vars | builtinThemes.js colormap → spectrogramColormap.js → useSpectrogramCanvas |
首屏占位变量由 npm run theme:generate 写入 src/generated/theme-fallbacks.css(与默认暗色语义同源)。
| 术语 | 定义 |
|---|---|
| WASAPI Loopback | Windows 原生 API:把输出设备当输入读,无需虚拟声卡 |
| Core Audio Tap | macOS 14.2+ 原生系统音频捕获(等效于 Windows WASAPI Loopback) |
| realtime-safe | 音频回调线程不做内存分配、不加锁、不 syscall |
| Channel | Tauri 高频单向推送通道(~60Hz 指标帧) |
| plvs:settings | 持久化全局偏好:appearance、themeId、referenceLufs、面板标签覆盖等 |
| plvs:workspace | 持久化工作区布局树与每个 panel 的控制状态 |
| plvs:presets | 持久化用户保存的 workspace presets |
| plvs:themes | 持久化自定义主题 |
| windowBounds | plvs-settings.json 顶层键,由 Rust 独立维护窗口几何,避免 JS settings 写回覆盖 |
| dockState | plvs-settings.json 顶层键(Rust 维护):dock 模式开关 / 贴边 / 显示器;dock 期间 windowBounds 停写 |
Dock form 由三个 native window 组成:main 是 56-160 logical px、默认 72px 的 meter strip,
dock-header 是 44 logical px 的全宽临时工具条,dock-editor 是右对齐的单列编辑器。
Windows 上只有 main 注册 AppBar;开启 Reserve screen space 时也只保留 meter strip,
header/editor 始终覆盖相邻工作区,因此 hover 不会触发 maximized window 重排。
Rust 的 dock.rs / dock_accessories.rs 拥有物理像素几何和窗口生命周期。主 React root
拥有 runtime、workspace、preset 和 Dock persistence;两个 accessory root 仅接收可序列化快照,
并通过 semantic Tauri events 回传 action/pointer,不挂载 audio intake 或持久化 store owner。
Dock panel display controls 位于 workspaceStore.dock.controlsByPanelId,独立于正常 workspace 的
panelControlsById;旧 controlsByModuleId 仅作为兼容字段保留。measurement runtime 和 channel label
semantics 仍共享。
Dock 只复用普通面板 live 交互的一个子集,其余是刻意不做,不是遗漏——要更细的调节或翻历史就进普通模式 / snapshot。别对着代码反复核对,现状如下:
Dock 有的:
- TP-max reset(
DockLevel,点读数复位真峰值最大值) - Vectorscope peak-hold reset(
DockVectorscopepolarLevel,点图复位峰值保持) - 时间窗缩放(
DockWaveform/DockLoudness/DockSpectrogram:滚轮缩放 + 右键双击复位 + 窗口秒数 HUD)。实现是普通useHistoryInteraction的精简子集,另走useDockHistoryViewport,只有窗宽、无时间偏移、缩放不以光标为锚。
Dock 刻意没有的:
- 数值轴缩放/平移(普通模式
useAxisInteraction提供 Level/Spectrum/Spectrogram/Loudness 的轴缩放)——dock 条太窄,无可抓的轴。 - 时间轴平移到历史 / scrub 选择——dock 是 live-only viewport,翻历史交给 snapshot。
- Spectrum 长按稳曲线、Vectorscope hold-slow 拖影——两个「按住」手势;前者可行但收益低,后者一半已由 dock 常驻的 correlation 平滑覆盖、另一半(Lissajous phosphor)成本高回报低,均评估后不做。
| 平台 | 系统音频路径 | 最低版本 |
|---|---|---|
| Windows | WASAPI Loopback(cpal) | Windows 10+(WebView2 required) |
| macOS | Core Audio process tap | macOS 14.2+(tap 能力要求) |
macOS 低于 14.2 或无 tap 能力时的回退行为以代码实现为准。免签名安装摩擦(Gatekeeper / SmartScreen)的用户说明见 README.md。