feat(mixer): install Master Strip Console per spec 47
This commit is contained in:
+306
-63
@@ -730,8 +730,304 @@ const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
|||||||
}, dbLabel),
|
}, dbLabel),
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
className: "text-[8px] font-mono truncate w-full text-center px-1 py-0.5 bg-[#222] border-t border-black/60 shrink-0",
|
className: "text-[8px] font-mono truncate w-full text-center px-1 py-0.5 bg-[#222] border-t border-black/60 shrink-0",
|
||||||
style: { color: trackColor }
|
style: { color: trackColor }
|
||||||
}, track.name));
|
}, track.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Master Strip Console Component (from md/47_MASTER_STRIP_CONSOLE.md) ──
|
||||||
|
const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal, setShowMasteringModal, masteringSettings, setMasteringSettings }) => {
|
||||||
|
const [isFxActive, setIsFxActive] = React.useState(true);
|
||||||
|
const [isTestPlaying, setIsTestPlaying] = React.useState(false);
|
||||||
|
|
||||||
|
const vuCanvasRef = React.useRef(null);
|
||||||
|
const testSourceRef = React.useRef(null);
|
||||||
|
const animFrameRef = React.useRef(null);
|
||||||
|
const peakLRef = React.useRef(null);
|
||||||
|
const peakRRef = React.useRef(null);
|
||||||
|
const rmsValRef = React.useRef(null);
|
||||||
|
const dbReadoutRef = React.useRef(null);
|
||||||
|
|
||||||
|
const ensureAudio = () => {
|
||||||
|
getAudioContext();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFaderChange = (val) => {
|
||||||
|
setMasterVolume(val);
|
||||||
|
ensureAudio();
|
||||||
|
if (masterBus && masterBus.output) {
|
||||||
|
const linear = val <= -50 ? 0 : Math.pow(10, val / 20);
|
||||||
|
masterBus.output.gain.setTargetAtTime(linear, audioCtx.currentTime, 0.01);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 startTestAudio = () => {
|
||||||
|
ensureAudio();
|
||||||
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||||
|
stopTestAudio();
|
||||||
|
|
||||||
|
const sampleRate = audioCtx.sampleRate;
|
||||||
|
const bufferSize = sampleRate * 2;
|
||||||
|
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;
|
||||||
|
const kick = Math.sin(2 * Math.PI * (55 * Math.exp(-(t % 0.5) * 15))) * Math.max(0, 1 - (t % 0.5) * 6);
|
||||||
|
const synth = Math.sin(2 * Math.PI * 440 * t) * 0.15;
|
||||||
|
left[i] = kick * 0.7 + synth;
|
||||||
|
right[i] = kick * 0.7 + synth * 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = audioCtx.createBufferSource();
|
||||||
|
source.buffer = buffer;
|
||||||
|
source.loop = true;
|
||||||
|
source.connect(masterBus.input);
|
||||||
|
source.start();
|
||||||
|
testSourceRef.current = source;
|
||||||
|
setIsTestPlaying(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopTestAudio = () => {
|
||||||
|
if (testSourceRef.current) {
|
||||||
|
try { testSourceRef.current.stop(); } catch(e) {}
|
||||||
|
try { testSourceRef.current.disconnect(); } catch(e) {}
|
||||||
|
testSourceRef.current = null;
|
||||||
|
}
|
||||||
|
setIsTestPlaying(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const canvas = vuCanvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
animFrameRef.current = requestAnimationFrame(render);
|
||||||
|
|
||||||
|
canvas.width = canvas.clientWidth;
|
||||||
|
canvas.height = canvas.clientHeight;
|
||||||
|
|
||||||
|
const w = canvas.width;
|
||||||
|
const h = canvas.height;
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
let levelL = 0;
|
||||||
|
let levelR = 0;
|
||||||
|
|
||||||
|
if (masterBus && masterBus.leftAnalyser && masterBus.rightAnalyser) {
|
||||||
|
const leftData = new Uint8Array(256);
|
||||||
|
const rightData = new Uint8Array(256);
|
||||||
|
masterBus.leftAnalyser.getByteTimeDomainData(leftData);
|
||||||
|
masterBus.rightAnalyser.getByteTimeDomainData(rightData);
|
||||||
|
|
||||||
|
let peakL = 0;
|
||||||
|
let peakR = 0;
|
||||||
|
for (let i = 0; i < leftData.length; i++) {
|
||||||
|
const v = Math.abs(leftData[i] - 128) / 128;
|
||||||
|
if (v > peakL) peakL = v;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < rightData.length; i++) {
|
||||||
|
const v = Math.abs(rightData[i] - 128) / 128;
|
||||||
|
if (v > peakR) peakR = v;
|
||||||
|
}
|
||||||
|
levelL = peakL;
|
||||||
|
levelR = peakR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const padding = 4;
|
||||||
|
const gap = 4;
|
||||||
|
const barW = Math.max(4, (w - padding * 2 - gap) / 2);
|
||||||
|
|
||||||
|
const grad = ctx.createLinearGradient(0, h, 0, 0);
|
||||||
|
grad.addColorStop(0, '#10b981');
|
||||||
|
grad.addColorStop(0.7, '#f59e0b');
|
||||||
|
grad.addColorStop(0.95, '#ef4444');
|
||||||
|
|
||||||
|
ctx.fillStyle = grad;
|
||||||
|
ctx.fillRect(padding, h - levelL * h, barW, levelL * h);
|
||||||
|
ctx.fillRect(padding + barW + gap, h - levelR * h, barW, levelR * h);
|
||||||
|
|
||||||
|
const dbL = levelL > 0 ? (20 * Math.log10(levelL)).toFixed(1) : '-inf';
|
||||||
|
const dbR = levelR > 0 ? (20 * Math.log10(levelR)).toFixed(1) : '-inf';
|
||||||
|
if (peakLRef.current) peakLRef.current.innerText = dbL > 0 ? dbL + 'dB' : '-inf';
|
||||||
|
if (peakRRef.current) peakRRef.current.innerText = dbR > 0 ? dbR + 'dB' : '-inf';
|
||||||
|
|
||||||
|
const maxLevel = Math.max(levelL, levelR);
|
||||||
|
if (rmsValRef.current) {
|
||||||
|
rmsValRef.current.innerText = (isTestPlaying || isPlaying) && maxLevel > 0 ? ((20 * Math.log10(maxLevel) - 3).toFixed(1)) : '-inf';
|
||||||
|
}
|
||||||
|
if (dbReadoutRef.current) {
|
||||||
|
dbReadoutRef.current.innerText = masterVolume <= -50 ? '-inf dB' : `${masterVolume > 0 ? '+' : ''}${masterVolume.toFixed(2)}dB`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
return () => {
|
||||||
|
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
|
||||||
|
};
|
||||||
|
}, [isTestPlaying, isPlaying, masterVolume]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
stopTestAudio();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFxClick = () => {
|
||||||
|
setShowMasteringModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePowerClick = () => {
|
||||||
|
const newActive = !isFxActive;
|
||||||
|
setIsFxActive(newActive);
|
||||||
|
if (setMasteringSettings) {
|
||||||
|
setMasteringSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
isBypassed: !newActive,
|
||||||
|
masterConnected: newActive
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement("div", {
|
||||||
|
className: "flex flex-col items-stretch w-[400px] shrink-0 strip-bg rounded-lg p-3 flex flex-col justify-between text-slate-300 select-none shadow-2xl relative"
|
||||||
|
},
|
||||||
|
React.createElement("div", { className: "space-y-1.5 mb-2" },
|
||||||
|
React.createElement("button", {
|
||||||
|
className: "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"
|
||||||
|
}, "Wave Observer"),
|
||||||
|
React.createElement("div", {
|
||||||
|
className: "flex items-center justify-between bg-slate-950 border border-slate-800 rounded px-1.5 py-0.5 text-[10px] font-mono"
|
||||||
|
},
|
||||||
|
React.createElement("span", { className: "text-slate-400 truncate" }, "Output 1 / Output 2"),
|
||||||
|
React.createElement("i", { className: "fa-solid fa-circle-notch text-[9px] text-slate-500" })
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement("div", { className: "flex flex-col items-center my-1.5" },
|
||||||
|
React.createElement("span", { className: "text-[10px] text-slate-400 font-mono mb-0.5" }, "center"),
|
||||||
|
React.createElement("div", {
|
||||||
|
className: "w-7 h-7 rounded-full bg-slate-800 border-2 border-slate-600 relative flex items-center justify-center shadow-inner cursor-pointer"
|
||||||
|
}, React.createElement("div", { className: "w-0.5 h-2.5 bg-slate-200 rounded absolute top-0.5" })),
|
||||||
|
React.createElement("div", {
|
||||||
|
ref: dbReadoutRef,
|
||||||
|
className: "text-xs font-bold font-mono text-slate-200 mt-1.5"
|
||||||
|
}, "0.00dB"),
|
||||||
|
React.createElement("div", {
|
||||||
|
className: "flex justify-between w-full text-[9px] font-mono text-slate-400 px-4 mt-0.5"
|
||||||
|
},
|
||||||
|
React.createElement("span", { ref: peakLRef }, "-inf"),
|
||||||
|
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("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: "w-16 flex items-center 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
|
||||||
|
},
|
||||||
|
React.createElement("div", { className: "w-0.5 h-full bg-slate-700 absolute" }),
|
||||||
|
React.createElement("input", {
|
||||||
|
id: "masterFader",
|
||||||
|
type: "range",
|
||||||
|
min: "-60",
|
||||||
|
max: "12",
|
||||||
|
step: "0.5",
|
||||||
|
value: masterVolume,
|
||||||
|
orient: "vertical",
|
||||||
|
className: "fader-slider w-full h-36 z-10",
|
||||||
|
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: "w-8 flex flex-col justify-between gap-1 text-[10px] font-bold" },
|
||||||
|
React.createElement("button", { className: "btn-daw h-5 rounded flex flex-col items-center justify-center text-[8px]", title: "Mono Switch" },
|
||||||
|
React.createElement("i", { className: "fa-solid fa-circle-half-stroke text-[9px]" }),
|
||||||
|
React.createElement("span", null, "MONO")
|
||||||
|
),
|
||||||
|
React.createElement("button", { className: "btn-daw h-5 rounded text-amber-500 font-bold hover:text-amber-400" }, "M"),
|
||||||
|
React.createElement("button", { className: "btn-daw h-5 rounded text-yellow-400 font-bold hover:text-yellow-300" }, "S"),
|
||||||
|
React.createElement("button", { className: "btn-daw h-5 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" : ""),
|
||||||
|
onClick: handleFxClick,
|
||||||
|
title: "Và 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" : ""),
|
||||||
|
onClick: handlePowerClick,
|
||||||
|
title: "Bật/Tắt MASTERING PANEL"
|
||||||
|
}, 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("i", { className: "fa-solid fa-info" })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement("div", { className: "mt-2 pt-1 border-t border-slate-800 flex flex-col items-center" },
|
||||||
|
React.createElement("div", { className: "flex justify-between w-full text-[9px] font-mono mb-0.5" },
|
||||||
|
React.createElement("span", { className: "text-emerald-400" }, "RMS"),
|
||||||
|
React.createElement("span", { ref: rmsValRef, className: "text-emerald-400 font-bold" }, "-inf")
|
||||||
|
),
|
||||||
|
React.createElement("div", {
|
||||||
|
className: "w-full flex items-center justify-between bg-black/80 py-0.5 rounded border border-slate-800 text-[10px] font-extrabold tracking-widest text-slate-100 uppercase px-1"
|
||||||
|
},
|
||||||
|
React.createElement("span", null, "MASTER"),
|
||||||
|
isTestPlaying
|
||||||
|
? React.createElement("button", {
|
||||||
|
onClick: stopTestAudio,
|
||||||
|
className: "text-[8px] bg-red-900 hover:bg-red-800 text-red-300 rounded px-1 py-0.5 transition-colors"
|
||||||
|
}, "STOP TEST")
|
||||||
|
: React.createElement("button", {
|
||||||
|
onClick: startTestAudio,
|
||||||
|
className: "text-[8px] bg-slate-800 hover:bg-slate-700 text-slate-300 rounded px-1 py-0.5 transition-colors"
|
||||||
|
}, "TEST")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const WaveformLane = ({
|
const WaveformLane = ({
|
||||||
track,
|
track,
|
||||||
@@ -19658,67 +19954,14 @@ const App = () => {
|
|||||||
className: "w-3 h-3"
|
className: "w-3 h-3"
|
||||||
})))), /*#__PURE__*/React.createElement("div", {
|
})))), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex-1 flex overflow-x-auto p-1.5 gap-1.5 items-stretch"
|
className: "flex-1 flex overflow-x-auto p-1.5 gap-1.5 items-stretch"
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}, /*#__PURE__*/React.createElement(MasterStripConsole, {
|
||||||
className: "flex flex-col items-stretch w-[108px] shrink-0 bg-[#2b2b2b] border border-black/70 overflow-hidden rounded-sm"
|
masterVolume: masterVolume,
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
setMasterVolume: setMasterVolume,
|
||||||
className: "text-[9px] font-bold text-zinc-300 uppercase tracking-wider w-full text-center py-0.5 bg-[#222] border-b border-black/60 shrink-0"
|
showMasteringModal: showMasteringModal,
|
||||||
}, "MASTER"), /*#__PURE__*/React.createElement("div", {
|
setShowMasteringModal: setShowMasteringModal,
|
||||||
className: "flex-1 flex items-stretch justify-center gap-1 px-1 py-1 min-h-0"
|
masteringSettings: masteringSettings,
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
setMasteringSettings: setMasteringSettings
|
||||||
className: "w-9 rounded-sm relative overflow-hidden bg-[#0d0d0d] border border-black/60 flex flex-col items-center cursor-pointer",
|
}), activeTracks.length > 0 && /*#__PURE__*/React.createElement("div", {
|
||||||
onMouseDown: function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var rect = e.currentTarget.getBoundingClientRect();
|
|
||||||
function onMove(ev) {
|
|
||||||
var pct = 1 - Math.max(0, Math.min(1, (ev.clientY - rect.top) / rect.height));
|
|
||||||
var val = Math.round((pct * 72 - 60) * 2) / 2;
|
|
||||||
setMasterVolume(val);
|
|
||||||
var linear = val <= -50 ? 0 : Math.pow(10, val / 20);
|
|
||||||
getAudioContext();
|
|
||||||
if (masterBus && masterBus.output) masterBus.output.gain.setValueAtTime(linear, audioCtx.currentTime);
|
|
||||||
}
|
|
||||||
function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }
|
|
||||||
document.addEventListener('mousemove', onMove);
|
|
||||||
document.addEventListener('mouseup', onUp);
|
|
||||||
onMove(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/* 0dB reference line */
|
|
||||||
React.createElement("div", {
|
|
||||||
className: "absolute w-full h-px bg-amber-400/70 z-10 pointer-events-none",
|
|
||||||
style: { bottom: '83.333%' }
|
|
||||||
}),
|
|
||||||
/* Tick marks */
|
|
||||||
[-48,-36,-24,-12,0,6].map(function(db) {
|
|
||||||
var pct = (db + 60) / 72 * 100;
|
|
||||||
return React.createElement("div", {
|
|
||||||
key: db,
|
|
||||||
className: "absolute w-1.5 h-px bg-slate-600/50 z-10 pointer-events-none",
|
|
||||||
style: { bottom: pct + '%', left: db === 0 ? '0' : 'auto', right: db === 0 ? 'auto' : '0', width: db === 0 ? '100%' : '4px' }
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
React.createElement("div", {
|
|
||||||
className: "flex-1 w-full flex flex-col items-center pb-0.5",
|
|
||||||
style: { background: 'linear-gradient(to top, #38bdf8, #f59e0b, #ef4444)' }
|
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
|
||||||
className: "w-full bg-[#0d0d0d] transition-all duration-75",
|
|
||||||
style: { height: `${(1 - Math.min(1, Math.max(0, (masterVolume + 60) / 72))) * 100}%` }
|
|
||||||
}))), /*#__PURE__*/React.createElement("div", {
|
|
||||||
className: "w-[18px] rounded-sm relative overflow-hidden bg-[#0d0d0d] border border-black/60"
|
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
|
||||||
className: "absolute bottom-0 w-full transition-all duration-75",
|
|
||||||
style: { height: `${Math.min(100, masterVU * 100)}%`, background: masterVU > 0.85 ? '#ef4444' : masterVU > 0.7 ? '#f59e0b' : '#38bdf8' }
|
|
||||||
}))), /*#__PURE__*/React.createElement("div", {
|
|
||||||
className: "text-[8px] font-mono font-bold text-center py-0.5 bg-[#222] border-t border-black/60 shrink-0 flex items-center justify-center gap-1"
|
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
|
||||||
className: "text-zinc-400"
|
|
||||||
}, masterVolume > 0 ? '+' : '', masterVolume.toFixed(1), " dB"), /*#__PURE__*/React.createElement("button", {
|
|
||||||
onClick: () => setShowMasteringModal(true),
|
|
||||||
className: "px-1 py-0.5 text-[7px] font-bold uppercase bg-cyan-900 hover:bg-cyan-800 text-cyan-300 rounded transition-colors",
|
|
||||||
title: "Open Mastering Suite"
|
|
||||||
}, "M")), /*#__PURE__*/React.createElement("div", {
|
|
||||||
className: "text-[8px] font-mono font-bold text-zinc-500 w-full text-center py-0.5 bg-[#222] border-t border-black/60 shrink-0"
|
|
||||||
}, "MAIN OUT")), activeTracks.length > 0 && /*#__PURE__*/React.createElement("div", {
|
|
||||||
className: "w-px bg-zinc-700 shrink-0 self-stretch mx-0.5"
|
className: "w-px bg-zinc-700 shrink-0 self-stretch mx-0.5"
|
||||||
}), activeTracks.map(function(track, idx) {
|
}), activeTracks.map(function(track, idx) {
|
||||||
return /*#__PURE__*/React.createElement(MixerStrip, {
|
return /*#__PURE__*/React.createElement(MixerStrip, {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -268,6 +268,62 @@
|
|||||||
.oz-scrollbar::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 2px; }
|
.oz-scrollbar::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 2px; }
|
||||||
.oz-scrollbar::-webkit-scrollbar-thumb:hover { background: #334155; }
|
.oz-scrollbar::-webkit-scrollbar-thumb:hover { background: #334155; }
|
||||||
.oz-font-mono { font-family: 'JetBrains Mono', monospace; }
|
.oz-font-mono { font-family: 'JetBrains Mono', monospace; }
|
||||||
|
|
||||||
|
/* Master Strip Console Styles (from md/47_MASTER_STRIP_CONSOLE.md) */
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metal Fader Cap */
|
||||||
|
.fader-cap {
|
||||||
|
background: linear-gradient(180deg, #d8d8d8 0%, #888888 45%, #444444 50%, #aaaaaa 100%);
|
||||||
|
border: 1px solid #111;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.9), inset 0 1px 1px rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
.btn-daw:hover {
|
||||||
|
background: linear-gradient(180deg, #555 0%, #333 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Range Input Custom Vertical Fader */
|
||||||
|
input[type=range].fader-slider {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
writing-mode: bt-linear;
|
||||||
|
appearance: slider-vertical;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
input[type=range].fader-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
background: linear-gradient(180deg, #e2e8f0 0%, #64748b 50%, #334155 100%);
|
||||||
|
border: 1px solid #0f172a;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.8);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -836,3 +836,10 @@
|
|||||||
- **Tóm tắt thay đổi:** (1) Tạo `UndoRedoEngine` service (`app/static/js/services/undoRedoEngine.js`) — global stack hỗ trợ cross-cutting state changes (BPM, playhead, selection, AI prompt, instrument). (2) Extend `handleUndo`/`handleRedo` trong `app.jsx` để ưu tiên `UndoRedoEngine`, fallback về track-level undo stack cũ. (3) Áp dụng undo/redo cho: Delete/Create items (section, midi, clip, track), text AI Prompt (native textarea undo stack + engine integration), instrument assignment, playhead position, item drag position, selection changes, BPM/tempo changes. (4) Update undo/redo buttons disabled state + keyboard shortcut indicators. (5) Global Ctrl+Z/Y handler skip INPUT/TEXTAREA focus để text input native undo hoạt động đúng.
|
- **Tóm tắt thay đổi:** (1) Tạo `UndoRedoEngine` service (`app/static/js/services/undoRedoEngine.js`) — global stack hỗ trợ cross-cutting state changes (BPM, playhead, selection, AI prompt, instrument). (2) Extend `handleUndo`/`handleRedo` trong `app.jsx` để ưu tiên `UndoRedoEngine`, fallback về track-level undo stack cũ. (3) Áp dụng undo/redo cho: Delete/Create items (section, midi, clip, track), text AI Prompt (native textarea undo stack + engine integration), instrument assignment, playhead position, item drag position, selection changes, BPM/tempo changes. (4) Update undo/redo buttons disabled state + keyboard shortcut indicators. (5) Global Ctrl+Z/Y handler skip INPUT/TEXTAREA focus để text input native undo hoạt động đúng.
|
||||||
- **Các file ảnh hưởng:** `app/static/js/services/undoRedoEngine.js` (NEW), `app/static/js/app.jsx`
|
- **Các file ảnh hưởng:** `app/static/js/services/undoRedoEngine.js` (NEW), `app/static/js/app.jsx`
|
||||||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||||||
|
---
|
||||||
|
|
||||||
|
### [2026-07-30 12:37] Task: Install Master Strip Console in mixer panel
|
||||||
|
- **Tóm tắt thay đổi:** (1) Thêm CSS styles cho Master Strip Console (`strip-bg`, `fader-track`, `btn-daw`, `btn-teal-active`, `fader-slider`) vào `app/templates/index.html`. (2) Tạo component `MasterStripConsole` trong `app/static/js/app.jsx` — thay thế MASTER strip cũ (fader đơn giản) bằng console theo spec `md/47_MASTER_STRIP_CONSOLE.md`: dual stereo VU meter canvas với dB scales L/R, vertical fader -60~+12dB, button stack (Mono/M/S/Route/FX/Power/Trim/Info), pan knob, dB readout, RMS footer. (3) Tích hợp test signal engine (Play/Stop test audio vào masterBus.input), FX button mở MasteringModal, Power button toggle mastering active/bypass. (4) VU meter animation loop sử dụng `masterBus.leftAnalyser`/`rightAnalyser` thay vì synthetic data.
|
||||||
|
- **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/app.jsx`
|
||||||
|
- **Ghi chú/Test (nếu có):** `npm run build` — build passes (Babel note: deoptimised styling for file >500KB, không phải lỗi).
|
||||||
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user