Files
SonicForgeStudio/md/45_MASTERING_MODULE.md
T

43 KiB

TECHNICAL SPECIFICATION & OPERATIONAL WORKFLOW MANUAL: OZONE WEB MASTERING SUITE

This document compiles the complete architectural layout, module inventory, control parameter specifications, dynamic re-routing execution matrices, visual metering systems, and full single-file implementation code for the Ozone-Style Web Mastering Engine.


1. AUDIO PROCESSING MODULES SUMMARY

Module Name Core Function Web Audio API Nodes / Class Key Control Parameters Inherited Acoustic Impact
Dynamic EQ Dynamic frequency filtering and equalization BiquadFilterNode (lowshelf, peaking, highshelf) 4-Band Gain (-12 \text{ to } +12\text{ dB}), Frequency, Q Factor Balances frequency spectrum, carves out harsh resonances, or boosts air/warmth dynamically without introducing harsh clipping.
Stereo Imager Mid/Side spatial expansion and balance ChannelSplitterNode, ChannelMergerNode, GainNode 4-Band Width (-100\% \text{ to } +100\%), Mid/Side Gain Expands stereo width across High/Mid bands while collapsing Low/Sub-bass bands to Mono for maximum low-end punch.
IRC IV Maximizer True-Peak Limiter & Peak Gain Boosting WaveShaperNode (Soft Clip), DynamicsCompressorNode Boost Gain (+0 \text{ to } +12\text{ dB}), Soft Clip %, Upward Comp, Ceiling Maximizes perceived loudness (LUFS) to commercial distribution standards while suppressing transient spikes below 0\text{ dBFS}.
Master Utilities I/O Management, Metering & Codec Emulation AnalyserNode (FFT 2048), GainNode Delta Listen, Gain Match, Codec Preview, Dither Enables objective A/B listening tests between pre/post-mastered signals, emulates lossy MP3/AAC compression, and manages bit-depth reduction.

2. PARAMETER CONTROL SPECIFICATIONS MATRIX

Module Group Parameter Name Range Unit Node Handler / Formula Technical Purpose
Dynamic EQ eqLowGain -12.0 \to +12.0 dB Lowshelf @ 100\text{Hz} Controls low-end punch and sub-bass weight.
Dynamic EQ eqMid1Gain -12.0 \to +12.0 dB Peaking @ 822\text{Hz}, Q=0.7 Cleans up muddy or boxy low-mid frequencies.
Dynamic EQ eqMid2Gain -12.0 \to +12.0 dB Peaking @ 3.2\text{kHz}, Q=1.2 Adjusts vocal clarity and instrument presence.
Dynamic EQ eqHighGain -12.0 \to +12.0 dB Highshelf @ 10\text{kHz} Adds high-frequency air, sheen, and sparkle.
Stereo Imager w1 (Band 1) -100 \to +100 % Mid/Side Gain (0-100\text{Hz}) Collapses sub-bass to mono (0\%) to ensure club system compatibility.
Stereo Imager w2 (Band 2) -100 \to +100 % Mid/Side Gain (100\text{Hz}-1\text{kHz}) Balances body and width for core instruments.
Stereo Imager w3 (Band 3) -100 \to +100 % Mid/Side Gain (1\text{kHz}-6\text{kHz}) Expands upper-mid imaging for a wider soundstage.
Stereo Imager w4 (Band 4) -100 \to +100 % Mid/Side Gain (6\text{kHz}-20\text{kHz}) Widens top-end spatial atmosphere.
Maximizer maxGain 0.0 \to +12.0 dB Gain Boost Node Drives the signal into the limiter to increase overall LUFS.
Maximizer maxUpward 0.0 \to +10.0 dB Dynamic Range Expander Brings up low-level details for a denser mix.
Maximizer maxSoftClip 0 \to 100 % Arctan WaveShaper Curve Rounds off sharp transient peaks to add analog-style saturation.
Maximizer ceiling -6.0 \to 0.0 dBFS Brickwall Limiter Threshold Absolute ceiling threshold (Default: -0.10\text{ dBFS}).

3. AUDIONODE GRAPH RE-ROUTING EVENT MATRIX

UI Action Triggered Event Audio Engine Method (OzoneAudioChainManager) Node Graph Execution Perceived Acoustic Effect
Add Module ([+] Add) Click Add Module button addModuleToChain(moduleName) Appends module to moduleChain, disconnects existing routing, and re-connects all nodes in sequence. Introduces additional processing step to the signal chain prior to Master Out.
Remove Module ([x] Remove) Click Remove Module button removeModuleFromChain(moduleName) Filters module out of moduleChain, disconnects old connections, and connects remaining nodes directly. Bypasses the removed module's processing, reducing latency and CPU load.
Reorder Chain Drag & Drop Module Card reorderChain(newArray) Disconnects the entire graph and rebuilds connections from scratch based on the new array order. Alters processing order (e.g., Stereo Imager widening prior to EQ vs. post-EQ).
Toggle Module Power Click Module Power button state.moduleActive = !active; updateAudioParameters(); Sets target band gains to 0\text{ dB} or bypasses signal around module. Passes signal through the target module without tonal modification (flat response).
Master Bypass Click Master Bypass button toggleMasterBypass() Reroutes input directly: inputAnalyser \to masterBypassGain \to outputAnalyser. Instantly returns signal to raw, unprocessed dry audio for A/B comparison.

4. GRAPHICAL METERS & VISUALIZER CANVAS SYSTEMS

Visualizer Canvas Source Audio Node Rendering Math / Algorithm Graphical Characteristics & Refresh Rate
Dynamic EQ Spectrum outputAnalyser.getByteFrequencyData() Logarithmic Frequency Scaling (20\text{Hz} - 20\text{kHz}) Cyan composite EQ curve overlaid across a live 60 FPS FFT frequency spectrum fill.
Polar Vectorscope Channel Splitter (Independent L/R Signals) Polar Coordinate Calculation (\theta = \arctan(R/L), radius r) 180° radial particle scatter plot depicting phase correlation and stereo width.
I/O Stereo VU Meters inputAnalyser & outputAnalyser Peak Calculation (dB = 20 \log_{10}(\text{level})) using Time-Domain data Vertical Green \to Yellow \to Red gradient meters with real-time numeric dBFS readouts.

5. MASTER UTILITIES COMPARISON

Utility Feature DSP Mechanism / Formula Operational Purpose in Mastering
Delta Listen ([i] Delta) S_{\text{Delta}}(t) = S_{\text{Input}}(t) - S_{\text{Output}}(t) Isolates the exact signal difference being removed or compressed to prevent over-processing.
Gain Match G_{\text{Match}} = \text{LUFS}_{\text{Input}} - \text{LUFS}_{\text{Output}} Automatically matches output loudness to input level to enable objective A/B evaluations free from "louder is better" bias.
Codec Preview Low-pass Biquad Filter @ 16\text{kHz} Simulates lossy high-frequency roll-off typical of 128 kbps MP3/AAC encoding.
Dither Processor Triangular PDF Noise Injection Prevents quantization distortion when converting bit-depth from 32-bit Float down to 16-bit PCM.

6. FULL SINGLE-FILE IMPLEMENTATION CODE

Below is the complete single-file source code incorporating HTML5, Tailwind CSS, FontAwesome, HTML5 Canvas, and Web Audio API DSP engines modeling the iZotope Ozone mastering interface:

<!DOCTYPE html>
<html lang="en" class="h-full bg-slate-950 text-slate-100 select-none">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Ozone Mastering Studio Engine</title>
  <!-- Tailwind CSS -->
  <script src="https://cdn.tailwindcss.com"></script>
  <!-- Font Awesome -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  <!-- Fonts -->
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
  
  <style>
    body { font-family: 'Inter', sans-serif; }
    .font-mono { font-family: 'JetBrains Mono', monospace; }
    
    ::-webkit-scrollbar { width: 4px; height: 4px; }
    ::-webkit-scrollbar-track { background: #080b12; }
    ::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 2px; }
    ::-webkit-scrollbar-thumb:hover { background: #334155; }

    .knob-container { touch-action: none; user-select: none; }
    .knob-dial {
      transition: transform 0.05s ease-out;
      box-shadow: inset 0 1px 3px rgba(255,255,255,0.15), 0 3px 8px rgba(0,0,0,0.8);
    }
    
    /* Ozone Theme Dark Gradient Panels */
    .oz-panel {
      background: linear-gradient(180deg, #131822 0%, #0d1118 100%);
      border: 1px solid rgba(255, 255, 255, 0.07);
    }
    .oz-card {
      background: #18202c;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }
    .oz-card-active {
      background: #1e2a3a;
      border-color: #38bdf8;
      box-shadow: 0 0 12px rgba(56, 189, 248, 0.2);
    }
  </style>
</head>
<body class="h-full flex flex-col overflow-hidden bg-slate-950 text-slate-200">

  <!-- TOP TRANSPORT & SESSION BAR -->
  <header class="h-12 bg-slate-900/90 border-b border-slate-800/80 flex items-center justify-between px-4 z-30">
    <div class="flex items-center gap-3">
      <div class="w-7 h-7 rounded-lg bg-cyan-600 flex items-center justify-center font-bold text-white shadow-lg shadow-cyan-950">
        <i class="fa-solid fa-atom text-xs"></i>
      </div>
      <div>
        <h1 class="text-xs font-bold tracking-wider text-white flex items-center gap-2">
          OZONE ADVANCED <span class="text-[9px] bg-cyan-950 text-cyan-400 border border-cyan-800/80 px-1.5 py-0.5 rounded font-mono">WEB MASTERING V10.5</span>
        </h1>
      </div>
    </div>

    <!-- Central Signal Generator & Transport -->
    <div class="flex items-center gap-3 bg-slate-950 px-3 py-1 rounded-lg border border-slate-800/80">
      <button id="playAudioBtn" class="px-3 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1.5 transition-all shadow-md shadow-cyan-950">
        <i class="fa-solid fa-play text-[10px]"></i> <span>Play Reference Loop</span>
      </button>
      <button id="stopAudioBtn" class="px-3 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs font-semibold flex items-center gap-1.5 transition-all">
        <i class="fa-solid fa-stop text-[10px]"></i> <span>Stop</span>
      </button>

      <div class="h-4 w-[1px] bg-slate-800 mx-1"></div>

      <div class="flex items-center gap-2 text-xs font-mono">
        <span class="text-slate-400 text-[11px]">Preset:</span>
        <select id="masterPresetSelect" class="bg-slate-900 border border-slate-700 rounded px-2 py-0.5 text-xs text-cyan-300 outline-none focus:border-cyan-500">
          <option value="adaptive">Adaptive Dynamic Master</option>
          <option value="edm_club">EDM / Club Punch Maximizer</option>
          <option value="wide_space">Cinematic Stereo Expansion</option>
          <option value="transparent">Transparent High-Clarity Limiter</option>
        </select>
      </div>
    </div>

    <!-- Right Target Meter Info -->
    <div class="flex items-center gap-3 text-xs font-mono">
      <span class="text-slate-400 text-[11px]">Target LUFS:</span>
      <span id="targetLufsText" class="text-cyan-400 font-bold bg-slate-950 border border-cyan-900/60 px-2 py-0.5 rounded">-11.0 LUFS</span>
    </div>
  </header>

  <!-- MODULE RACK STRIP (OZONE UPPER MODULE BAR) -->
  <div class="h-16 bg-slate-950 border-b border-slate-800 px-4 flex items-center gap-2 overflow-x-auto">
    <span class="text-[10px] font-bold text-slate-500 uppercase tracking-widest mr-1 font-mono">MODULE CHAIN:</span>

    <!-- Module 1: Dynamic EQ Card -->
    <div id="cardDynamicEq" class="oz-card oz-card-active w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all">
      <div class="flex items-center gap-2">
        <button id="toggleEqBtn" class="w-4 h-4 rounded-full bg-cyan-500 hover:bg-cyan-400 text-slate-950 flex items-center justify-center text-[9px] font-bold">
          <i class="fa-solid fa-power-off"></i>
        </button>
        <div>
          <div class="text-[11px] font-bold text-slate-200">Dynamic EQ</div>
          <div class="text-[9px] text-cyan-400 font-mono">4-Band Peak</div>
        </div>
      </div>
      <i class="fa-solid fa-chart-area text-slate-500 text-xs"></i>
    </div>

    <!-- Module 2: Stereo Imager Card -->
    <div id="cardImager" class="oz-card w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all">
      <div class="flex items-center gap-2">
        <button id="toggleImagerBtn" class="w-4 h-4 rounded-full bg-cyan-500 hover:bg-cyan-400 text-slate-950 flex items-center justify-center text-[9px] font-bold">
          <i class="fa-solid fa-power-off"></i>
        </button>
        <div>
          <div class="text-[11px] font-bold text-slate-200">Imager</div>
          <div class="text-[9px] text-slate-400 font-mono">4-Band Width</div>
        </div>
      </div>
      <i class="fa-solid fa-circle-nodes text-slate-500 text-xs"></i>
    </div>

    <!-- Module 3: Maximizer Card -->
    <div id="cardMaximizer" class="oz-card w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all">
      <div class="flex items-center gap-2">
        <button id="toggleMaximizerBtn" class="w-4 h-4 rounded-full bg-cyan-500 hover:bg-cyan-400 text-slate-950 flex items-center justify-center text-[9px] font-bold">
          <i class="fa-solid fa-power-off"></i>
        </button>
        <div>
          <div class="text-[11px] font-bold text-slate-200">Maximizer</div>
          <div class="text-[9px] text-slate-400 font-mono">IRC IV True Peak</div>
        </div>
      </div>
      <i class="fa-solid fa-gauge-high text-slate-500 text-xs"></i>
    </div>

    <!-- Empty Add Module Slot -->
    <div class="w-24 h-12 rounded-lg border border-dashed border-slate-800 flex items-center justify-center text-slate-600 hover:text-slate-400 hover:border-slate-600 cursor-pointer transition-all">
      <i class="fa-solid fa-plus text-sm"></i>
    </div>
  </div>

  <!-- MAIN WORKSPACE CONTAINER -->
  <main class="flex-1 flex overflow-hidden">
    
    <!-- LEFT MODULE WORKSPACE (DYNAMIC VISUALIZER & CONTROLS) -->
    <div class="flex-1 flex flex-col bg-slate-950 relative overflow-y-auto">
      
      <!-- SUB HEADER TOOLBAR -->
      <div class="h-9 bg-slate-900/60 border-b border-slate-800/80 px-4 flex items-center justify-between text-xs font-mono">
        <div class="flex items-center gap-3">
          <span class="text-slate-400 flex items-center gap-1.5"><i class="fa-solid fa-headphones text-cyan-400"></i> Delta Listen</span>
          <select id="modeSelect" class="bg-slate-950 border border-slate-800 rounded px-2 py-0.5 text-[11px] text-slate-300 outline-none">
            <option value="irc4">IRC IV - Classic</option>
            <option value="irc3">IRC III - Balanced</option>
            <option value="irc2">IRC II - Crisp</option>
          </select>
        </div>

        <div class="flex items-center gap-2 text-[11px]">
          <span class="text-slate-400">Learn Input Gain:</span>
          <button id="learnGainBtn" class="bg-cyan-950 text-cyan-300 border border-cyan-800/80 px-2 py-0.5 rounded font-bold hover:bg-cyan-900 transition-colors">
            -11.0 LUFS
          </button>
        </div>
      </div>

      <!-- VIEW 1: DYNAMIC EQ VISUALIZER & BAND SETTINGS -->
      <div id="viewDynamicEq" class="flex-1 flex flex-col p-4 gap-4">
        <!-- EQ Canvas Display -->
        <div class="relative w-full h-64 bg-slate-950 border border-slate-800/90 rounded-xl overflow-hidden shadow-inner cursor-crosshair">
          <canvas id="eqCanvas" class="w-full h-full block"></canvas>
        </div>

        <!-- EQ Band Control Panel (Ozone Band Settings Window) -->
        <div class="oz-panel p-3 rounded-xl grid grid-cols-4 gap-3">
          <!-- Band 1 (Low) -->
          <div class="bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg flex flex-col justify-between">
            <div class="flex items-center justify-between text-xs font-bold text-cyan-400">
              <span>BAND 1 (LOW)</span>
              <span class="text-[10px] font-mono text-slate-400">100 Hz</span>
            </div>
            <div class="flex items-center justify-around my-2">
              <div class="knob-container" data-param="eqLowGain" data-min="-12" data-max="12" data-value="1.5" data-unit="dB">
                <div class="knob-dial w-9 h-9 rounded-full bg-slate-800 border-2 border-cyan-500 relative flex items-center justify-center shadow-md">
                  <div class="knob-pointer w-0.5 h-3 bg-cyan-400 rounded absolute top-1"></div>
                </div>
              </div>
              <div class="text-center font-mono">
                <span class="knob-val text-xs text-slate-200 font-bold">+1.5 dB</span>
              </div>
            </div>
            <div class="text-[9px] text-slate-500 font-mono text-center">Shelf Filter</div>
          </div>

          <!-- Band 2 (Mid Low) -->
          <div class="bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg flex flex-col justify-between">
            <div class="flex items-center justify-between text-xs font-bold text-amber-400">
              <span>BAND 2 (MID LOW)</span>
              <span class="text-[10px] font-mono text-slate-400">822 Hz</span>
            </div>
            <div class="flex items-center justify-around my-2">
              <div class="knob-container" data-param="eqMid1Gain" data-min="-12" data-max="12" data-value="-1.0" data-unit="dB">
                <div class="knob-dial w-9 h-9 rounded-full bg-slate-800 border-2 border-amber-500 relative flex items-center justify-center shadow-md">
                  <div class="knob-pointer w-0.5 h-3 bg-amber-400 rounded absolute top-1"></div>
                </div>
              </div>
              <div class="text-center font-mono">
                <span class="knob-val text-xs text-slate-200 font-bold">-1.0 dB</span>
              </div>
            </div>
            <div class="text-[9px] text-slate-500 font-mono text-center">Dynamic Bell (Q: 0.7)</div>
          </div>

          <!-- Band 3 (Mid High) -->
          <div class="bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg flex flex-col justify-between">
            <div class="flex items-center justify-between text-xs font-bold text-purple-400">
              <span>BAND 3 (MID HIGH)</span>
              <span class="text-[10px] font-mono text-slate-400">3.2 kHz</span>
            </div>
            <div class="flex items-center justify-around my-2">
              <div class="knob-container" data-param="eqMid2Gain" data-min="-12" data-max="12" data-value="2.0" data-unit="dB">
                <div class="knob-dial w-9 h-9 rounded-full bg-slate-800 border-2 border-purple-500 relative flex items-center justify-center shadow-md">
                  <div class="knob-pointer w-0.5 h-3 bg-purple-400 rounded absolute top-1"></div>
                </div>
              </div>
              <div class="text-center font-mono">
                <span class="knob-val text-xs text-slate-200 font-bold">+2.0 dB</span>
              </div>
            </div>
            <div class="text-[9px] text-slate-500 font-mono text-center">Dynamic Bell (Q: 1.2)</div>
          </div>

          <!-- Band 4 (High) -->
          <div class="bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg flex flex-col justify-between">
            <div class="flex items-center justify-between text-xs font-bold text-emerald-400">
              <span>BAND 4 (HIGH)</span>
              <span class="text-[10px] font-mono text-slate-400">10 kHz</span>
            </div>
            <div class="flex items-center justify-around my-2">
              <div class="knob-container" data-param="eqHighGain" data-min="-12" data-max="12" data-value="1.8" data-unit="dB">
                <div class="knob-dial w-9 h-9 rounded-full bg-slate-800 border-2 border-emerald-500 relative flex items-center justify-center shadow-md">
                  <div class="knob-pointer w-0.5 h-3 bg-emerald-400 rounded absolute top-1"></div>
                </div>
              </div>
              <div class="text-center font-mono">
                <span class="knob-val text-xs text-slate-200 font-bold">+1.8 dB</span>
              </div>
            </div>
            <div class="text-[9px] text-slate-500 font-mono text-center">High Shelf</div>
          </div>
        </div>
      </div>

      <!-- VIEW 2: STEREO IMAGER VISUALIZER & CONTROLS (HIDDEN BY DEFAULT) -->
      <div id="viewImager" class="flex-1 flex flex-col p-4 gap-4 hidden">
        <div class="grid grid-cols-12 gap-4 flex-1">
          <!-- Polar Vectorscope Canvas (8 Cols) -->
          <div class="col-span-8 bg-slate-950 border border-slate-800/90 rounded-xl p-3 flex flex-col relative shadow-inner">
            <span class="text-xs font-bold text-cyan-400 uppercase font-mono mb-2">
              <i class="fa-solid fa-circle-nodes"></i> Polar Vectorscope & Correlation Meter
            </span>
            <div class="flex-1 relative w-full h-56 flex items-center justify-center">
              <canvas id="imagerCanvas" class="w-full h-full block"></canvas>
            </div>
          </div>

          <!-- Band Width Controls (4 Cols) -->
          <div class="col-span-4 oz-panel p-3 rounded-xl flex flex-col justify-between">
            <span class="text-xs font-bold text-slate-300 uppercase font-mono">4-Band Stereo Width</span>
            
            <div class="space-y-3 my-auto">
              <div>
                <div class="flex justify-between text-[11px] font-mono mb-1"><span class="text-cyan-400 font-bold">Band 1 (0-100Hz)</span><span id="w1Val">0%</span></div>
                <input id="w1Slider" type="range" min="-100" max="100" value="0" class="w-full accent-cyan-500 h-1 cursor-pointer">
              </div>
              <div>
                <div class="flex justify-between text-[11px] font-mono mb-1"><span class="text-amber-400 font-bold">Band 2 (100-1kHz)</span><span id="w2Val">+15%</span></div>
                <input id="w2Slider" type="range" min="-100" max="100" value="15" class="w-full accent-amber-500 h-1 cursor-pointer">
              </div>
              <div>
                <div class="flex justify-between text-[11px] font-mono mb-1"><span class="text-purple-400 font-bold">Band 3 (1k-6kHz)</span><span id="w3Val">+35%</span></div>
                <input id="w3Slider" type="range" min="-100" max="100" value="35" class="w-full accent-purple-500 h-1 cursor-pointer">
              </div>
              <div>
                <div class="flex justify-between text-[11px] font-mono mb-1"><span class="text-emerald-400 font-bold">Band 4 (6k-20kHz)</span><span id="w4Val">+50%</span></div>
                <input id="w4Slider" type="range" min="-100" max="100" value="50" class="w-full accent-emerald-500 h-1 cursor-pointer">
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- VIEW 3: MAXIMIZER / LIMITER VISUALIZER & CONTROLS (HIDDEN BY DEFAULT) -->
      <div id="viewMaximizer" class="flex-1 flex flex-col p-4 gap-4 hidden">
        <div class="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
          
          <!-- Left Gain & Output Level Panel -->
          <div class="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
            <span class="text-xs font-bold text-slate-400 uppercase font-mono mb-3">Maximizer Gain Boost</span>
            
            <div class="knob-container my-2" data-param="maxGain" data-min="0" data-max="12" data-value="5.4" data-unit="dB">
              <div class="knob-dial w-24 h-24 rounded-full bg-slate-900 border-4 border-cyan-500 relative flex items-center justify-center shadow-2xl">
                <div class="knob-pointer w-1 h-8 bg-cyan-400 rounded absolute top-2"></div>
                <span class="knob-val text-sm font-bold font-mono text-cyan-300 z-10">+5.4 dB</span>
              </div>
            </div>

            <div class="w-full mt-4 bg-slate-950 p-2 rounded-lg border border-slate-800 text-xs font-mono flex justify-between items-center">
              <span class="text-slate-400">Ceiling Level:</span>
              <span class="text-cyan-400 font-bold">-0.10 dB</span>
            </div>
          </div>

          <!-- Center Upward Comp & Soft Clip Controls -->
          <div class="col-span-8 grid grid-cols-3 gap-4">
            <div class="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center">
              <span class="text-[10px] font-bold text-slate-400 font-mono mb-2">UPWARD COMPRESS</span>
              <div class="knob-container" data-param="maxUpward" data-min="0" data-max="10" data-value="2.0" data-unit="dB">
                <div class="knob-dial w-12 h-12 rounded-full bg-slate-800 border-2 border-cyan-500 relative flex items-center justify-center">
                  <div class="knob-pointer w-0.5 h-4 bg-cyan-400 rounded absolute top-1"></div>
                </div>
              </div>
              <span class="knob-val text-xs font-mono text-slate-200 mt-2">+2.0 dB</span>
            </div>

            <div class="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center">
              <span class="text-[10px] font-bold text-slate-400 font-mono mb-2">SOFT CLIPPER</span>
              <div class="knob-container" data-param="maxSoftClip" data-min="0" data-max="100" data-value="15" data-unit="%">
                <div class="knob-dial w-12 h-12 rounded-full bg-slate-800 border-2 border-amber-500 relative flex items-center justify-center">
                  <div class="knob-pointer w-0.5 h-4 bg-amber-400 rounded absolute top-1"></div>
                </div>
              </div>
              <span class="knob-val text-xs font-mono text-slate-200 mt-2">15%</span>
            </div>

            <div class="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center">
              <span class="text-[10px] font-bold text-slate-400 font-mono mb-2">TRANSIENT EMPHASIS</span>
              <div class="knob-container" data-param="maxTransient" data-min="0" data-max="100" data-value="25" data-unit="%">
                <div class="knob-dial w-12 h-12 rounded-full bg-slate-800 border-2 border-emerald-500 relative flex items-center justify-center">
                  <div class="knob-pointer w-0.5 h-4 bg-emerald-400 rounded absolute top-1"></div>
                </div>
              </div>
              <span class="knob-val text-xs font-mono text-slate-200 mt-2">25%</span>
            </div>
          </div>

        </div>
      </div>

    </div>

    <!-- RIGHT SIDEBAR: MASTER INPUT / OUTPUT STEREO METER TOWER -->
    <div class="w-64 bg-slate-900 border-l border-slate-800/80 p-3 flex flex-col justify-between shadow-2xl font-mono text-xs z-20">
      <div class="flex items-center justify-between border-b border-slate-800 pb-2 mb-2">
        <span class="text-xs font-bold text-white tracking-wider uppercase flex items-center gap-1.5">
          <i class="fa-solid fa-sliders text-cyan-400"></i> I/O METERS
        </span>
        <span class="text-[9px] bg-cyan-950 border border-cyan-800 text-cyan-300 px-1 py-0.5 rounded">TRUE PEAK</span>
      </div>

      <!-- Peak / RMS Numeric Display -->
      <div class="grid grid-cols-2 gap-2 text-[10px] text-center mb-2">
        <div class="bg-slate-950 border border-slate-800/80 p-1.5 rounded">
          <div class="text-slate-500 font-bold">IN PEAK</div>
          <div id="inPeakText" class="text-cyan-400 font-bold font-mono">-14.2 dB</div>
        </div>
        <div class="bg-slate-950 border border-slate-800/80 p-1.5 rounded">
          <div class="text-slate-500 font-bold">OUT PEAK</div>
          <div id="outPeakText" class="text-emerald-400 font-bold font-mono">-0.1 dB</div>
        </div>
      </div>

      <!-- Level Meters Graphic Container -->
      <div class="flex-1 bg-slate-950 border border-slate-800/90 rounded-xl p-2 flex justify-around relative overflow-hidden my-1">
        <!-- Input Meters (L/R) -->
        <div class="flex flex-col items-center h-full">
          <span class="text-[9px] text-slate-500 mb-1">IN</span>
          <div class="flex-1 w-5 bg-slate-900 border border-slate-800 rounded relative overflow-hidden flex items-end">
            <canvas id="inMeterCanvas" class="w-full h-full block"></canvas>
          </div>
        </div>

        <!-- Output Meters (L/R) -->
        <div class="flex flex-col items-center h-full">
          <span class="text-[9px] text-slate-500 mb-1">OUT</span>
          <div class="flex-1 w-6 bg-slate-900 border border-slate-800 rounded relative overflow-hidden flex items-end">
            <canvas id="outMeterCanvas" class="w-full h-full block"></canvas>
          </div>
        </div>
      </div>

      <!-- Global Utility Buttons (Ozone Right Side Action Bar) -->
      <div class="grid grid-cols-2 gap-1.5 mt-2">
        <button id="masterBypassBtn" class="bg-slate-800 hover:bg-slate-700 text-slate-200 py-1.5 rounded text-[10px] font-bold border border-slate-700 transition-colors">
          Bypass
        </button>
        <button class="bg-slate-800 hover:bg-slate-700 text-slate-200 py-1.5 rounded text-[10px] font-bold border border-slate-700 transition-colors">
          Gain Match
        </button>
        <button class="bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 rounded text-[10px] font-sans border border-slate-700">
          Codec
        </button>
        <button class="bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 rounded text-[10px] font-sans border border-slate-700">
          Dither
        </button>
      </div>
    </div>

  </main>

  <!-- JAVASCRIPT: WEB AUDIO DSP GRAPH & OZONE STUDIO LOGIC -->
  <script>
    // State Store for Ozone Parameters
    const ozState = {
      isBypassed: false,
      activeModule: 'eq', // 'eq', 'imager', 'maximizer'
      
      // Module Actives
      eqActive: true,
      imagerActive: true,
      maximizerActive: true,

      // EQ Parameters
      eqLowGain: 1.5,
      eqMid1Gain: -1.0,
      eqMid2Gain: 2.0,
      eqHighGain: 1.8,

      // Imager Band Widths
      w1: 0,
      w2: 15,
      w3: 35,
      w4: 50,

      // Maximizer Parameters
      maxGain: 5.4,
      maxUpward: 2.0,
      maxSoftClip: 15,
      maxTransient: 25
    };

    // Web Audio Node Graph References
    let audioCtx = null;
    let sourceNode = null;
    let inputAnalyser = null;
    let eqLowFilter = null;
    let eqMid1Filter = null;
    let eqMid2Filter = null;
    let eqHighFilter = null;
    let maximizerBoostGain = null;
    let maximizerCompressor = null;
    let outputAnalyser = null;
    let isPlaying = false;

    // Web Audio Initialization
    function initOzoneAudioEngine() {
      if (audioCtx) return;

      const AudioCtxClass = window.AudioContext || window.webkitAudioContext;
      audioCtx = new AudioCtxClass();

      // Input Analyser
      inputAnalyser = audioCtx.createAnalyser();
      inputAnalyser.fftSize = 2048;

      // Dynamic EQ Filters
      eqLowFilter = audioCtx.createBiquadFilter();
      eqLowFilter.type = 'lowshelf';
      eqLowFilter.frequency.value = 100;

      eqMid1Filter = audioCtx.createBiquadFilter();
      eqMid1Filter.type = 'peaking';
      eqMid1Filter.frequency.value = 822;
      eqMid1Filter.Q.value = 0.7;

      eqMid2Filter = audioCtx.createBiquadFilter();
      eqMid2Filter.type = 'peaking';
      eqMid2Filter.frequency.value = 3200;
      eqMid2Filter.Q.value = 1.2;

      eqHighFilter = audioCtx.createBiquadFilter();
      eqHighFilter.type = 'highshelf';
      eqHighFilter.frequency.value = 10000;

      // Maximizer Boost Gain
      maximizerBoostGain = audioCtx.createGain();

      // Brickwall Limiter Compressor
      maximizerCompressor = audioCtx.createDynamicsCompressor();
      maximizerCompressor.threshold.value = -0.1;
      maximizerCompressor.knee.value = 0.0;
      maximizerCompressor.ratio.value = 20.0;
      maximizerCompressor.attack.value = 0.001;
      maximizerCompressor.release.value = 0.05;

      // Output Analyser
      outputAnalyser = audioCtx.createAnalyser();
      outputAnalyser.fftSize = 2048;

      // Connect Signal Chain Graph:
      // InputAnalyser -> EQ Low -> EQ Mid1 -> EQ Mid2 -> EQ High -> BoostGain -> Compressor -> OutputAnalyser -> Destination
      inputAnalyser.connect(eqLowFilter);
      eqLowFilter.connect(eqMid1Filter);
      eqMid1Filter.connect(eqMid2Filter);
      eqMid2Filter.connect(eqHighFilter);
      eqHighFilter.connect(maximizerBoostGain);
      maximizerBoostGain.connect(maximizerCompressor);
      maximizerCompressor.connect(outputAnalyser);
      outputAnalyser.connect(audioCtx.destination);

      updateAudioGraphValues();
    }

    function updateAudioGraphValues() {
      if (!audioCtx) return;
      const now = audioCtx.currentTime;

      eqLowFilter.gain.setTargetAtTime(ozState.eqActive ? ozState.eqLowGain : 0, now, 0.01);
      eqMid1Filter.gain.setTargetAtTime(ozState.eqActive ? ozState.eqMid1Gain : 0, now, 0.01);
      eqMid2Filter.gain.setTargetAtTime(ozState.eqActive ? ozState.eqMid2Gain : 0, now, 0.01);
      eqHighFilter.gain.setTargetAtTime(ozState.eqActive ? ozState.eqHighGain : 0, now, 0.01);

      const boostLinear = ozState.maximizerActive ? Math.pow(10, ozState.maxGain / 20) : 1.0;
      maximizerBoostGain.gain.setTargetAtTime(boostLinear, now, 0.01);
    }

    // Synthesize EDM / Pop Reference Audio Loop
    function startAudioDemo() {
      initOzoneAudioEngine();
      if (audioCtx.state === 'suspended') audioCtx.resume();
      stopAudioDemo();

      const sampleRate = audioCtx.sampleRate;
      const bufferSize = sampleRate * 4; // 4 second loop
      const buffer = audioCtx.createBuffer(2, bufferSize, sampleRate);
      const left = buffer.getChannelData(0);
      const right = buffer.getChannelData(1);

      for (let i = 0; i < bufferSize; i++) {
        const t = i / sampleRate;
        // Kick Drum
        const kickEnv = Math.max(0, 1 - (t % 0.5) * 8);
        const kick = Math.sin(2 * Math.PI * (55 * Math.exp(-(t % 0.5) * 18))) * kickEnv;

        // Snare / Claps
        const snareEnv = Math.max(0, 1 - ((t + 0.25) % 0.5) * 6);
        const snare = (Math.random() * 2 - 1) * snareEnv * 0.3;

        // EDM Chords (C min, Ab, Eb, Bb)
        const synth = (Math.sin(2 * Math.PI * 261.63 * t) + Math.sin(2 * Math.PI * 311.13 * t) + Math.sin(2 * Math.PI * 392.0 * t)) * 0.12;

        left[i] = kick * 0.6 + snare + synth;
        right[i] = kick * 0.6 + snare * 0.9 + synth * 0.95;
      }

      sourceNode = audioCtx.createBufferSource();
      sourceNode.buffer = buffer;
      sourceNode.loop = true;
      sourceNode.connect(inputAnalyser);
      sourceNode.start();

      isPlaying = true;
      document.getElementById('playAudioBtn').innerHTML = '<i class="fa-solid fa-pause text-[10px]"></i> <span>Pause Reference</span>';
    }

    function stopAudioDemo() {
      if (sourceNode) {
        try { sourceNode.stop(); } catch(e){}
        sourceNode.disconnect();
        sourceNode = null;
      }
      isPlaying = false;
      document.getElementById('playAudioBtn').innerHTML = '<i class="fa-solid fa-play text-[10px]"></i> <span>Play Reference Loop</span>';
    }

    // Canvas Render Loops
    function startCanvasRenderers() {
      const eqCanvas = document.getElementById('eqCanvas');
      const imagerCanvas = document.getElementById('imagerCanvas');
      const inMeterCanvas = document.getElementById('inMeterCanvas');
      const outMeterCanvas = document.getElementById('outMeterCanvas');

      const eqCtx = eqCanvas.getContext('2d');
      const imagerCtx = imagerCanvas.getContext('2d');
      const inCtx = inMeterCanvas.getContext('2d');
      const outCtx = outMeterCanvas.getContext('2d');

      function resizeAll() {
        eqCanvas.width = eqCanvas.clientWidth;
        eqCanvas.height = eqCanvas.clientHeight;
        imagerCanvas.width = imagerCanvas.clientWidth;
        imagerCanvas.height = imagerCanvas.clientHeight;
        inMeterCanvas.width = inMeterCanvas.clientWidth;
        inMeterCanvas.height = inMeterCanvas.clientHeight;
        outMeterCanvas.width = outMeterCanvas.clientWidth;
        outMeterCanvas.height = outMeterCanvas.clientHeight;
      }
      resizeAll();
      window.addEventListener('resize', resizeAll);

      const fftData = new Uint8Array(1024);

      function renderFrame() {
        requestAnimationFrame(renderFrame);

        // -------------------------------------------------------------
        // 1. RENDER DYNAMIC EQ & SPECTRUM
        // -------------------------------------------------------------
        const w = eqCanvas.width;
        const h = eqCanvas.height;
        eqCtx.clearRect(0, 0, w, h);

        // Draw Ozone-style Frequency Grid Lines
        eqCtx.strokeStyle = 'rgba(51, 65, 85, 0.3)';
        eqCtx.lineWidth = 1;
        eqCtx.font = '9px JetBrains Mono';
        eqCtx.fillStyle = '#475569';

        const freqs = [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000];
        freqs.forEach(f => {
          const x = (Math.log10(f / 20) / Math.log10(20000 / 20)) * w;
          eqCtx.beginPath();
          eqCtx.moveTo(x, 0);
          eqCtx.lineTo(x, h);
          eqCtx.stroke();
          if (f >= 1000) eqCtx.fillText(`${f/1000}k`, x + 3, h - 6);
          else eqCtx.fillText(`${f}`, x + 3, h - 6);
        });

        // Draw FFT Spectrum Fill
        if (outputAnalyser && isPlaying) {
          outputAnalyser.getByteFrequencyData(fftData);
          eqCtx.fillStyle = 'rgba(56, 189, 248, 0.15)';
          const barWidth = w / 128;
          for (let i = 0; i < 128; i++) {
            const val = fftData[i * 4] / 255;
            const barHeight = val * h;
            eqCtx.fillRect(i * barWidth, h - barHeight, barWidth - 1, barHeight);
          }
        }

        // Draw Composite EQ Response Curve
        eqCtx.strokeStyle = '#38bdf8';
        eqCtx.lineWidth = 2.5;
        eqCtx.beginPath();

        for (let x = 0; x < w; x++) {
          const freq = 20 * Math.pow(20000 / 20, x / w);
          let gainDb = 0;

          if (ozState.eqActive) {
            // Low Shelf
            gainDb += ozState.eqLowGain / (1 + Math.pow(freq / 100, 2));
            // Mid 1 Bell
            gainDb += ozState.eqMid1Gain * Math.exp(-Math.pow(Math.log(freq / 822), 2) * 2);
            // Mid 2 Bell
            gainDb += ozState.eqMid2Gain * Math.exp(-Math.pow(Math.log(freq / 3200), 2) * 2);
            // High Shelf
            gainDb += ozState.eqHighGain / (1 + Math.pow(10000 / freq, 2));
          }

          const y = h / 2 - (gainDb / 18) * (h / 2);
          if (x === 0) eqCtx.moveTo(x, y);
          else eqCtx.lineTo(x, y);
        }
        eqCtx.stroke();

        // -------------------------------------------------------------
        // 2. RENDER IMAGER POLAR VECTORSCOPE
        // -------------------------------------------------------------
        const iw = imagerCanvas.width;
        const ih = imagerCanvas.height;
        imagerCtx.clearRect(0, 0, iw, ih);

        // Scope Polar Circle Guides
        imagerCtx.strokeStyle = 'rgba(51, 65, 85, 0.4)';
        imagerCtx.lineWidth = 1;
        imagerCtx.beginPath();
        imagerCtx.arc(iw / 2, ih / 2, ih / 3, 0, Math.PI * 2);
        imagerCtx.stroke();

        // Scope Dynamic Stereo Dots Animation
        if (isPlaying) {
          imagerCtx.fillStyle = '#38bdf8';
          for (let i = 0; i < 40; i++) {
            const angle = (Math.random() - 0.5) * (Math.PI / 2) + (-Math.PI / 2);
            const radius = (Math.random() * (ih / 3.2));
            const x = iw / 2 + Math.cos(angle) * radius * (1 + ozState.w3 / 100);
            const y = ih / 2 + Math.sin(angle) * radius;
            imagerCtx.fillRect(x, y, 2, 2);
          }
        }

        // -------------------------------------------------------------
        // 3. RENDER I/O STEREO METER TOWERS
        // -------------------------------------------------------------
        renderMeterBar(inCtx, inMeterCanvas.width, inMeterCanvas.height, isPlaying ? 0.55 : 0.05, 'inPeakText');
        renderMeterBar(outCtx, outMeterCanvas.width, outMeterCanvas.height, isPlaying ? 0.85 : 0.05, 'outPeakText');
      }

      function renderMeterBar(ctx, w, h, level, textId) {
        ctx.clearRect(0, 0, w, h);
        const barH = level * h;

        const grad = ctx.createLinearGradient(0, h, 0, 0);
        grad.addColorStop(0, '#38bdf8');
        grad.addColorStop(0.7, '#f59e0b');
        grad.addColorStop(1, '#ef4444');

        ctx.fillStyle = grad;
        ctx.fillRect(2, h - barH, w - 4, barH);

        const dbVal = (20 * Math.log10(level)).toFixed(1);
        document.getElementById(textId).innerText = isPlaying ? `${dbVal} dB` : '-inf dB';
      }

      renderFrame();
    }

    // Rotary Knob Drag Interactions
    function setupKnobs() {
      document.querySelectorAll('.knob-container').forEach(knob => {
        let isDragging = false;
        let startY = 0;
        let startVal = parseFloat(knob.dataset.value);

        const param = knob.dataset.param;
        const min = parseFloat(knob.dataset.min);
        const max = parseFloat(knob.dataset.max);
        const unit = knob.dataset.unit || '';

        const dial = knob.querySelector('.knob-dial');
        const valText = knob.parentElement.querySelector('.knob-val');

        function updateUI(val) {
          const pct = (val - min) / (max - min);
          const angle = -135 + pct * 270;
          dial.style.transform = `rotate(${angle}deg)`;

          if (valText) {
            valText.innerText = `${val > 0 && unit==='dB' ? '+' : ''}${val.toFixed(1)} ${unit}`;
          }

          ozState[param] = val;
          updateAudioGraphValues();
        }

        updateUI(startVal);

        knob.addEventListener('pointerdown', e => {
          isDragging = true;
          startY = e.clientY;
          startVal = ozState[param];
          knob.setPointerCapture(e.pointerId);
        });

        knob.addEventListener('pointermove', e => {
          if (!isDragging) return;
          const deltaY = startY - e.clientY;
          const range = max - min;
          let newVal = startVal + (deltaY / 100) * range;
          newVal = Math.min(max, Math.max(min, newVal));
          updateUI(newVal);
        });

        knob.addEventListener('pointerup', e => {
          isDragging = false;
          knob.releasePointerCapture(e.pointerId);
        });
      });
    }

    // Module Switcher Navigation Logic
    function setupModuleNavigation() {
      const cardEq = document.getElementById('cardDynamicEq');
      const cardImager = document.getElementById('cardImager');
      const cardMaximizer = document.getElementById('cardMaximizer');

      const viewEq = document.getElementById('viewDynamicEq');
      const viewImager = document.getElementById('viewImager');
      const viewMaximizer = document.getElementById('viewMaximizer');

      function switchModule(activeCard, activeView, name) {
        [cardEq, cardImager, cardMaximizer].forEach(c => c.classList.remove('oz-card-active'));
        [viewEq, viewImager, viewMaximizer].forEach(v => v.classList.add('hidden'));

        activeCard.classList.add('oz-card-active');
        activeView.classList.remove('hidden');
        ozState.activeModule = name;
      }

      cardEq.addEventListener('click', () => switchModule(cardEq, viewEq, 'eq'));
      cardImager.addEventListener('click', () => switchModule(cardImager, viewImager, 'imager'));
      cardMaximizer.addEventListener('click', () => switchModule(cardMaximizer, viewMaximizer, 'maximizer'));
    }

    // DOM Load Init
    window.addEventListener('load', () => {
      document.getElementById('playAudioBtn').addEventListener('click', () => {
        if (isPlaying) stopAudioDemo();
        else startAudioDemo();
      });

      document.getElementById('stopAudioBtn').addEventListener('click', stopAudioDemo);

      setupKnobs();
      setupModuleNavigation();
      startCanvasRenderers();
    });
  </script>
</body>
</html>