FEAT: thêm tính năng MasteringModules Modal

This commit is contained in:
2026-07-29 22:03:20 +07:00
parent 47af85c072
commit 7b5d4bf27a
5 changed files with 862 additions and 4 deletions
@@ -0,0 +1,197 @@
# Implementation Plan: Ozone Mastering Module
## Nguồn DUY NHẤT để tạo giao diện
**Toàn bộ giao diện của Mastering Modal được lấy TRỰC TIẾP từ HTML trong `md/45_MASTERING_MODULE.md` dòng 74885.**
Không thiết kế giao diện mới. Không suy luận UI. Lấy chính xác:
- **Cấu trúc HTML**: copy verbatim từ spec, chuyển sang JSX (className thay class, onChange thay onchange, v.v.)
- **CSS styles**: copy verbatim từ spec (oz-panel, oz-card, oz-card-active, knob-dial, knob-pointer, gradient backgrounds)
- **JavaScript logic**: port verbatim từ spec vào React hooks (useState thay ozState, useRef thay DOM refs, useEffect thay event listeners)
- **Web Audio DSP graph**: copy y hệt node graph + connection từ spec (BiquadFilterNode, GainNode, DynamicsCompressorNode, AnalyserNode)
- **Canvas renderers**: copy y hệt drawing logic từ spec (frequency grid, FFT fill, EQ curve, polar vectorscope, VU meter gradient)
## Goal
Dịch spec HTML `md/45_MASTERING_MODULE.md` (lines 74885) thành `MasteringModal` React component trong `app.jsx`.
## Ánh xạ HTML → React (chi tiết từ spec)
### HTML lines 74118: Dependencies & Styles
| Spec | Action |
|------|--------|
| `<script src="cdn.tailwindcss.com">` | Đã có trong `index.html:9` |
| `<link FA 6.4.0>` | Đã có trong `index.html:10` (dùng lucide thay FA, giữ nguyên) |
| `<link Google Fonts Inter>` | Đã có font-family trong `index.html` style |
| CSS `.oz-panel` `oz-card` `oz-card-active` `knob-*` scrollbar | Thêm vào `index.html` `<style>` block |
| CSS `.font-mono { JetBrains Mono }` | Thêm vào `index.html` `<style>` |
**→ Sửa `app/templates/index.html`**: append ~30 dòng CSS class (oz-panel, oz-card, knob-dial, scrollbar tối)
---
### HTML lines 121161: Header Bar
| Spec Element | React JSX Mapping |
|---|---|
| Logo `.fa-atom` + "OZONE ADVANCED" | `<div>` với cùng class & lucide icon `wand-2` thay FA |
| Play/Stop buttons + preset select | JSX nguyên bản, class giữ nguyên |
| Target LUFS text | JSX nguyên bản |
**→ Trong `MasteringModal` return JSX**, copy verbatim HTML, chỉ thay `.fa-*` → lucide `i` tương ứng.
---
### HTML lines 163213: Module Chain Strip
| Spec Element | React |
|---|---|
| `#cardDynamicEq` + power toggle + icon | JSX với `onClick` → set activeModule('eq') |
| `#cardImager` + power toggle + icon | JSX với `onClick` → set activeModule('imager') |
| `#cardMaximizer` + power toggle + icon | JSX với `onClick` → set activeModule('maximizer') |
| Empty add-module slot `.border-dashed` | JSX verbatim |
| `oz-card-active` class toggle | `className={\`oz-card ${ozState.activeModule==='eq' ? 'oz-card-active' : ''}\`}` |
---
### HTML lines 216325: Dynamic EQ View
| Spec Element | React |
|---|---|
| `#viewDynamicEq` container | `className={ozState.activeModule === 'eq' ? '' : 'hidden'}` |
| `#eqCanvas` | `<canvas ref={el => canvasRefs.current.eq = el}>` |
| 4-band knobs (Low/Mid1/Mid2/High) | Mỗi band là `<div>` với `.knob-container`, `data-param`, pointer events |
| Knob dial HTML `.knob-dial > .knob-pointer` | JSX verbatim + inline `transform: rotate(Xdeg)` |
| Band labels & freq text | JSX verbatim |
**Knob interaction (spec lines 794843)**: port pointerdown/move/up logic vào `useEffect` với `.knob-container` querySelectorAll, update `ozState[param]` + gọi `updateAudioGraphValues`.
---
### HTML lines 327363: Stereo Imager View
| Spec Element | React |
|---|---|
| `#viewImager` | `className={ozState.activeModule === 'imager' ? '' : 'hidden'}` |
| `#imagerCanvas` (polar vectorscope) | `<canvas ref={...}>` |
| 4-band width sliders `#w1Slider``#w4Slider` | `<input type="range" value={ozState.w1} onChange={...}>` |
| Value spans `#w1Val``#w4Val` | `<span>{ozState.w1}%</span>` |
---
### HTML lines 366421: Maximizer View
| Spec Element | React |
|---|---|
| `#viewMaximizer` | `className={ozState.activeModule === 'maximizer' ? '' : 'hidden'}` |
| Maximizer gain knob (`.knob-dial w-24 h-24`) | JSX verbatim, pointer events |
| Ceiling level text | `<span>{ozState.ceiling.toFixed(2)} dB</span>` |
| Upward compress knob | JSX verbatim + pointer events |
| Soft clipper knob | JSX verbatim + pointer events |
| Transient emphasis knob | JSX verbatim + pointer events |
---
### HTML lines 425480: Right Sidebar (I/O Meters + Utilities)
| Spec Element | React |
|---|---|
| `#inPeakText` / `#outPeakText` | `<span>{inPeak.toFixed(1)} dB</span>` etc. |
| `#inMeterCanvas` / `#outMeterCanvas` | `<canvas ref={...}>` |
| Bypass / Gain Match / Codec / Dither buttons | JSX verbatim |
| Module active state checkboxes (power toggles) | Thêm `onClick` set `ozState.eqActive` etc. + gọi `updateAudioGraphValues` |
---
### HTML lines 484585: Audio Engine (JavaScript → React)
| Spec Function | React Mapping |
|---|---|
| `ozState` object | `const [ozState, setOzState] = useState({...})` — values copy verbatim |
| `initOzoneAudioEngine()` | `useEffect` on mount: tạo AudioContext (dùng `window.SonicAudio?.getAudioContext()` nếu có), tạo AnalyserNode (fftSize=2048), BiquadFilter nodes (lowshelf 100Hz, peaking 822Hz Q0.7, peaking 3.2kHz Q1.2, highshelf 10kHz), GainNode boost, DynamicsCompressorNode (threshold -0.1, knee 0, ratio 20, attack 0.001, release 0.05), outputAnalyser |
| Node graph connection | `inputAnalyser → eqLow → eqMid1 → eqMid2 → eqHigh → boostGain → compressor → outputAnalyser → destination` |
| `updateAudioGraphValues()` | Copy verbatim logic: `eqLowFilter.gain.setTargetAtTime(ozState.eqActive ? ozState.eqLowGain : 0, now, 0.01)` etc. |
| `startAudioDemo()` | Copy verbatim: create 4-second stereo buffer, kick/snare/synth oscillator, loop playback |
| `stopAudioDemo()` | Copy verbatim: `sourceNode.stop(); sourceNode.disconnect();` |
---
### HTML lines 586791: Canvas Renderers (JavaScript → React)
| Spec Canvas | React Mapping |
|---|---|
| `eqCanvas` (lines 680738) | `useEffect` với `requestAnimationFrame` loop: draw freq grid lines, FFT spectrum bar fill từ `outputAnalyser.getByteFrequencyData`, draw composite EQ curve |
| `imagerCanvas` (lines 742765) | `requestAnimationFrame`: draw polar circle guides + random scatter dots with `ozState.w3` width |
| `inMeterCanvas`/`outMeterCanvas` (lines 769787) | `requestAnimationFrame`: `renderMeterBar()` with linear gradient green→yellow→red, dBFS numeric readout |
| `resizeAll()` | `useEffect` + `window.addEventListener('resize', resizeAll)` |
---
### HTML lines 793882: Interactions & Init (JavaScript → React)
| Spec | React |
|---|---|
| `setupKnobs()` (lines 794843) | `useEffect` on mount: query `.knob-container`, attach pointerdown/move/up, update UI rotation + `ozState` + `updateAudioGraphValues` |
| `setupModuleNavigation()` (lines 846868) | Inline JSX click handlers thay vì event listeners |
| `window.addEventListener('load', ...)` (lines 871882) | `useEffect` on mount: init audio engine, setup event handlers, start canvas renderers |
---
## Task List
### Task 1: Append CSS classes vào app/templates/index.html
Thêm ~30 dòng CSS vào `<style>` block (`index.html:28-245`):
- `.oz-panel` `oz-card` `oz-card-active`
- `.knob-container` `.knob-dial` `.knob-pointer`
- Scrollbar dark theme
- `.font-mono` JetBrains Mono override
- Gradient panel backgrounds
### Task 2: Thêm state + menu item trong app.jsx
- `showMasteringModal` state (dòng 7708)
- Menu item "Mastering Suite" (dòng 15585)
- Render `MasteringModal` (dòng 18664)
### Task 3: Viết MasteringModal component trong app.jsx (trước dòng 6694)
~500 dòng gồm:
**3a. State**: `ozState` (activeModule, gains, widths, maximizer params), `isPlaying`, `canvasRefs`, `audioRefs`
**3b. useEffect (mount)**: `initOzoneAudioEngine()` — copy verbatim từ spec lines 529-584, dùng `window.SonicAudio?.getAudioContext()` fallback
**3c. useEffect (mount)**: `setupKnobs()` — port pointer events từ spec lines 794-843, gọi `updateAudioGraphValues()` khi giá trị thay đổi
**3d. useEffect (mount + deps)**: Canvas render loop — port `startCanvasRenderers()` từ spec lines 650-791, sử dụng `ozState` thay vì global
**3e. useEffect (cleanup)**: Stop audio + disconnect nodes khi unmount
**3f. JSX return**: ~250 dòng JSX, dịch sát spec HTML lines 121-480:
- Overlay modal container `fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm`
- Bên trong: `flex flex-col` container với `h-[90vh] w-[90vw] max-w-[1400px]`
- Header bar (logo, transport, preset select, LUFS)
- Module chain strip (3 module cards + add slot, `oz-card-active` toggle)
- Main workspace:
- Sub toolbar (Delta listen, IRC mode select, Learn Input Gain)
- `viewDynamicEq` (EQ canvas + 4-band knob panel, show khi `activeModule==='eq'`)
- `viewImager` (vectorscope canvas + 4-band sliders, show khi `activeModule==='imager'`)
- `viewMaximizer` (gain knob, upward comp, soft clip, transient knobs, show khi `activeModule==='maximizer'`)
- Right sidebar I/O meters (in/out peak text, meter canvases, bypass/gain match/codec/dither buttons)
- Close button (góc trên phải)
### Task 4: Build & Verify
```bash
npm run build
```
Kiểm tra: mở app → menu → "Mastering Suite" → modal hiển thị, Play Reference Loop phát âm thanh, canvas vẽ, knobs kéo được.
## Files Affected
| File | Lines | Change |
|------|-------|--------|
| `app/templates/index.html` | +30 | Thêm CSS classes (oz-panel, oz-card, knob-dial, scrollbar) |
| `app/static/js/app.jsx` | ~dòng 6690 + 7708 + 15585 + 18664 | Thêm MasteringModal component + state + menu item + render |
## Skipped (PONYTALL)
- Real M/S processing: phase 1 chỉ UI + vectorscope viz. Thêm actual M/S khi user upload file stereo thật.
- Delta Listen / Gain Match / Codec / Dither: UI button renders, DSP stubbed. Thêm sau.
- Drag-drop module reorder: spec define UX. Phase 1: fixed chain.
- Preset save/load: UI dropdown + defaults. Backend integration sau.