Files
SonicForgeStudio/md/48_TRACK_STRIP_COMPONENT.md
T

19 KiB

TECHNICAL SPECIFICATION & VISUAL DISPLAY GUIDE: TRACK CHANNEL STRIP


1. Visual Geometry & Layout Description

The Track Channel Strip is built as a vertical, compact metal unit (Fixed Compact Unit) modeled after professional DAWs like REAPER with fixed dimensions of $145\text{px} \times 370\text{px}$.

The user interface consists of 6 functional zones arranged vertically from top to bottom:

+-------------------------------------------------------------+
| 1. TOP TRACK COLOR ACCENT BAR (Height: 6px)                 |
+-------------------------------------------------------------+
| 2. PAN ROTARY DIAL AREA (Height: 46px)                      |
|    - Rotary Knob Dial (Left / Right Panning)                |
|    - Text Readout: "center", "L50", "R100"                  |
+-------------------------------------------------------------+
| 3. CENTER DUAL-COLUMN AREA (Height: 210px)                  |
|    +--------------------------+ +-------------------------+ |
|    | Peak dB Value (-inf/dB)  | | [M] Mute Switch         | |
|    | +----------------------+ | | [S] Solo Switch         | |
|    | | Rail Track (160px)   | | | [Routing Matrix]        | |
|    | | - Metal Fader Cap    | | | [FX Chain]              | |
|    | +----------------------+ | | [⏻] FX Power Toggle     | |
|    | +----------------------+ | | [Automation Envelopes]  | |
|    | | LED Peak VU Canvas   | | | [Ø] Phase Invert (180°) | |
|    | +----------------------+ | +-------------------------+ |
|    +--------------------------+                             |
+-------------------------------------------------------------+
| 4. RECORD ARM BUTTON ROW (Height: 28px)                     |
|    - Input Monitor Icon + Glowing Red ARM Button (🔴)       |
+-------------------------------------------------------------+
| 5. TRACK NAME IDENTIFIER (Height: 26px)                     |
|    - Dark Box with Track Name Display ("PIANO")             |
+-------------------------------------------------------------+
| 6. TRACK COLOR FOOTER BAR (Height: 22px)                    |
|    - Accent Background with Track Index Number ("1")        |
+-------------------------------------------------------------+


2. Audio Processing Graph & Wiring (Web Audio API)

Audio signals passing through the Track Channel Strip follow a serial processing chain (Post-FX Signal Path):

\text{Input Source} \xrightarrow{\quad} \text{Phase Invert ($\text{\O}$)} \xrightarrow{\quad} \text{Track FX Chain} \xrightarrow{\quad} \text{Mute / Solo Switch} \xrightarrow{\quad} \text{Volume Fader (Gain)} \xrightarrow{\quad} \text{Stereo Panner} \xrightarrow{\quad} \text{Analyser (VU Meter)} \xrightarrow{\quad} \text{Master Bus}
  • Phase Invert (\text{\O}): Inverts the signal phase by 180^\circ (G = -1) to resolve phase cancellation issues between multiple microphone inputs.

  • Track FX Chain: Dedicated track insertion effects (Dynamic EQ, Reverb, Delay).

  • Mute / Solo Logic:

  • Mute [M]: Sets Linear Gain = 0.0.

  • Solo [S]: Silences all other non-soloed tracks across the session.

  • Volume Fader Scaling: Converts decibels (\text{dB}) from the fader position into a linear gain factor G:

G = 10^{\frac{\text{dB}}{20}}

(Control range: -60\text{ dB} to +12\text{ dB}).

  • Stereo Panner: Adjusts spatial positioning across the stereo field from Left (-1.0) to Right (+1.0).
  • Peak VU Metering: An AnalyserNode extracts real-time amplitude data to render a dynamic LED meter (Green \to Yellow \to Red).

3. Practical Track Channel Applications

  • Mixer Dock Rendering: Iterates and renders a strip for each item in session.tracks along the docked Mixer Console at the bottom of the screen.
  • Color Accent Synchronization: Accent bars at the top and bottom (topColorBar and bottomFooterBar) match the assigned track identification color on the main Timeline.
  • Live Recording (ARM 🔴): Toggling the ARM button prepares the channel to receive real-time input from a live microphone or hardware MIDI keyboard.

4. html

<!DOCTYPE html>
<html lang="vi" 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>DAW Track Channel Strip Component</title>
  <!-- Tailwind CSS -->
  <script src="https://cdn.tailwindcss.com"></script>
  <!-- Font Awesome Icons -->
  <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@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; }

    /* Custom Scrollbar */
    ::-webkit-scrollbar { width: 6px; height: 6px; }
    ::-webkit-scrollbar-track { background: #090d16; }
    ::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 3px; }

    /* REAPER Metallic Dark DAW Styling */
    .strip-bg {
      background: linear-gradient(180deg, #2c2c2c 0%, #1e1e1e 100%);
      border: 1px solid #111111;
      box-shadow: inset 1px 1px 0 rgba(255,255,255,0.08), 0 10px 30px rgba(0,0,0,0.85);
    }

    .fader-track-bg {
      background: #101010;
      box-shadow: inset 1px 1px 3px rgba(0,0,0,0.9), 1px 1px 0 rgba(255,255,255,0.05);
    }

    /* DAW Metallic Buttons */
    .btn-daw {
      background: linear-gradient(180deg, #3d3d3d 0%, #242424 100%);
      border: 1px solid #141414;
      box-shadow: inset 0 1px 0 rgba(255,255,255,0.12), 0 2px 4px rgba(0,0,0,0.6);
      transition: all 0.1s ease;
    }
    .btn-daw:hover {
      background: linear-gradient(180deg, #4d4d4d 0%, #2f2f2f 100%);
    }
    .btn-daw:active {
      box-shadow: inset 0 2px 4px rgba(0,0,0,0.8);
      transform: translateY(0.5px);
    }

    /* Active State Button Styles */
    .btn-mute-active {
      background: linear-gradient(180deg, #ef4444 0%, #991b1b 100%) !important;
      border-color: #f87171 !important;
      color: #ffffff !important;
      box-shadow: 0 0 8px rgba(239, 68, 68, 0.6) !important;
    }

    .btn-solo-active {
      background: linear-gradient(180deg, #eab308 0%, #854d0e 100%) !important;
      border-color: #fde047 !important;
      color: #000000 !important;
      box-shadow: 0 0 8px rgba(234, 179, 8, 0.6) !important;
    }

    .btn-arm-active {
      background: radial-gradient(circle, #ef4444 0%, #7f1d1d 100%) !important;
      border-color: #fca5a5 !important;
      box-shadow: 0 0 12px rgba(239, 68, 68, 0.9), inset 0 1px 2px rgba(255,255,255,0.8) !important;
      animation: pulse-red 1.2s infinite alternate;
    }

    @keyframes pulse-red {
      0% { box-shadow: 0 0 6px rgba(239, 68, 68, 0.6); }
      100% { box-shadow: 0 0 16px rgba(239, 68, 68, 1); }
    }

    /* FIXED VERTICAL SLIDER STYLING */
    input[type=range].fader-slider {
      -webkit-appearance: slider-vertical;
      appearance: slider-vertical;
      writing-mode: bt-linear;
      width: 20px;
      height: 160px; /* Fixed Height prevents stretching */
      background: transparent;
      cursor: pointer;
      margin: 0;
      padding: 0;
    }
  </style>
</head>
<body class="h-screen max-h-screen bg-slate-950 flex flex-col items-center justify-center p-4 overflow-hidden select-none">

  <!-- REAPER STYLE NARROW TRACK STRIP CONSOLE COMPONENT -->
  <div id="trackStripConsole" class="w-[145px] h-[370px] strip-bg rounded-md flex flex-col justify-between text-slate-300 select-none shadow-2xl relative border border-slate-900 overflow-hidden shrink-0">
    
    <!-- 1. TOP TRACK COLOR ACCENT BAR (6px) -->
    <div id="topColorBar" class="h-1.5 w-full bg-emerald-500 transition-colors shrink-0"></div>

    <!-- 2. PAN ROTARY DIAL AREA (46px) -->
    <div id="panBoxBg" class="h-[46px] bg-emerald-950/60 border-b border-emerald-700/40 py-1 px-2 flex flex-col items-center justify-center transition-colors shrink-0">
      <div id="panKnobDial" class="w-6 h-6 rounded-full bg-slate-800 border-2 border-slate-400 relative flex items-center justify-center cursor-pointer shadow-md" title="Kéo chuột lên/xuống để chỉnh Pan (Left/Right)">
        <div id="panPointer" class="w-0.5 h-2 bg-emerald-300 rounded absolute top-0.5 transition-transform" style="transform: rotate(0deg);"></div>
      </div>
      <span id="panText" class="text-[8px] font-mono text-emerald-200 mt-0.5 font-semibold">center</span>
    </div>

    <!-- 3. CENTER AREA: PEAK DB + NARROW FADER + STEREO VU METER + RIGHT BUTTON STACK (210px) -->
    <div class="p-1 flex gap-1 justify-between items-stretch h-[210px] shrink-0">
      
      <!-- LEFT FADER & VU METER COLUMN -->
      <div class="flex-1 flex flex-col justify-between items-center bg-black/60 p-1 rounded border border-slate-800/80 h-full">
        <!-- DB Peak Readout -->
        <div class="w-full flex justify-center text-[8px] font-mono text-slate-400 h-4 items-center">
          <span id="peakDbVal">-inf</span>
        </div>

        <!-- Track Fader Rail & VU Meter Dual Column -->
        <div class="flex items-center justify-around w-full h-[175px] relative py-1">
          <!-- Track Fader Rail Line & Slider -->
          <div class="relative fader-track-bg w-3 h-[165px] rounded flex items-center justify-center">
            <div class="w-0.5 h-full bg-slate-700 absolute"></div>
            <!-- Vertical Volume Slider (-60dB to +12dB) -->
            <input id="volumeFader" type="range" min="-60" max="12" step="0.5" value="0" orient="vertical"
                   class="fader-slider z-10" title="Chỉnh Volume Track (Fader)">
          </div>

          <!-- Vertical LED Stereo Peak VU Meter Canvas -->
          <div class="w-2.5 h-[165px] bg-slate-950 rounded border border-slate-900 overflow-hidden relative" title="Peak VU Meter">
            <canvas id="trackVuCanvas" class="w-full h-full block"></canvas>
          </div>
        </div>
      </div>

      <!-- RIGHT SIDE BUTTON STACK (M, S, ROUTE, FX, POWER, AUTOMATION, PHASE INVERT) -->
      <div class="w-7 flex flex-col justify-between h-full text-[8px] font-bold shrink-0">
        <button id="muteBtn" class="btn-daw h-[24px] rounded text-amber-500 font-extrabold flex items-center justify-center" title="Mute Track (Tắt tiếng)">M</button>
        <button id="soloBtn" class="btn-daw h-[24px] rounded text-yellow-400 font-extrabold flex items-center justify-center" title="Solo Track (Phát đơn độc)">S</button>
        <button class="btn-daw h-[24px] rounded text-emerald-400 flex items-center justify-center" title="Routing Matrix (Định tuyến I/O)"><i class="fa-solid fa-bars-staggered text-[8px]"></i></button>
        <button id="fxBtn" class="btn-daw h-[24px] rounded text-slate-300 font-semibold flex items-center justify-center text-[7px]" title="Track FX Chain">FX</button>
        <button id="powerBtn" class="btn-daw h-[24px] rounded text-emerald-400 flex items-center justify-center text-[8px]" title="Toggle FX Power (Bật/Tắt FX)"><i class="fa-solid fa-power-off"></i></button>
        <button class="btn-daw h-[24px] rounded text-slate-400 flex items-center justify-center" title="Automation Envelopes"><i class="fa-solid fa-chart-line text-[8px]"></i></button>
        <button id="phaseBtn" class="btn-daw h-[24px] rounded text-slate-400 flex items-center justify-center text-[9px]" title="Phase Invert (Ø - Đảo pha)">Ø</button>
      </div>

    </div>

    <!-- 4. RECORD ARM BUTTON ROW (28px) -->
    <div class="h-[28px] flex items-center justify-between mx-1.5 px-1.5 bg-black/40 rounded border border-slate-800/60 shrink-0">
      <i class="fa-solid fa-volume-high text-[9px] text-slate-400" title="Input Monitoring"></i>
      <!-- ARM RECORD BUTTON -->
      <button id="armBtn" class="w-5 h-5 rounded-full bg-red-950 border-2 border-red-800 flex items-center justify-center text-red-500 transition-all shadow-inner" title="Arm for Recording (Sẵn sàng thu âm)">
        <i class="fa-solid fa-circle text-[8px]"></i>
      </button>
    </div>

    <!-- 5. TRACK NAME IDENTIFIER (26px) -->
    <div class="h-[26px] mx-1.5 flex items-center justify-center bg-slate-950/80 rounded border border-slate-800/80 shrink-0">
      <span id="trackNameText" class="text-[11px] font-bold text-emerald-400 tracking-wider font-sans uppercase">Piano</span>
    </div>

    <!-- 6. TRACK COLOR FOOTER BAR (22px) -->
    <div id="bottomFooterBar" class="h-[22px] w-full bg-emerald-500 text-slate-950 flex items-center justify-center font-extrabold text-xs font-mono tracking-widest transition-colors shrink-0">
      1
    </div>

  </div>

  <script>
    // Audio Context & State Management
    let audioCtx = null;
    let trackGainNode = null;
    let trackPanNode = null;
    let analyserNode = null;
    let synthOsc = null;
    let isPlaying = false;

    const trackState = {
      volumeDb: 0.0,
      pan: 0.0,
      isMuted: false,
      isSoloed: false,
      isArmed: false,
      isFxActive: true,
      isPhaseInverted: false
    };

    // DOM Elements
    const volumeFader = document.getElementById('volumeFader');
    const peakDbVal = document.getElementById('peakDbVal');
    const panKnobDial = document.getElementById('panKnobDial');
    const panPointer = document.getElementById('panPointer');
    const panText = document.getElementById('panText');
    const muteBtn = document.getElementById('muteBtn');
    const soloBtn = document.getElementById('soloBtn');
    const armBtn = document.getElementById('armBtn');
    const powerBtn = document.getElementById('powerBtn');
    const phaseBtn = document.getElementById('phaseBtn');
    const vuCanvas = document.getElementById('trackVuCanvas');
    const vuCtx = vuCanvas.getContext('2d');

    function initAudioEngine() {
      if (audioCtx) return;
      const AudioCtxClass = window.AudioContext || window.webkitAudioContext;
      audioCtx = new AudioCtxClass();

      trackGainNode = audioCtx.createGain();
      trackPanNode = audioCtx.createStereoPanner ? audioCtx.createStereoPanner() : null;
      analyserNode = audioCtx.createAnalyser();
      analyserNode.fftSize = 256;

      if (trackPanNode) {
        trackGainNode.connect(trackPanNode);
        trackPanNode.connect(analyserNode);
      } else {
        trackGainNode.connect(analyserNode);
      }
      analyserNode.connect(audioCtx.destination);

      updateAudioNodes();
    }

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

      let linearGain = 0;
      if (!trackState.isMuted) {
        const db = trackState.volumeDb;
        linearGain = db <= -60 ? 0 : Math.pow(10, db / 20);
      }

      if (trackState.isPhaseInverted) {
        linearGain = -linearGain;
      }

      trackGainNode.gain.setTargetAtTime(linearGain, now, 0.01);

      if (trackPanNode) {
        trackPanNode.pan.setTargetAtTime(trackState.pan, now, 0.01);
      }
    }

    // Play Synth Sound Preview when interacting with controls
    function playPreviewNote() {
      initAudioEngine();
      if (audioCtx.state === 'suspended') audioCtx.resume();

      if (synthOsc) {
        try { synthOsc.stop(); } catch(e){}
        synthOsc.disconnect();
      }

      synthOsc = audioCtx.createOscillator();
      const oscGain = audioCtx.createGain();

      synthOsc.type = 'triangle';
      synthOsc.frequency.setValueAtTime(440, audioCtx.currentTime); // Note A4

      oscGain.gain.setValueAtTime(0.3, audioCtx.currentTime);
      oscGain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1.2);

      synthOsc.connect(oscGain);
      oscGain.connect(trackGainNode);

      synthOsc.start();
      synthOsc.stop(audioCtx.currentTime + 1.2);
      isPlaying = true;
    }

    // Volume Slider Event
    volumeFader.addEventListener('input', (e) => {
      const db = parseFloat(e.target.value);
      trackState.volumeDb = db;
      updateAudioNodes();
      playPreviewNote();
    });

    // Rotary Pan Knob Dragging
    let isPanDragging = false;
    let panStartY = 0;
    let startPanVal = 0;

    panKnobDial.addEventListener('pointerdown', (e) => {
      isPanDragging = true;
      panStartY = e.clientY;
      startPanVal = trackState.pan;
      panKnobDial.setPointerCapture(e.pointerId);
    });

    panKnobDial.addEventListener('pointermove', (e) => {
      if (!isPanDragging) return;
      const deltaY = panStartY - e.clientY;
      let newPan = startPanVal + (deltaY / 80);
      newPan = Math.min(1.0, Math.max(-1.0, newPan));

      trackState.pan = newPan;
      const angle = newPan * 120;
      panPointer.style.transform = `rotate(${angle}deg)`;

      if (newPan === 0) panText.innerText = 'center';
      else if (newPan < 0) panText.innerText = `L${Math.abs(Math.round(newPan * 100))}`;
      else panText.innerText = `R${Math.round(newPan * 100)}`;

      updateAudioNodes();
      playPreviewNote();
    });

    panKnobDial.addEventListener('pointerup', (e) => {
      isPanDragging = false;
      try { panKnobDial.releasePointerCapture(e.pointerId); } catch(err){}
    });

    // Buttons Interactions
    muteBtn.addEventListener('click', () => {
      trackState.isMuted = !trackState.isMuted;
      muteBtn.classList.toggle('btn-mute-active', trackState.isMuted);
      updateAudioNodes();
    });

    soloBtn.addEventListener('click', () => {
      trackState.isSoloed = !trackState.isSoloed;
      soloBtn.classList.toggle('btn-solo-active', trackState.isSoloed);
    });

    armBtn.addEventListener('click', () => {
      trackState.isArmed = !trackState.isArmed;
      armBtn.classList.toggle('btn-arm-active', trackState.isArmed);
    });

    powerBtn.addEventListener('click', () => {
      trackState.isFxActive = !trackState.isFxActive;
      powerBtn.classList.toggle('text-slate-500', !trackState.isFxActive);
      powerBtn.classList.toggle('text-emerald-400', trackState.isFxActive);
    });

    phaseBtn.addEventListener('click', () => {
      trackState.isPhaseInverted = !trackState.isPhaseInverted;
      phaseBtn.classList.toggle('bg-amber-600', trackState.isPhaseInverted);
      phaseBtn.classList.toggle('text-white', trackState.isPhaseInverted);
      updateAudioNodes();
    });

    // Peak VU Meter Renderer
    function renderVuMeter() {
      requestAnimationFrame(renderVuMeter);

      vuCanvas.width = vuCanvas.clientWidth;
      vuCanvas.height = vuCanvas.clientHeight;

      const w = vuCanvas.width;
      const h = vuCanvas.height;

      vuCtx.clearRect(0, 0, w, h);

      let level = 0;
      if (isPlaying && !trackState.isMuted) {
        const db = trackState.volumeDb;
        const linearGain = db <= -60 ? 0 : Math.pow(10, db / 20);
        level = (0.25 + Math.random() * 0.6) * linearGain;
      }

      const barH = level * h;
      const grad = vuCtx.createLinearGradient(0, h, 0, 0);
      grad.addColorStop(0, '#10b981');
      grad.addColorStop(0.7, '#f59e0b');
      grad.addColorStop(0.95, '#ef4444');

      vuCtx.fillStyle = grad;
      vuCtx.fillRect(1, h - barH, w - 2, barH);

      if (level > 0) {
        const dbVal = (20 * Math.log10(level)).toFixed(1);
        peakDbVal.innerText = `${dbVal}dB`;
      } else {
        peakDbVal.innerText = '-inf';
      }
    }

    renderVuMeter();
  </script>
</body>
</html>