FEAT: cài đặt tính năng cho volume main out

This commit is contained in:
2026-07-30 09:34:36 +07:00
parent 66eef405a7
commit e64070a83e
3 changed files with 96 additions and 22 deletions
+18 -16
View File
@@ -53,11 +53,11 @@ function initMasterBus(ctx) {
masterBus.compressor.attack.value = 0.003; masterBus.compressor.attack.value = 0.003;
masterBus.compressor.release.value = 0.25; masterBus.compressor.release.value = 0.25;
masterBus.output.gain.value = 1; masterBus.output.gain.value = 1;
// Default routing: input compressor analyser output destination // MAIN OUT routing per spec: input compressor output (MAIN OUT GAIN) analyser destination
masterBus.input.connect(masterBus.compressor); masterBus.input.connect(masterBus.compressor);
masterBus.compressor.connect(masterBus.analyser); masterBus.compressor.connect(masterBus.output);
masterBus.analyser.connect(masterBus.output); masterBus.output.connect(masterBus.analyser);
masterBus.output.connect(ctx.destination); masterBus.analyser.connect(ctx.destination);
return masterBus; return masterBus;
} }
@@ -81,8 +81,8 @@ function toggleMasteringOnMaster(activate, nodes) {
nodes.eqMid2Filter.connect(nodes.eqHighFilter); nodes.eqMid2Filter.connect(nodes.eqHighFilter);
nodes.eqHighFilter.connect(nodes.maximizerBoostGain); nodes.eqHighFilter.connect(nodes.maximizerBoostGain);
nodes.maximizerBoostGain.connect(nodes.maximizerCompressor); nodes.maximizerBoostGain.connect(nodes.maximizerCompressor);
nodes.maximizerCompressor.connect(masterBus.analyser); nodes.maximizerCompressor.connect(masterBus.output);
masterBus.analyser.connect(masterBus.output); masterBus.output.connect(masterBus.analyser);
masterBus.masteringActive = true; masterBus.masteringActive = true;
masterBus.eqNodes = nodes; masterBus.eqNodes = nodes;
} else if (!activate && masterBus.masteringActive) { } else if (!activate && masterBus.masteringActive) {
@@ -97,10 +97,10 @@ function toggleMasteringOnMaster(activate, nodes) {
try { masterBus.eqNodes.maximizerBoostGain.disconnect(); } catch(e) {} try { masterBus.eqNodes.maximizerBoostGain.disconnect(); } catch(e) {}
try { masterBus.eqNodes.maximizerCompressor.disconnect(); } catch(e) {} try { masterBus.eqNodes.maximizerCompressor.disconnect(); } catch(e) {}
} }
// Restore default routing // Restore default routing: input compressor output analyser destination
masterBus.input.connect(masterBus.compressor); masterBus.input.connect(masterBus.compressor);
masterBus.compressor.connect(masterBus.analyser); masterBus.compressor.connect(masterBus.output);
masterBus.analyser.connect(masterBus.output); masterBus.output.connect(masterBus.analyser);
masterBus.masteringActive = false; masterBus.masteringActive = false;
masterBus.eqNodes = null; masterBus.eqNodes = null;
} }
@@ -18950,18 +18950,18 @@ const App = () => {
})))), /*#__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("div", {
className: "flex flex-col items-stretch w-16 shrink-0 bg-[#2b2b2b] border border-black/70 overflow-hidden rounded-sm" className: "flex flex-col items-stretch w-[72px] shrink-0 bg-[#2b2b2b] border border-black/70 overflow-hidden rounded-sm"
}, /*#__PURE__*/React.createElement("div", { }, /*#__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" 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", { }, "MASTER"), /*#__PURE__*/React.createElement("div", {
className: "flex-1 flex items-stretch justify-center gap-1 px-1 py-1 min-h-0" className: "flex-1 flex items-stretch justify-center gap-1 px-1 py-1 min-h-0"
}, /*#__PURE__*/React.createElement("div", { }, /*#__PURE__*/React.createElement("div", {
className: "w-5 rounded-sm relative overflow-hidden bg-[#0d0d0d] border border-black/60 flex flex-col items-center" className: "w-6 rounded-sm relative overflow-hidden bg-[#0d0d0d] border border-black/60 flex flex-col items-center"
}, /*#__PURE__*/React.createElement("input", { }, /*#__PURE__*/React.createElement("input", {
type: "range", type: "range",
min: "-60", max: "12", step: "0.5", min: "-60", max: "12", step: "0.5",
value: masterVolume, 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); }, onChange: e => { const v = parseFloat(e.target.value); setMasterVolume(v); const linear = v <= -50 ? 0 : Math.pow(10, v / 20); if (masterBus && masterBus.output) masterBus.output.gain.setValueAtTime(linear, getAudioContext().currentTime); },
className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10", className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10",
style: { writingMode: 'vertical-lr', direction: 'rtl' } style: { writingMode: 'vertical-lr', direction: 'rtl' }
}), /*#__PURE__*/React.createElement("div", { }), /*#__PURE__*/React.createElement("div", {
@@ -18971,17 +18971,19 @@ const App = () => {
className: "w-full bg-[#0d0d0d] transition-all duration-75", className: "w-full bg-[#0d0d0d] transition-all duration-75",
style: { height: `${(1 - Math.min(1, Math.max(0, (masterVolume + 60) / 72))) * 100}%` } style: { height: `${(1 - Math.min(1, Math.max(0, (masterVolume + 60) / 72))) * 100}%` }
}))), /*#__PURE__*/React.createElement("div", { }))), /*#__PURE__*/React.createElement("div", {
className: "w-2.5 rounded-sm relative overflow-hidden bg-[#0d0d0d] border border-black/60" className: "w-3 rounded-sm relative overflow-hidden bg-[#0d0d0d] border border-black/60"
}, /*#__PURE__*/React.createElement("div", { }, /*#__PURE__*/React.createElement("div", {
className: "absolute bottom-0 w-full transition-all duration-75", 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' } style: { height: `${Math.min(100, masterVU * 100)}%`, background: masterVU > 0.85 ? '#ef4444' : masterVU > 0.7 ? '#f59e0b' : '#38bdf8' }
}))), /*#__PURE__*/React.createElement("div", { }))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 justify-center py-0.5 bg-[#222] border-t border-black/60 shrink-0" 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("button", { }, /*#__PURE__*/React.createElement("span", {
className: "text-zinc-400"
}, masterVolume > 0 ? '+' : '', masterVolume.toFixed(1), " dB"), /*#__PURE__*/React.createElement("button", {
onClick: () => setShowMasteringModal(true), 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", 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" title: "Open Mastering Suite"
}, "Mastering")), /*#__PURE__*/React.createElement("div", { }, "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" 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", { }, "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"
File diff suppressed because one or more lines are too long
+72
View File
@@ -0,0 +1,72 @@
# IMPACT OF MAIN OUT VOLUME SLIDER IN THE AUDIO PROCESSING PIPELINE
In Web DAW and Mastering Suite audio architectures (powered by the Web Audio API), the MAIN OUT Volume Slider serves as the final amplification control node (**Post-Processing Master Gain Node**).
Below is a detailed breakdown of how the MAIN OUT slider impacts the audio signal processing chain:
---
## 1. Signal Graph Position
The MAIN OUT Volume Slider controls a `GainNode` located at the very end of the processing chain (**Post-FX Stage**), directly preceding hardware output delivery (`AudioContext.destination` / Speakers / Headphones):
```text
[ Input Analyser ]
[ Processing Chain ] (Dynamic EQ ➔ Stereo Imager ➔ Maximizer / Limiter)
[ MAIN OUT Volume Slider (Gain Node) ] ◄── (dB Adjustment Slider)
[ Output Analyser / Peak Meter ]
[ AudioContext.destination ] (Hardware Speakers / DAC)
```
---
## 2. Specific Impacts on the Audio Stream
### 1. Dynamics & Character Unchanged
Because it is placed after the processing modules (Compressor, Dynamic EQ, Maximizer/Limiter), increasing or decreasing the MAIN OUT level **does NOT** alter compression behavior, harmonic saturation, or the ceiling/threshold point of the Brickwall Limiter.
The audio retains $100\%$ of its tonal color, frequency balance, and dynamic range ratio processed by the Master FX chain.
### 2. Linear Amplitude Scaling
The slider converts decibel values ($\text{dB}$) into a linear gain factor $G$ using the standard formula:
$$G = 10^{\frac{\text{dB}}{20}}$$
All PCM Float32 audio samples ($[-1.0, 1.0]$) passing through the node are multiplied directly by this factor $G$:
$$S_{\text{out}}(t) = S_{\text{processed}}(t) \times G$$
### 3. Impact on Hardware Output & Clipping
* **Decreasing the Slider ($< 0\text{ dB}$):** Reduces the waveform amplitude, allowing users to lower the monitoring volume without affecting the underlying Master processing.
* **Increasing the Slider ($> 0\text{ dB}$):** Increases signal amplitude. If the scaled amplitude exceeds $1.0$ ($0\text{ dBFS}$), the digital-to-analog converter (DAC) will suffer from hardware clipping and distortion—even if the preceding Maximizer successfully capped true peaks.
### 4. Impact on Output Metering (Post-Fader Metering)
When set to Post-Fader mode, output Peak/RMS/LUFS meters accurately reflect signal levels after multiplication by the MAIN OUT Gain factor. This ensures operators monitor the actual signal level going to speakers or export files.
### 5. Impact on WAV Bounce / Export
During final mix export (**Master WAV Export**), the MAIN OUT Slider value scales the entire final PCM Audio Buffer. Consequently, this slider dictates the absolute volume level of the rendered output WAV file.
---
## 3. Comparison Matrix: MAXIMIZER GAIN vs. MAIN OUT SLIDER
| Parameter / Criteria | Maximizer Boost Gain | MAIN OUT Volume Slider |
| --- | --- | --- |
| **Signal Chain Location** | Inside FX Chain (Pre-Limiter) | After FX Chain (Post-Limiter) |
| **Dynamics Impact** | Alters LUFS, drives signal into Ceiling, increases compression density. | Does NOT alter dynamics; purely scales master output volume. |
| **Clipping Risk** | **No** (Controlled/limited by the Brickwall Ceiling). | **Yes** (Boosting $> 0\text{ dB}$ can exceed $0\text{ dBFS}$ and clip the DAC). |
| **Primary Purpose** | Increases perceived loudness (LUFS). | Controls monitoring volume for speakers/headphones or final export scaling. |