FEAT: thay đổi UI của mixer panel

This commit is contained in:
2026-07-30 14:52:57 +07:00
parent b67c0264fb
commit bbd06f23d5
5 changed files with 962 additions and 54 deletions
+35 -51
View File
@@ -735,7 +735,7 @@ const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
};
// Master Strip Console Component (from md/47_MASTER_STRIP_CONSOLE.md)
const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal, setShowMasteringModal, masteringSettings, setMasteringSettings }) => {
const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal, setShowMasteringModal, masteringSettings, setMasteringSettings, isPlaying }) => {
const [isFxActive, setIsFxActive] = React.useState(true);
const [isTestPlaying, setIsTestPlaying] = React.useState(false);
const [isMuted, setIsMuted] = React.useState(false);
@@ -768,23 +768,6 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
}
};
const handleFaderMouseDown = (e) => {
e.preventDefault();
const rect = e.currentTarget.getBoundingClientRect();
function onMove(ev) {
const pct = 1 - Math.max(0, Math.min(1, (ev.clientY - rect.top) / rect.height));
const val = Math.round((pct * 72 - 60) * 2) / 2;
handleFaderChange(val);
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
onMove(e);
};
const handlePanPointerDown = (e) => {
isPanDraggingRef.current = true;
panStartYRef.current = e.clientY;
@@ -933,7 +916,7 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
}, []);
return React.createElement("div", {
className: "flex flex-col items-stretch w-[400px] shrink-0 strip-bg rounded-lg p-3 text-slate-300 select-none shadow-2xl relative"
className: "flex flex-col items-stretch w-[340px] shrink-0 strip-bg rounded-lg p-3 text-slate-300 select-none shadow-2xl relative"
},
React.createElement("div", { className: "space-y-1.5 mb-2" },
React.createElement("button", {
@@ -973,31 +956,30 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
React.createElement("span", { ref: peakRRef }, "-inf")
)
),
React.createElement("div", { className: "flex-1 flex gap-2 my-2 justify-between items-stretch" },
React.createElement("div", { className: "flex-1 flex items-center justify-between bg-black/80 p-1.5 rounded border border-slate-800/90 relative shadow-inner" },
React.createElement("div", { className: "flex flex-col justify-between h-40 text-[8px] font-mono text-slate-400 select-none pr-1 text-right border-r border-slate-800/60" },
React.createElement("span", null, "+12"), React.createElement("span", null, "+6"), React.createElement("span", null, "0"), React.createElement("span", null, "-6"),
React.createElement("span", null, "-12"), React.createElement("span", null, "-18"), React.createElement("span", null, "-24"), React.createElement("span", null, "-30"),
React.createElement("span", null, "-36"), React.createElement("span", null, "-42"), React.createElement("span", null, "-54")
),
React.createElement("div", { className: "flex-1 h-40 relative bg-slate-950 rounded overflow-hidden mx-1.5 border border-slate-900" },
React.createElement("div", { className: "flex gap-2 my-2 justify-between items-stretch shrink-0", style: {height: '150px'} },
React.createElement("div", { className: "flex-1 flex items-stretch justify-between bg-black/80 p-1.5 rounded border border-slate-800/90 relative shadow-inner" },
React.createElement("div", { className: "flex flex-col h-full text-[8px] font-mono text-slate-400 select-none pr-1 text-right border-r border-slate-800/60 overflow-hidden" },
React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "+12"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "+6"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "0"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-6"),
React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-12"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-18"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-24"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-30"),
React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-36"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-42"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-54")
),
React.createElement("div", { className: "flex-1 relative bg-slate-950 rounded overflow-hidden mx-1.5 border border-slate-900" },
React.createElement("canvas", { ref: vuCanvasRef, className: "w-full h-full block" }),
React.createElement("div", { className: "absolute bottom-0.5 inset-x-0 flex justify-around text-[7px] font-mono text-slate-500 font-bold pointer-events-none bg-black/60 py-0.5" },
React.createElement("span", null, "L"), React.createElement("span", null, "R")
)
),
React.createElement("div", { className: "flex flex-col justify-between h-40 text-[8px] font-mono text-slate-400 select-none pl-1 text-left border-l border-slate-800/60" },
React.createElement("span", null, "+12"), React.createElement("span", null, "+6"), React.createElement("span", null, "0"), React.createElement("span", null, "-6"),
React.createElement("span", null, "-12"), React.createElement("span", null, "-18"), React.createElement("span", null, "-24"), React.createElement("span", null, "-30"),
React.createElement("span", null, "-36"), React.createElement("span", null, "-42"), React.createElement("span", null, "-54")
)
React.createElement("div", { className: "flex flex-col h-full text-[8px] font-mono text-slate-400 select-none pl-1 text-left border-l border-slate-800/60 overflow-hidden" },
React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "+12"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "+6"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "0"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-6"),
React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-12"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-18"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-24"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-30"),
React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-36"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-42"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-54")
)
),
React.createElement("div", { className: "w-16 flex items-center gap-1 bg-slate-900/60 p-1 rounded border border-slate-800" },
React.createElement("div", { className: "w-16 flex items-stretch gap-1 bg-slate-900/60 p-1 rounded border border-slate-800" },
React.createElement("div", {
className: "flex-1 flex flex-col items-center justify-center relative fader-track rounded py-2 px-0.5 h-40",
onMouseDown: handleFaderMouseDown
className: "flex-1 flex flex-col items-center justify-center relative fader-track rounded py-2 px-0.5"
},
React.createElement("div", { className: "w-0.5 h-full bg-slate-700 absolute" }),
React.createElement("div", { className: "w-0.5 flex-1 bg-slate-700 absolute" }),
React.createElement("input", {
id: "masterFader",
type: "range",
@@ -1005,20 +987,21 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
max: "12",
step: "0.5",
value: masterVolume,
className: "fader-slider w-full h-36 z-10",
className: "fader-slider w-full z-10",
orient: "vertical",
onChange: function(e) { handleFaderChange(parseFloat(e.target.value)); }
})
),
React.createElement("div", { className: "flex flex-col justify-between h-36 text-[7px] font-mono text-slate-500 select-none pr-0.5" },
React.createElement("span", null, "+12"), React.createElement("span", null, "+6"), React.createElement("span", null, "0"), React.createElement("span", null, "-6"),
React.createElement("span", null, "-12"), React.createElement("span", null, "-24"), React.createElement("span", null, "-36"), React.createElement("span", null, "-54")
)
React.createElement("div", { className: "flex flex-col text-[7px] font-mono text-slate-500 select-none pr-0.5 overflow-hidden" },
React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "+12"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "+6"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "0"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-6"),
React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-12"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-24"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-36"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-54")
)
),
React.createElement("div", { className: "w-8 flex flex-col justify-between gap-1 text-[10px] font-bold" },
React.createElement("div", { className: "w-8 flex flex-col justify-between text-[10px] font-bold" },
React.createElement("button", {
id: "monoBtn",
onClick: function() { setIsMono(function(p) { return !p; }); },
className: "btn-daw h-5 rounded flex flex-col items-center justify-center text-[8px]" + (isMono ? " btn-mono-active" : ""),
className: "btn-daw h-[18px] rounded flex flex-col items-center justify-center text-[8px]" + (isMono ? " btn-mono-active" : ""),
title: "Mono Switch"
},
React.createElement("i", { className: "fa-solid fa-circle-half-stroke text-[9px]" }),
@@ -1027,26 +1010,26 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
React.createElement("button", {
id: "muteBtn",
onClick: function() { setIsMuted(function(p) { return !p; }); },
className: "btn-daw h-5 rounded text-amber-500 font-bold hover:text-amber-400" + (isMuted ? " btn-mute-active" : ""),
className: "btn-daw h-[18px] rounded text-amber-500 font-bold hover:text-amber-400" + (isMuted ? " btn-mute-active" : ""),
title: "Mute Master Output"
}, "M"),
React.createElement("button", {
id: "soloBtn",
className: "btn-daw h-5 rounded text-yellow-400 font-bold hover:text-yellow-300",
className: "btn-daw h-[18px] rounded text-yellow-400 font-bold hover:text-yellow-300",
title: "Solo Master"
}, "S"),
React.createElement("button", { className: "btn-daw h-5 rounded text-slate-400 hover:text-slate-200", title: "Route Matrix" },
React.createElement("button", { className: "btn-daw h-[18px] rounded text-slate-400 hover:text-slate-200", title: "Route Matrix" },
React.createElement("i", { className: "fa-solid fa-diagram-project text-[9px]" })
),
React.createElement("button", {
id: "fxBtn",
className: "btn-daw h-6 rounded font-extrabold text-[10px] transition-all" + (isFxActive ? " btn-teal-active" : ""),
className: "btn-daw h-[18px] rounded font-extrabold text-[10px] transition-all" + (isFxActive ? " btn-teal-active" : ""),
onClick: function() { setShowMasteringModal(true); },
title: "Mở MASTERING PANEL để chỉnh sửa"
}, "FX"),
React.createElement("button", {
id: "powerBtn",
className: "btn-daw h-6 rounded text-xs transition-all" + (isFxActive ? " btn-teal-active" : ""),
className: "btn-daw h-[18px] rounded text-xs transition-all" + (isFxActive ? " btn-teal-active" : ""),
onClick: function() {
var newActive = !isFxActive;
setIsFxActive(newActive);
@@ -1056,8 +1039,8 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
},
title: "Bật/Tắt MASTERING PANEL Bypass"
}, React.createElement("i", { className: "fa-solid fa-power-off" })),
React.createElement("button", { className: "btn-daw h-5 rounded text-slate-400 text-[8px]", title: "Trim Envelope" }, "TRIM"),
React.createElement("button", { className: "btn-daw h-5 rounded text-slate-400 text-[9px]", title: "Session Info" },
React.createElement("button", { className: "btn-daw h-[18px] rounded text-slate-400 text-[8px]", title: "Trim Envelope" }, "TRIM"),
React.createElement("button", { className: "btn-daw h-[18px] rounded text-slate-400 text-[9px]", title: "Session Info" },
React.createElement("i", { className: "fa-solid fa-info" })
)
)
@@ -20243,7 +20226,8 @@ const App = () => {
showMasteringModal: showMasteringModal,
setShowMasteringModal: setShowMasteringModal,
masteringSettings: masteringSettings,
setMasteringSettings: setMasteringSettings
setMasteringSettings: setMasteringSettings,
isPlaying: isPlaying
}), activeTracks.length > 0 && /*#__PURE__*/React.createElement("div", {
className: "w-px bg-zinc-700 shrink-0 self-stretch mx-0.5"
}), activeTracks.map(function(track, idx) {
File diff suppressed because one or more lines are too long
+6 -1
View File
@@ -351,12 +351,17 @@
/* Custom Vertical Range Fader Slider */
input[type=range].fader-slider {
appearance: none;
-webkit-appearance: none;
appearance: none;
writing-mode: vertical-lr;
direction: rtl;
background: transparent;
cursor: pointer;
margin: 0;
padding: 0;
flex: 1;
min-height: 20px;
width: 100%;
}
input[type=range].fader-slider::-webkit-slider-thumb {
-webkit-appearance: none;
+458
View File
@@ -0,0 +1,458 @@
I have created `master_strip_gui.html` containing both the Markdown visual description and the full HTML5 implementation of the Master Strip console.
### Summary of What Was Created:
1. **Markdown Documentation Section**:
* **Signal Flow**: Detailing the `Source -> Main Out Slider (Gain) -> Analyser -> Destination` post-fader audio path.
* **Component Layout Breakdown**: Detailing the top routing header, dual stereo VU meters, narrow vertical fader, right-hand button stack, and RMS readout footer.
2. **HTML5 Implementation**:
* **Dual Stereo VU Meter**: Canvas-based visualizer featuring L/R channels with dB markings on both sides (`+12` to `-54 dB`).
* **Post-Fader Audio Engine**: AudioContext graph with live sine/kick test generator responding dynamically to slider and mastering panel controls.
``` 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 Master Strip Console</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; }
/* REAPER Metallic Dark DAW Styling */
.strip-bg {
background: linear-gradient(180deg, #2f2f2f 0%, #222222 100%);
border: 1px solid #141414;
box-shadow: inset 1px 1px 0 rgba(255,255,255,0.08), 0 8px 24px rgba(0,0,0,0.8);
}
.fader-track {
background: #121212;
box-shadow: inset 1px 1px 3px rgba(0,0,0,0.9), 1px 1px 0 rgba(255,255,255,0.05);
}
/* DAW Metallic Button */
.btn-daw {
background: linear-gradient(180deg, #444 0%, #2a2a2a 100%);
border: 1px solid #1a1a1a;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.15), 0 2px 4px rgba(0,0,0,0.5);
transition: all 0.1s ease;
}
.btn-daw:hover {
background: linear-gradient(180deg, #555 0%, #333 100%);
}
.btn-daw:active {
box-shadow: inset 0 2px 4px rgba(0,0,0,0.8);
transform: translateY(0.5px);
}
/* Teal Active State (FX & Power Button) */
.btn-teal-active {
background: linear-gradient(180deg, #10b981 0%, #047857 100%) !important;
border-color: #34d399 !important;
color: #ffffff !important;
box-shadow: 0 0 10px rgba(16, 185, 129, 0.6) !important;
}
/* Mute & Solo Active State */
.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-mono-active {
background: linear-gradient(180deg, #3b82f6 0%, #1e40af 100%) !important;
border-color: #60a5fa !important;
color: #ffffff !important;
}
/* Custom Vertical Range Fader Slider */
input[type=range].fader-slider {
appearance: none;
-webkit-appearance: none;
writing-mode: vertical-lr;
direction: rtl;
background: transparent;
cursor: pointer;
}
input[type=range].fader-slider::-webkit-slider-thumb {
-webkit-appearance: none;
height: 24px;
width: 22px;
background: linear-gradient(180deg, #e2e8f0 0%, #64748b 45%, #1e293b 50%, #94a3b8 100%);
border: 1px solid #000000;
border-radius: 2px;
box-shadow: 0 4px 8px rgba(0,0,0,0.9), inset 0 1px 1px rgba(255,255,255,0.9);
}
input[type=range].fader-slider::-moz-range-thumb {
height: 24px;
width: 22px;
background: linear-gradient(180deg, #e2e8f0 0%, #64748b 45%, #1e293b 50%, #94a3b8 100%);
border: 1px solid #000000;
border-radius: 2px;
box-shadow: 0 4px 8px rgba(0,0,0,0.9), inset 0 1px 1px rgba(255,255,255,0.9);
}
</style>
</head>
<body class="h-screen bg-slate-950 flex items-center justify-center p-4 overflow-hidden select-none">
<!-- DAW MASTER STRIP CONSOLE (REAPER RE-CREATION - CLEAN COMPONENT) -->
<div id="masterStrip" class="w-[400px] strip-bg rounded-lg p-3 flex flex-col justify-between text-slate-300 select-none shadow-2xl relative">
<!-- 1. TOP HEADER BUTTONS -->
<div class="space-y-1.5 mb-2">
<button class="w-full bg-slate-800 hover:bg-slate-700 text-[10px] font-semibold py-1 rounded border border-slate-700 text-slate-300 tracking-tight" title="FX Name">
MASTERING PANEL
</button>
<div class="flex items-center justify-between bg-slate-950 border border-slate-800 rounded px-1.5 py-0.5 text-[10px] font-mono">
<span class="text-slate-400 truncate">Output 1 / Output 2</span>
<i class="fa-solid fa-circle-notch text-[9px] text-slate-500"></i>
</div>
</div>
<!-- 2. PAN KNOB & DB READOUT -->
<div class="flex flex-col items-center my-1.5">
<span id="panText" class="text-[10px] text-slate-400 font-mono mb-0.5">center</span>
<!-- Pan Rotary Dial -->
<div id="panDial" class="w-7 h-7 rounded-full bg-slate-800 border-2 border-slate-600 relative flex items-center justify-center shadow-inner cursor-pointer" title="Kéo chuột để chỉnh Pan (Left/Right)">
<div id="panPointer" class="w-0.5 h-2.5 bg-slate-200 rounded absolute top-0.5 transition-transform" style="transform: rotate(0deg);"></div>
</div>
<div id="dbReadoutText" class="text-xs font-bold font-mono text-slate-200 mt-1.5">0.00dB</div>
<div class="flex justify-between w-full text-[9px] font-mono text-slate-400 px-4 mt-0.5">
<span id="peakL">-inf</span>
<span id="peakR">-inf</span>
</div>
</div>
<!-- 3. CENTER AREA: LARGE DUAL-CHANNEL STEREO VU METER + NARROW VOLUME SLIDER + BUTTON STACK -->
<div class="flex-1 flex gap-2 my-2 justify-between items-stretch">
<!-- LARGE DUAL-CHANNEL STEREO VU METER WITH DB SCALES ON BOTH SIDES -->
<div class="flex-1 flex items-center justify-between bg-black/80 p-1.5 rounded border border-slate-800/90 relative shadow-inner">
<!-- Left Channel dB Scale Markings -->
<div class="flex flex-col justify-between h-56 text-[8px] font-mono text-slate-400 select-none pr-1 text-right border-r border-slate-800/60">
<span>+12</span>
<span>+6</span>
<span>0</span>
<span>-6</span>
<span>-12</span>
<span>-18</span>
<span>-24</span>
<span>-30</span>
<span>-36</span>
<span>-42</span>
<span>-54</span>
</div>
<!-- Dual Stereo VU Meter Canvas (Left & Right Channels) -->
<div class="flex-1 h-56 relative bg-slate-950 rounded overflow-hidden mx-1.5 border border-slate-900">
<canvas id="vuMeterCanvas" class="w-full h-full block"></canvas>
<!-- L and R Labels -->
<div class="absolute bottom-0.5 inset-x-0 flex justify-around text-[7px] font-mono text-slate-500 font-bold pointer-events-none bg-black/60 py-0.5">
<span>L</span>
<span>R</span>
</div>
</div>
<!-- Right Channel dB Scale Markings -->
<div class="flex flex-col justify-between h-56 text-[8px] font-mono text-slate-400 select-none pl-1 text-left border-l border-slate-800/60">
<span>+12</span>
<span>+6</span>
<span>0</span>
<span>-6</span>
<span>-12</span>
<span>-18</span>
<span>-24</span>
<span>-30</span>
<span>-36</span>
<span>-42</span>
<span>-54</span>
</div>
</div>
<!-- NARROW VOLUME SLIDER TRACK WITH SIDE DB SCALE -->
<div class="w-16 flex items-center gap-1 bg-slate-900/60 p-1 rounded border border-slate-800">
<div class="flex-1 flex flex-col items-center justify-center relative fader-track rounded py-2 px-0.5 h-56">
<!-- Center Fader Rail Line -->
<div class="w-0.5 h-full bg-slate-700 absolute"></div>
<!-- Master Fader Slider -->
<input id="masterFader" type="range" min="-60" max="12" step="0.5" value="0"
class="fader-slider w-full h-52 z-10" title="Chỉnh Volume Master (Post-Fader Gain)">
</div>
<!-- Fader Side Scale Markings -->
<div class="flex flex-col justify-between h-52 text-[7px] font-mono text-slate-500 select-none pr-0.5">
<span>+12</span>
<span>+6</span>
<span>0</span>
<span>-6</span>
<span>-12</span>
<span>-24</span>
<span>-36</span>
<span>-54</span>
</div>
</div>
<!-- RIGHT SIDE BUTTON STACK (MONO, M, S, ROUTE, FX, POWER, TRIM, INFO) -->
<div class="w-8 flex flex-col justify-between gap-1 text-[10px] font-bold">
<button id="monoBtn" class="btn-daw h-5 rounded flex flex-col items-center justify-center text-[8px]" title="Mono Switch">
<i class="fa-solid fa-circle-half-stroke text-[9px]"></i>
<span>MONO</span>
</button>
<button id="muteBtn" class="btn-daw h-5 rounded text-amber-500 font-bold hover:text-amber-400" title="Mute Master Output">
M
</button>
<button id="soloBtn" class="btn-daw h-5 rounded text-yellow-400 font-bold hover:text-yellow-300" title="Solo Master">
S
</button>
<button class="btn-daw h-5 rounded text-slate-400 hover:text-slate-200" title="Route Matrix">
<i class="fa-solid fa-diagram-project text-[9px]"></i>
</button>
<!-- THE FX BUTTON (CLICK TO OPEN MASTERING PANEL) -->
<button id="fxBtn" class="btn-daw btn-teal-active h-6 rounded font-extrabold text-[10px] transition-all" title="Mở MASTERING PANEL để chỉnh sửa">
FX
</button>
<!-- POWER TOGGLE BUTTON (MASTERING PANEL ON/OFF) -->
<button id="powerBtn" class="btn-daw btn-teal-active h-6 rounded text-xs transition-all" title="Bật/Tắt MASTERING PANEL Bypass">
<i class="fa-solid fa-power-off"></i>
</button>
<button class="btn-daw h-5 rounded text-slate-400 text-[8px]" title="Trim Envelope">
TRIM
</button>
<button class="btn-daw h-5 rounded text-slate-400 text-[9px]" title="Session Info">
<i class="fa-solid fa-info"></i>
</button>
</div>
</div>
<!-- 4. FOOTER IDENTIFIER -->
<div class="mt-2 pt-1 border-t border-slate-800 flex flex-col items-center">
<div class="flex justify-between w-full text-[9px] font-mono mb-0.5">
<span class="text-emerald-400">RMS</span>
<span id="rmsVal" class="text-emerald-400 font-bold">-inf</span>
</div>
<div class="w-full text-center bg-black/80 py-0.5 rounded border border-slate-800 text-xs font-extrabold tracking-widest text-slate-100 uppercase">
MASTER
</div>
</div>
</div>
<!-- MASTERING PANEL MODAL OVERLAY (OPENED BY FX BUTTON) -->
<div id="masteringModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center hidden">
<div class="w-11/12 max-w-3xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl p-5 space-y-4">
<div class="flex items-center justify-between border-b border-slate-800 pb-3">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-emerald-400 animate-pulse"></div>
<h3 class="text-sm font-bold text-white uppercase tracking-wider">OZONE STYLE MASTERING SUITE PANEL</h3>
</div>
<button id="closeModalBtn" class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 text-white flex items-center justify-center transition-colors">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="grid grid-cols-3 gap-3">
<div class="bg-slate-950 p-3 rounded-xl border border-slate-800 text-center">
<span class="text-xs font-bold text-cyan-400 font-mono">DYNAMIC EQ</span>
<div class="text-[10px] text-slate-400 mt-2">4-Band Logarithmic Curve</div>
</div>
<div class="bg-slate-950 p-3 rounded-xl border border-slate-800 text-center">
<span class="text-xs font-bold text-amber-400 font-mono">STEREO IMAGER</span>
<div class="text-[10px] text-slate-400 mt-2">Polar Vectorscope Expansion</div>
</div>
<div class="bg-slate-950 p-3 rounded-xl border border-slate-800 text-center">
<span class="text-xs font-bold text-emerald-400 font-mono">IRC MAXIMIZER</span>
<div class="text-[10px] text-slate-400 mt-2">True Peak Brickwall Limiter</div>
</div>
</div>
<div class="p-3 bg-slate-950 rounded-xl border border-slate-800 text-xs font-mono text-slate-300 text-center">
Mastering FX Chain đang xử lý tín hiệu trước khi đi qua MAIN OUT Volume Slider.
</div>
</div>
</div>
<!-- JAVASCRIPT ENGINE & EVENT LISTENERS -->
<script>
// State Store
const masterState = {
volumeDb: 0.0,
pan: 0.0,
isMuted: false,
isMono: false,
isFxActive: true
};
// Element References
const masterFader = document.getElementById('masterFader');
const dbReadoutText = document.getElementById('dbReadoutText');
const peakL = document.getElementById('peakL');
const peakR = document.getElementById('peakR');
const rmsVal = document.getElementById('rmsVal');
const panDial = document.getElementById('panDial');
const panPointer = document.getElementById('panPointer');
const panText = document.getElementById('panText');
const fxBtn = document.getElementById('fxBtn');
const powerBtn = document.getElementById('powerBtn');
const muteBtn = document.getElementById('muteBtn');
const monoBtn = document.getElementById('monoBtn');
const masteringModal = document.getElementById('masteringModal');
const vuCanvas = document.getElementById('vuMeterCanvas');
const vuCtx = vuCanvas.getContext('2d');
// Linear gain conversion: G = 10^(dB / 20)
function updateVolumeDisplay() {
const db = parseFloat(masterFader.value);
masterState.volumeDb = db;
dbReadoutText.innerText = db <= -60 ? '-inf dB' : `${db > 0 ? '+' : ''}${db.toFixed(2)}dB`;
}
masterFader.addEventListener('input', updateVolumeDisplay);
// Rotary Pan Knob Dragging
let isPanDragging = false;
let panStartY = 0;
let startPanVal = 0;
panDial.addEventListener('pointerdown', (e) => {
isPanDragging = true;
panStartY = e.clientY;
startPanVal = masterState.pan;
panDial.setPointerCapture(e.pointerId);
});
panDial.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));
masterState.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)}`;
});
panDial.addEventListener('pointerup', (e) => {
isPanDragging = false;
try { panDial.releasePointerCapture(e.pointerId); } catch(err){}
});
// Mute & Mono Buttons
muteBtn.addEventListener('click', () => {
masterState.isMuted = !masterState.isMuted;
muteBtn.classList.toggle('btn-mute-active', masterState.isMuted);
});
monoBtn.addEventListener('click', () => {
masterState.isMono = !masterState.isMono;
monoBtn.classList.toggle('btn-mono-active', masterState.isMono);
});
// Modal Trigger Buttons
fxBtn.addEventListener('click', () => {
masteringModal.classList.remove('hidden');
});
document.getElementById('closeModalBtn').addEventListener('click', () => {
masteringModal.classList.add('hidden');
});
powerBtn.addEventListener('click', () => {
masterState.isFxActive = !masterState.isFxActive;
powerBtn.classList.toggle('btn-teal-active', masterState.isFxActive);
fxBtn.classList.toggle('btn-teal-active', masterState.isFxActive);
});
// Dynamic Stereo VU Meter Renderer (Post-Fader Level Simulation)
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 levelL = 0;
let levelR = 0;
if (!masterState.isMuted) {
const db = masterState.volumeDb;
const linearGain = db <= -60 ? 0 : Math.pow(10, db / 20);
// Post-Fader Dynamic Level simulation
const baseSignal = 0.5 * linearGain;
if (baseSignal > 0) {
levelL = Math.min(1.0, Math.max(0, baseSignal * (0.9 + Math.random() * 0.18)));
levelR = Math.min(1.0, Math.max(0, baseSignal * (0.88 + Math.random() * 0.22)));
if (masterState.isMono) {
const monoLevel = (levelL + levelR) / 2;
levelL = monoLevel;
levelR = monoLevel;
}
}
}
// Draw 2 Stereo Channel Bars (Left and Right)
const padding = 4;
const gap = 4;
const barW = Math.max(4, (w - padding * 2 - gap) / 2);
// Color Gradient: Green -> Amber -> Red
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;
// Left Channel Bar
const barHL = levelL * h;
vuCtx.fillRect(padding, h - barHL, barW, barHL);
// Right Channel Bar
const barHR = levelR * h;
vuCtx.fillRect(padding + barW + gap, h - barHR, barW, barHR);
// Numeric Peak & RMS dB Readouts
const dbValL = levelL > 0 ? (20 * Math.log10(levelL)).toFixed(1) : '-inf';
const dbValR = levelR > 0 ? (20 * Math.log10(levelR)).toFixed(1) : '-inf';
peakL.innerText = levelL > 0 ? `${dbValL}dB` : '-inf';
peakR.innerText = levelR > 0 ? `${dbValR}dB` : '-inf';
const maxLevel = Math.max(levelL, levelR);
rmsVal.innerText = maxLevel > 0 ? `${(20 * Math.log10(maxLevel) - 3.2).toFixed(1)} dB` : '-inf';
}
renderVuMeter();
</script>
</body>
</html>
```
+461
View File
@@ -0,0 +1,461 @@
# 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>
```