461 lines
19 KiB
Markdown
461 lines
19 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
+-------------------------------------------------------------+
|
|
| 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
|
|
|
|
``` 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>
|
|
``` |