# 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 74–885.**

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 74–885) thành `MasteringModal` React component trong `app.jsx`.

## Ánh xạ HTML → React (chi tiết từ spec)

### HTML lines 74–118: 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 121–161: 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 163–213: 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 216–325: 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 794–843)**: port pointerdown/move/up logic vào `useEffect` với `.knob-container` querySelectorAll, update `ozState[param]` + gọi `updateAudioGraphValues`.

---

### HTML lines 327–363: 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 366–421: 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 425–480: 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 484–585: 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 586–791: Canvas Renderers (JavaScript → React)

| Spec Canvas | React Mapping |
|---|---|
| `eqCanvas` (lines 680–738) | `useEffect` với `requestAnimationFrame` loop: draw freq grid lines, FFT spectrum bar fill từ `outputAnalyser.getByteFrequencyData`, draw composite EQ curve |
| `imagerCanvas` (lines 742–765) | `requestAnimationFrame`: draw polar circle guides + random scatter dots with `ozState.w3` width |
| `inMeterCanvas`/`outMeterCanvas` (lines 769–787) | `requestAnimationFrame`: `renderMeterBar()` with linear gradient green→yellow→red, dBFS numeric readout |
| `resizeAll()` | `useEffect` + `window.addEventListener('resize', resizeAll)` |

---

### HTML lines 793–882: Interactions & Init (JavaScript → React)

| Spec | React |
|---|---|
| `setupKnobs()` (lines 794–843) | `useEffect` on mount: query `.knob-container`, attach pointerdown/move/up, update UI rotation + `ozState` + `updateAudioGraphValues` |
| `setupModuleNavigation()` (lines 846–868) | Inline JSX click handlers thay vì event listeners |
| `window.addEventListener('load', ...)` (lines 871–882) | `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.
