FEAT: thêm tính năng hiển thị ở master track
This commit is contained in:
+155
-8
@@ -31,6 +31,81 @@ const API_TASKS = `${API_BASE_URL}/api/v1/audio/tasks`;
|
||||
// Storage for server-side file IDs mapped to track IDs
|
||||
let serverFileIdMap = {};
|
||||
let audioCtx;
|
||||
let masterBus = null; // { input, compressor, analyser, output, masteringActive }
|
||||
|
||||
function initMasterBus(ctx) {
|
||||
if (masterBus) return masterBus;
|
||||
masterBus = {
|
||||
input: ctx.createGain(),
|
||||
compressor: ctx.createDynamicsCompressor(),
|
||||
analyser: ctx.createAnalyser(),
|
||||
output: ctx.createGain(),
|
||||
masteringActive: false,
|
||||
// Mastering processing nodes (created on demand)
|
||||
eqNodes: null,
|
||||
masteringInput: null,
|
||||
masteringOutput: null
|
||||
};
|
||||
masterBus.analyser.fftSize = 256;
|
||||
masterBus.compressor.threshold.value = -6;
|
||||
masterBus.compressor.knee.value = 6;
|
||||
masterBus.compressor.ratio.value = 4;
|
||||
masterBus.compressor.attack.value = 0.003;
|
||||
masterBus.compressor.release.value = 0.25;
|
||||
masterBus.output.gain.value = 1;
|
||||
// Default routing: input → compressor → analyser → output → destination
|
||||
masterBus.input.connect(masterBus.compressor);
|
||||
masterBus.compressor.connect(masterBus.analyser);
|
||||
masterBus.analyser.connect(masterBus.output);
|
||||
masterBus.output.connect(ctx.destination);
|
||||
return masterBus;
|
||||
}
|
||||
|
||||
function setMasterVolume(linear) {
|
||||
if (masterBus) masterBus.output.gain.setValueAtTime(linear, audioCtx.currentTime);
|
||||
}
|
||||
|
||||
function toggleMasteringOnMaster(activate, nodes) {
|
||||
if (!masterBus) return;
|
||||
const ctx = audioCtx;
|
||||
const now = ctx.currentTime;
|
||||
if (activate && nodes && !masterBus.masteringActive) {
|
||||
// Disconnect default routing
|
||||
masterBus.input.disconnect();
|
||||
masterBus.compressor.disconnect();
|
||||
// Insert mastering chain: input → compressor → eqLow → eqMid1 → eqMid2 → eqHigh → boostGain → compressor2 → analyser → output
|
||||
masterBus.input.connect(masterBus.compressor);
|
||||
masterBus.compressor.connect(nodes.eqLowFilter);
|
||||
nodes.eqLowFilter.connect(nodes.eqMid1Filter);
|
||||
nodes.eqMid1Filter.connect(nodes.eqMid2Filter);
|
||||
nodes.eqMid2Filter.connect(nodes.eqHighFilter);
|
||||
nodes.eqHighFilter.connect(nodes.maximizerBoostGain);
|
||||
nodes.maximizerBoostGain.connect(nodes.maximizerCompressor);
|
||||
nodes.maximizerCompressor.connect(masterBus.analyser);
|
||||
masterBus.analyser.connect(masterBus.output);
|
||||
masterBus.masteringActive = true;
|
||||
masterBus.eqNodes = nodes;
|
||||
} else if (!activate && masterBus.masteringActive) {
|
||||
// Disconnect mastering chain
|
||||
masterBus.input.disconnect();
|
||||
masterBus.compressor.disconnect();
|
||||
if (masterBus.eqNodes) {
|
||||
try { masterBus.eqNodes.eqLowFilter.disconnect(); } catch(e) {}
|
||||
try { masterBus.eqNodes.eqMid1Filter.disconnect(); } catch(e) {}
|
||||
try { masterBus.eqNodes.eqMid2Filter.disconnect(); } catch(e) {}
|
||||
try { masterBus.eqNodes.eqHighFilter.disconnect(); } catch(e) {}
|
||||
try { masterBus.eqNodes.maximizerBoostGain.disconnect(); } catch(e) {}
|
||||
try { masterBus.eqNodes.maximizerCompressor.disconnect(); } catch(e) {}
|
||||
}
|
||||
// Restore default routing
|
||||
masterBus.input.connect(masterBus.compressor);
|
||||
masterBus.compressor.connect(masterBus.analyser);
|
||||
masterBus.analyser.connect(masterBus.output);
|
||||
masterBus.masteringActive = false;
|
||||
masterBus.eqNodes = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getAudioContext() {
|
||||
if (!audioCtx) {
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
@@ -6704,6 +6779,7 @@ const MasteringModal = ({ isOpen, onClose }) => {
|
||||
});
|
||||
const [isPlaying, setIsPlaying] = React.useState(false);
|
||||
const [peakLevels, setPeakLevels] = React.useState({ inPeak: 0.05, outPeak: 0.05 });
|
||||
const [masterConnected, setMasterConnected] = React.useState(false);
|
||||
|
||||
const audioRef = React.useRef({ ctx: null, nodes: {}, source: null });
|
||||
const eqCanvasRef = React.useRef(null);
|
||||
@@ -7022,9 +7098,30 @@ const MasteringModal = ({ isOpen, onClose }) => {
|
||||
if (!isOpen) {
|
||||
stopAudioDemo();
|
||||
knobsInitializedRef.current = false;
|
||||
if (masterConnected) {
|
||||
toggleMasteringOnMaster(false, null);
|
||||
setMasterConnected(false);
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Sync mastering chain with master bus
|
||||
React.useEffect(() => {
|
||||
const nodes = audioRef.current.nodes;
|
||||
const hasNodes = nodes.eqLowFilter && nodes.maximizerCompressor;
|
||||
if (masterConnected && hasNodes) {
|
||||
// Ensure audio engine is initialized with proper values
|
||||
initOzoneAudioEngine();
|
||||
toggleMasteringOnMaster(true, nodes);
|
||||
updateAudioGraphValues();
|
||||
} else if (!masterConnected) {
|
||||
toggleMasteringOnMaster(false, null);
|
||||
}
|
||||
return () => {
|
||||
if (masterConnected) toggleMasteringOnMaster(false, null);
|
||||
};
|
||||
}, [masterConnected]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const switchModule = (name) => setOzState(prev => ({ ...prev, activeModule: name }));
|
||||
@@ -7086,6 +7183,10 @@ const MasteringModal = ({ isOpen, onClose }) => {
|
||||
<div className="flex items-center gap-3 text-xs oz-font-mono">
|
||||
<span className="text-slate-400 text-[11px]">Target LUFS:</span>
|
||||
<span className="text-cyan-400 font-bold bg-slate-950 border border-cyan-900/60 px-2 py-0.5 rounded">-11.0 LUFS</span>
|
||||
<button onClick={() => { initOzoneAudioEngine(); setMasterConnected(prev => !prev); }}
|
||||
className={`px-2 py-0.5 rounded text-[10px] font-bold border transition-colors ${masterConnected ? 'bg-emerald-700 border-emerald-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700'}`}>
|
||||
{masterConnected ? 'Master ON' : 'Master OFF'}
|
||||
</button>
|
||||
<button onClick={onClose} className="text-slate-500 hover:text-slate-300 ml-2">
|
||||
<i data-lucide="x" className="w-4 h-4"></i>
|
||||
</button>
|
||||
@@ -7851,6 +7952,9 @@ const App = () => {
|
||||
var saved = localStorage.getItem('studio_mixer_height');
|
||||
return saved ? parseInt(saved) : 200;
|
||||
}());
|
||||
const [masterVolume, setMasterVolume] = useState(0); // dB
|
||||
const [masterVU, setMasterVU] = useState(0); // 0-1
|
||||
const [masterMeterPeak, setMasterMeterPeak] = useState(0);
|
||||
const [rightSidebarWidth, setRightSidebarWidth] = useState(320);
|
||||
const [tcpWidth, setTcpWidth] = useState(320);
|
||||
const [mediaExplorerHeight, setMediaExplorerHeight] = useState(50);
|
||||
@@ -11000,7 +11104,7 @@ const App = () => {
|
||||
source.connect(volumeGainNode);
|
||||
volumeGainNode.connect(pannerNode);
|
||||
pannerNode.connect(fadeGainNode);
|
||||
fadeGainNode.connect(context.destination);
|
||||
fadeGainNode.connect(masterBus ? masterBus.input : context.destination);
|
||||
source.start(context.currentTime, offsetBuffer);
|
||||
activeSourcesRef.current = [source];
|
||||
activeTrackNodesRef.current[st.trackId] = {
|
||||
@@ -11018,7 +11122,7 @@ const App = () => {
|
||||
const osc = ctx.createOscillator();
|
||||
const gainNode = ctx.createGain();
|
||||
osc.connect(gainNode);
|
||||
gainNode.connect(ctx.destination);
|
||||
gainNode.connect(masterBus ? masterBus.input : ctx.destination);
|
||||
|
||||
osc.frequency.setValueAtTime(isDownbeat ? 1000 : 800, time);
|
||||
gainNode.gain.setValueAtTime(0.08, time);
|
||||
@@ -11272,7 +11376,9 @@ const App = () => {
|
||||
gainNode.gain.setValueAtTime(volLinear, context.currentTime);
|
||||
const pannerNode = context.createStereoPanner();
|
||||
pannerNode.pan.setValueAtTime((track.pan ?? 0) / 100, context.currentTime);
|
||||
pannerNode.connect(context.destination);
|
||||
// Route through master bus if available, else direct to destination
|
||||
const dest = masterBus ? masterBus.input : context.destination;
|
||||
pannerNode.connect(dest);
|
||||
let fxStopFn;
|
||||
if (track.fxType === 'chorus') {
|
||||
const fxInput = context.createGain();
|
||||
@@ -12066,7 +12172,7 @@ const App = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const monitorGain = track.monitoringEnabled ? context.destination : null;
|
||||
const monitorGain = track.monitoringEnabled ? (masterBus ? masterBus.input : context.destination) : null;
|
||||
await audioRec.start(monitorGain, track.monitoringEnabled);
|
||||
activeAudioRecordersRef.current[track.id] = audioRec;
|
||||
} catch (err) {
|
||||
@@ -16106,6 +16212,28 @@ const App = () => {
|
||||
}, [showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
|
||||
showMediaExplorer, showFxRack, showMidiEvents, panelPositions,
|
||||
rightSidebarWidth, mediaExplorerHeight, selectedProviderId, currentUser]);
|
||||
|
||||
// Master VU meter animation loop
|
||||
const masterVUAnimRef = useRef(null);
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
if (masterBus && masterBus.analyser) {
|
||||
const data = new Uint8Array(128);
|
||||
masterBus.analyser.getByteTimeDomainData(data);
|
||||
let peak = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const v = Math.abs(data[i] - 128) / 128;
|
||||
if (v > peak) peak = v;
|
||||
}
|
||||
setMasterVU(peak);
|
||||
setMasterMeterPeak(prev => Math.max(prev * 0.97, peak));
|
||||
}
|
||||
masterVUAnimRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
masterVUAnimRef.current = requestAnimationFrame(tick);
|
||||
return () => { if (masterVUAnimRef.current) cancelAnimationFrame(masterVUAnimRef.current); };
|
||||
}, []);
|
||||
|
||||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
||||
className: "h-full w-full flex flex-col bg-[#1e1e1e]"
|
||||
}, /*#__PURE__*/React.createElement("header", {
|
||||
@@ -18804,19 +18932,38 @@ const App = () => {
|
||||
})))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1 flex overflow-x-auto p-1.5 gap-1.5 items-stretch"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex flex-col items-stretch w-14 shrink-0 bg-[#2b2b2b] border border-black/70 overflow-hidden rounded-sm"
|
||||
className: "flex flex-col items-stretch w-16 shrink-0 bg-[#2b2b2b] border border-black/70 overflow-hidden rounded-sm"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
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"
|
||||
}, "MASTER"), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1 flex items-stretch justify-center gap-1 px-1 py-1 min-h-0"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "w-3 bg-[#161616] rounded border border-black/60"
|
||||
className: "w-5 rounded-sm relative overflow-hidden bg-[#0d0d0d] border border-black/60 flex flex-col items-center"
|
||||
}, /*#__PURE__*/React.createElement("input", {
|
||||
type: "range",
|
||||
min: "-60", max: "12", step: "0.5",
|
||||
value: masterVolume,
|
||||
onChange: e => { const v = parseFloat(e.target.value); setMasterVolume(v); if (masterBus) setMasterVolume(v); const linear = v <= -50 ? 0 : Math.pow(10, v / 20); if (masterBus) masterBus.output.gain.setValueAtTime(linear, getAudioContext().currentTime); },
|
||||
className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10",
|
||||
style: { writingMode: 'vertical-lr', direction: 'rtl' }
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1 w-full flex flex-col-reverse 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-2.5 rounded-sm relative overflow-hidden bg-[#0d0d0d] border border-black/60"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "absolute bottom-0 w-full bg-white/40 transition-all duration-75",
|
||||
style: { height: '50%' }
|
||||
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: "flex items-center gap-1 justify-center py-0.5 bg-[#222] border-t border-black/60 shrink-0"
|
||||
}, /*#__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"
|
||||
}, "Mastering")), /*#__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"
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -23,7 +23,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202607290941" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202607292209" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user