Skip to content

Latest commit

 

History

History
281 lines (227 loc) · 19.3 KB

File metadata and controls

281 lines (227 loc) · 19.3 KB

PLVS — Architecture

文档用途:PLVS 技术地图——技术栈选型、目录结构、音频管线、前后端通信、主题系统。
面向读者:项目作者 + AI agent。以 main 分支实现 为准;文档与代码冲突时以代码为准。
配套文档:产品范围见 prd.md;UI token 细节见 design-tokens.md;架构决策见 adr/


1. 技术栈

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)。


2. 系统架构

┌─────────────────────────────────────────────┐
│              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

数据流:

  1. 采集:Rust 通过 cpal(Windows WASAPI Loopback + 物理输入)或 macOS Core Audio Tap 获取 PCM。
  2. DSP:音频线程并行计算 Peak / True Peak / LUFS / FFT spectrum / correlation。
  3. 推送:高频指标(~60Hz)→ Tauri Channel;低频状态(~2Hz)→ Tauri Event。
  4. 渲染:React 订阅数据更新面板。
  5. 控制:前端按钮(START/STOP/设备切换)→ invoke 调 Rust command。

3. 目录结构

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

4. 音频管线

采集层(src-tauri/src/audio/

  • Windowscpal_backend.rs 通过 cpal 打开 WASAPI Loopback——无需虚拟声卡,直接读系统输出 PCM。物理输入也走 cpal。
  • macOS:系统音频走 macos/(Core Audio process tap,需 macOS 14.2+);物理输入走 cpal。平台分发由 platform_backend.rs 处理。

DSP 层(src-tauri/src/dsp/

模块 计算
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 + 相关系数

编排层(src-tauri/src/engine/meter_pipeline.rs

PCM 帧 → 并行 DSP → 打包 MeteringFrame → Channel(~60Hz)推前端;慢速响度 / 状态 → Event(~2Hz)广播。

History cadence(节奏分层)

核心契约:源 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 模式FilePcmHistoryChunkerfile_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 共享上述契约,但不共享代码路径。

5. 前后端通信(IPC)

前端 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


6. 主题与 Token 系统

首屏渲染流程(src/main.jsx):

  1. 通过 settingsStore 读取 appearancesystem|fixed)与 themeId(Tauri 下由 Rust 预注入 window.__PLVS_INITIAL_STATE__,浏览器开发环境走 localStorage
  2. resolveThemeId(结合 prefers-color-scheme)→ 当前 themeId
  3. getBuiltinTheme(themeId) → 主题对象(含 colorScheme
  4. applyLayoutToDocument(UI_PREFERENCES):布局 / 字号 / 几何 / 非调色变量
  5. applyThemeToDocument(themeId)data-themecolor-scheme、shadcn semantic tokens、seed 派生的 instrument color tokens、Peak 渐变色

Token 分层(详见 design-tokens.md 与 ADR 0001/0002):

CSS 变量前缀 定义位置
shadcn 语义(表面色) --background, --foreground, --primary builtinThemes.jsapplyThemeToDocument
UI 布局 --ui-* data.jsapplyLayoutToDocument
Instrument 色 --ui-loudness-*, --ui-spectrum-*, --ui-vectorscope-*, --ui-waveform-*, --ui-signal-*, --ui-meter-gradient-* builtinThemes.js seeds → buildThemeTokens.jsapplyThemeToDocument
Spectrogram colormap JS LUT, not CSS vars builtinThemes.js colormap → spectrogramColormap.jsuseSpectrogramCanvas

首屏占位变量由 npm run theme:generate 写入 src/generated/theme-fallbacks.css(与默认暗色语义同源)。


7. 关键术语

术语 定义
WASAPI Loopback Windows 原生 API:把输出设备当输入读,无需虚拟声卡
Core Audio Tap macOS 14.2+ 原生系统音频捕获(等效于 Windows WASAPI Loopback)
realtime-safe 音频回调线程不做内存分配、不加锁、不 syscall
Channel Tauri 高频单向推送通道(~60Hz 指标帧)
plvs:settings 持久化全局偏好:appearancethemeIdreferenceLufs、面板标签覆盖等
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 三窗口边界

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 交互边界(有意精简)

Dock 只复用普通面板 live 交互的一个子集,其余是刻意不做,不是遗漏——要更细的调节或翻历史就进普通模式 / snapshot。别对着代码反复核对,现状如下:

Dock 有的:

  • TP-max resetDockLevel,点读数复位真峰值最大值)
  • Vectorscope peak-hold resetDockVectorscope polarLevel,点图复位峰值保持)
  • 时间窗缩放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)成本高回报低,均评估后不做。

8. 平台说明

平台 系统音频路径 最低版本
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