Files
SonicForgeStudio/mixer_panel_backup_20260729_0941.patch

565 lines
29 KiB
Diff

From 46d3778a5d5305ba625318433b7f98c1e4dac932 Mon Sep 17 00:00:00 2001
From: 3dtours <yeunhiepanh.photo@gmail.com>
Date: Wed, 29 Jul 2026 09:11:54 +0700
Subject: [PATCH] =?UTF-8?q?FIX:=20chi=E1=BB=81u=20cao=20c=E1=BB=A7a=20trac?=
=?UTF-8?q?k=20thu=20nh=E1=BB=8F=20theo=20chi=E1=BB=81u=20cao=20c=E1=BB=A7?=
=?UTF-8?q?a=20Mixer=20Panel?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.kilo/plans/mixer-equalizer-plan.md | 293 ++++++++++++++++++
.../js/services/audioMixerGraphManager.js | 20 +-
app/storage/sonicforge.db | Bin 77824 -> 77824 bytes
app/templates/index.html | 2 +-
4 files changed, 310 insertions(+), 5 deletions(-)
create mode 100644 .kilo/plans/mixer-equalizer-plan.md
diff --git a/.kilo/plans/mixer-equalizer-plan.md b/.kilo/plans/mixer-equalizer-plan.md
new file mode 100644
index 0000000..2fbf79e
--- /dev/null
+++ b/.kilo/plans/mixer-equalizer-plan.md
@@ -0,0 +1,293 @@
+# Mixer Console & Equalizer Implementation Plan
+
+## Goal
+Implement a docked Mixer Console panel (similar to REAPER's MCP) at the bottom of the DAW, featuring per-track channel strips with volume fader, pan knob, Mute/Solo, VU meter, and FX controls. Support MAIN SESSION and SECTION-TAB contexts with hierarchical audio routing.
+
+---
+
+## Task 1: Create `AudioMixerGraphManager`
+
+**File:** `app/static/js/services/audioMixerGraphManager.js` (new)
+
+Class managing all Web Audio nodes with a proper Master Bus signal chain:
+
+```
+source → trackGain → faderGain → panNode → analyserNode → masterGain → masterPan → masterAnalyser → destination
+```
+
+### Responsibilities
+
+- **Singleton pattern** — created once with `audioCtx`, stored on `window.AudioMixerGraphManager`.
+- **Master Bus** — `masterGain → masterPan → masterAnalyser → audioCtx.destination`.
+- **Track Node Map** — `Map<trackId, { inputGain, faderGain, panNode, analyserNode, track }>`.
+- **Section Bus Map** — `Map<sectionId, { subMixGain, subMixPan, hostTrackId }>`.
+- `createMainTrackNodes(track)` — creates node chain, connects analyser → masterGain. Stores in `trackNodesMap`. Calls `updateTrackVolumePan()`.
+- `createSectionSubMixBus(sectionId, hostTrackId)` — creates subMixGain→subMixPan, pipes to host track's `inputGain`. Falls back to masterGain if host track not found.
+- `createSectionTrackNodes(sectionId, secTrack)` — creates nodes for internal section tracks, connects analyser → section's `subMixGain`.
+- `updateTrackVolumePan(trackId, volumeDb, panValue, isMuted)` — updates gain/pan in real-time. `gain = isMuted ? 0 : Math.pow(10, volumeDb / 20.0)`.
+- `getAnalyserData(trackId)` — returns `analyserNode` frequency/time-domain data for VU meter rendering.
+- `removeTrackNodes(trackId)` — disconnects and removes from map.
+
+### Signal Equations
+- `linearGain = isMuted ? 0 : Math.pow(10, volumeDb / 20.0)` — dB to linear scale.
+- `panValue = track.pan / 100` — existing -100..+100 range maps to -1.0..+1.0 `StereoPanner.pan`.
+- Use `setTargetAtTime(linearGain, ctx.currentTime, 0.01)` for smooth transitions.
+
+---
+
+## Task 2: Refactor `app.jsx` Audio Node Creation
+
+**File:** `app/static/js/app.jsx` — modify existing audio routing functions.
+
+### 2a. Replace `getOrCreateTrackNode()` with `AudioMixerGraphManager`
+
+Currently lines 10314-10343 create a local `{ gainNode, pannerNode }` per track and connect `pannerNode → context.destination`. Replace this with:
+
+```js
+function getOrCreateTrackNode(track, context) {
+ if (!window.AudioMixerGraphManager) return null;
+ const mgr = window.AudioMixerGraphManager;
+ const bundle = mgr.trackNodesMap.get(track.id);
+ if (bundle) return bundle.faderGain;
+ // Create nodes via AudioMixerGraphManager
+ mgr.createMainTrackNodes(track);
+ return mgr.trackNodesMap.get(track.id).faderGain;
+}
+```
+
+### 2b. Section Sub-Track Audio Routing
+
+Replace `getOrCreateSubTrackNode()` (lines 10344-10359) to:
+1. Create section sub-mix bus via `AudioMixerGraphManager.createSectionSubMixBus()` (first time only, lazy init).
+2. Create section internal track nodes via `AudioMixerGraphManager.createSectionTrackNodes()`.
+3. Return `faderGain` node for audio connection.
+
+### 2c. SoundFont MIDI Audio Routing Fix
+
+**Critical:** FluidSynth renders directly to `audioCtx.destination` via ScriptProcessorNode. SoundFont player (`soundfontPlayer.js` lines 27-49) has its own `_gainNode` connected to destination. This bypasses the track's gain/panner nodes.
+
+**Fix in `soundfontPlayer.js`:**
+- Modify the first `_gainNode` creation so that instead of `_gainNode.connect(ctx.destination)`, it connects to a pass-through that can be dynamically routed to any destination node.
+- OR: In `startTrackPlayback()` (app.jsx ~line 10382), after calling `window.SonicSF.playNote(...)`, re-route the SF output by calling a new method `window.SonicSF.setOutputNode(destinationNode)` that disconnects the internal `_gainNode` from `destination` and reconnects it to the track's `faderGain`.
+
+**Simpler approach:** Add `setOutputNode(destinationNode)` to `soundfontPlayer.js`:
+```js
+function setOutputNode(newDest) {
+ _gainNode.disconnect();
+ _gainNode.connect(newDest || ctx.destination);
+}
+```
+
+Call this from `getOrCreateTrackNode()` after creating/retrieving the track's `faderGain`, passing `trackFaderGain` as the destination.
+
+---
+
+## Task 3: Mixer Console State & Toggle
+
+**File:** `app/static/js/app.jsx`
+
+### 3a. Add Mixer Visibility State
+
+```js
+const [showMixer, setShowMixer] = useState(false);
+```
+
+### 3b. Mixer Toggle Button
+
+Add a toolbar button (e.g., in the TCP header near the "Add Track" button, or in a bottom toolbar). Icon: "mixer" (sliders icon). Toggles `showMixer`.
+
+### 3c. Determine Active Context
+
+When `activeTab === 'main'`, mixer shows MAIN SESSION tracks. When a SECTION-TAB is active, mixer shows the section's internal tracks (from `sessionTabs.find(s => s.id === activeTab).tracks`).
+
+### 3d. Pass to MixerConsole
+
+```
+mixerContext = { isSectionTab, currentTracks, currentTitle, activeTabContext }
+```
+
+---
+
+## Task 4: Create MixerConsole React Component
+
+**File:** `app/static/js/components/MixerConsole.jsx` (new)
+
+Docked at the bottom of the timeline (inside the `bottom` dock panel or a fixed container at bottom of the `flex-1 flex flex-col overflow-hidden` area, below the timeline canvas).
+
+### 4a. Component Structure
+
+```
+MixerConsole (flex-col h-48 border-t border-zinc-700 bg-slate-900)
+├── MixerHeader (flex, shows "MAIN SESSION" or "SECTION TAB" label + close button)
+└── MixerStripsWrapper (flex-1 flex overflow-x-auto p-2 gap-1.5)
+ ├── MasterChannelStrip (w-28, fixed, left-aligned)
+ ├── Divider (w-[1px] bg-zinc-700)
+ └── Track strips (horizontal scroll, each TrackChannelStrip w-24)
+```
+
+### 4b. MasterChannelStrip
+
+Props: `isSectionBus, mixerGraphMgr`
+
+- Shows "MASTER" or "SEC BUS" header.
+- VU Meter canvas element (reads `masterAnalyser` frequency data, draws RMS/Peak bar).
+- Peak/rms numeric readout.
+- Mono toggle button.
+- "MAIN OUT" / "SUB-MIX" footer.
+
+### 4c. TrackChannelStrip
+
+Props: `track, index, mixerGraphMgr`
+
+Local state: `volumeDb, pan, isMuted, isSoloed` — initialized from `track` props.
+
+Renders:
+1. **Pan knob** — `<input type="range" min="-1" max="1" step="0.05">` vertical/horizontal slider + numeric label (C / Lxx / Rxx).
+2. **dB Readout** — `volumeDb <= -60 ? "-inf" : "[+/-]N.NdB"`.
+3. **Mute / Solo buttons** — `[M]` orange when muted, `[S]` yellow when soloed. Mute dispatches `updateTrackVolumePan(trackId, vol, pan, newMute)`. Solo dispatches `toggleTrackSoloEvaluate(trackId)`.
+4. **Fader + VU Meter** — vertical fader `<input type="range" min="-60" max="+12" step="0.5">` with CSS `writing-mode: vertical-lr`. VU meter bar (vertical `div` with gradient, height calculated from analyser RMS data). RequestAnimationFrame loop reads `analyserNode.getByteTimeDomainData()` and updates VU height.
+5. **FX / Power buttons** — `[FX]` stub that shows toast "FX panel coming soon". `[⑈]` bypass toggle (stub).
+6. **Footer** — `{index}. {track.name}` + `{track.type}`.
+
+### 4e. VU Meter Animation
+
+Use `requestAnimationFrame` loop inside `TrackChannelStrip`:
+```js
+useEffect(() => {
+ const analyser = mixerGraphMgr?.trackNodesMap.get(track.id)?.analyserNode;
+ if (!analyser) return;
+ analyser.fftSize = 128;
+ const bufferLength = analyser.frequencyBinCount;
+ const dataArray = new Uint8Array(bufferLength);
+ let rafId;
+ const draw = () => {
+ analyser.getByteTimeDomainData(dataArray);
+ const rms = Math.sqrt(dataArray.reduce((sum, v) => sum + ((v - 128) / 128) ** 2, 0) / bufferLength);
+ setMeterLevel(Math.min(1, rms * 2));
+ rafId = requestAnimationFrame(draw);
+ };
+ rafId = requestAnimationFrame(draw);
+ return () => cancelAnimationFrame(rafId);
+}, [track.id, mixerGraphMgr]);
+```
+
+---
+
+## Task 5: Add Mixer Toggle Button (TCP Header)
+
+**File:** `app/static/js/app.jsx` — TCP header area (line 16648-16656).
+
+Add a mixer toggle button next to the "Add Track" button:
+
+```jsx
+/*#__PURE__*/React.createElement("button", {
+ onClick: () => setShowMixer(!showMixer),
+ className: "px-2 py-1 bg-indigo-700 hover:bg-indigo-600 text-white rounded text-xs font-semibold flex items-center gap-1 shadow transition"
+}, /*#__PURE__*/React.createElement("span", {
+ className: "inline-flex items-center shrink-0"
+}, /*#__PURE__*/React.createElement("i", {
+ "data-lucide": "sliders-horizontal",
+ className: "w-3.5 h-3.5"
+})), " Mixer")
+```
+
+## Task 6: Add F7 Keyboard Shortcut for Mixer Toggle
+
+**File:** `app/static/js/app.jsx` — modify the global keyboard handler (lines 8037-8335).
+
+The keydown handler at line 8334 (`window.addEventListener('keydown', handler, { capture: true })`) already handles hotkeys like Space (play/pause) and function keys for sub-tab operations.
+
+Add an `F7` check inside the handler, **before** the sub-tab-only block at line 8054 (so it works in both MAIN SESSION and sub-tabs):
+
+```js
+// F7: Toggle Mixer Console
+if (e.key === 'F7') {
+ e.preventDefault();
+ setShowMixerRef.current(prev => !prev);
+ return;
+}
+```
+
+Use a ref (`showMixerRef`) to access the current `showMixer` state without needing it as a dependency of the effect. Create the ref and keep it synced in the component body:
+
+```js
+const showMixerRef = useRef(showMixer);
+showMixerRef.current = showMixer;
+```
+
+Keep `setShowMixerRef` as a ref to `setShowMixer` to call it from the event listener without stale closures.
+
+This avoids adding `showMixer` as a dependency to the keyboard effect, which would cause unnecessary listener re-registrations.
+
+---
+
+## Task 7: Integrate MixerConsole into App Layout
+
+**File:** `app/static/js/app.jsx`
+
+### 5a. Placement
+
+After the timeline track list (the `div` containing `activeTracks.map(...)` and the add-track row), and before the transport bar, insert:
+
+```jsx
+showMixer && React.createElement(MixerConsole, {
+ key: 'mixer-console',
+ mixerContext: { isSectionTab, currentTracks, currentTitle, activeTabContext },
+ mixerGraphMgr: window.AudioMixerGraphManager,
+ trackActions: { updateTrackVolumeDb, updateTrackPan, toggleTrackMute, toggleTrackSoloEvaluate }
+})
+```
+
+### 5b. Panel System Integration
+
+Alternatively, register "mixer" as a panel that can be docked to the `bottom` dock panel. The panel content renders `MixerConsole`. This reuses the existing `dockPanels` / `renderDock` system — add a menu item or button that calls `addToDock('bottom', 'mixer')`.
+
+Simpler approach: fixed position below the timeline with a toggle button.
+
+---
+
+## Task 8: Section-Tab Context Switching
+
+**File:** `app/static/js/app.jsx`
+
+When `activeTab` changes to a SECTION-TAB (`sessionTabs.some(s => s.id === activeTab)`):
+- MixerConsole's `currentTracks` switches to the section's internal tracks.
+- The Master strip label changes from "MASTER" to "SEC BUS".
+- Header shows "MIXER: SECTION TAB ({sectionName})".
+- Audio routing uses `createSectionSubMixBus` + `createSectionTrackNodes`.
+
+When switching back to `activeTab === 'main'`:
+- MixerConsole reverts to MAIN SESSION tracks and Master strip.
+
+This logic lives inside a `useEffect` that watches `activeTab` and re-renders the MixerConsole with the correct context.
+
+---
+
+## Task 9: Mixer Resize & Toggle Persistence
+
+- Add a top drag handle on the MixerConsole (`h-1 cursor-ns-resize hover:bg-cyan-500/50`) to adjust height (range: `h-32` to `h-80`). Store height in a ref.
+- Store `showMixer` and mixer height in `localStorage` for persistence.
+- Close button (`[X]`) on the Mixer header sets `showMixer = false`.
+
+---
+
+## Validation Plan (Additions for F7 + Toggle Button)
+
+1. [ ] **Button toggle** — Click "Mixer" button in TCP header → MixerConsole appears. Click again → hides.
+2. [ ] **F7 hotkey** — Press F7 → MixerConsole toggles. Works in both MAIN SESSION and SECTION-TAB. Does NOT interfere with input/textarea focus.
+3. [ ] **No stale closure** — Repeated F7 presses reliably toggle state (setShowMixerRef.current used, not a stale function).
+
+## Extended Validation Plan
+
+1. **Toggle mixer** — Click the Mixer button → MixerConsole appears at bottom. Click again → hides.
+2. **MAIN SESSION mode** — Shows all tracks in the session. Master strip on left.
+3. **Volume fader** — Drag fader → track volume changes audibly in real-time. Numeric dB readout updates.
+4. **Pan knob** — Drag pan → stereo position changes. Label shows C / Lxx / Rxx.
+5. **Mute** — Click [M] → track mutes, button turns orange. Click again → unmutes.
+6. **Solo** — Click [S] on track 1 → track 1 plays, all other tracks silence. Click again → all tracks resume.
+7. **VU Meter** — During playback, meter bars animate with signal level (green/yellow/red gradient).
+8. **SECTION-TAB** — Open a section → MixerConsole switches to section tracks, Master strip shows "SEC BUS".
+9. **Section sub-mix volume** — Adjust fader on the SECTION host track → entire section volume changes.
+10. **MIDI audio routing** — SoundFont MIDI notes play through the track's fader/panner, not bypassing.
+11. **Resize** — Drag the resize handle on top of MixerConsole → height changes.
+12. **Persistence** — Reload page → mixer state restored from localStorage.
diff --git a/app/static/js/services/audioMixerGraphManager.js b/app/static/js/services/audioMixerGraphManager.js
index 93c530b..60c39c2 100644
--- a/app/static/js/services/audioMixerGraphManager.js
+++ b/app/static/js/services/audioMixerGraphManager.js
@@ -4,10 +4,22 @@
function AudioMixerGraphManager(audioCtx) {
this.audioCtx = audioCtx;
this.trackNodesMap = new Map();
- this.sectionBusMap = new Map();
-
- // Master bus chain: masterGain -> masterPan -> masterAnalyser -> destination
- this.masterGain = audioCtx.createGain();
+ this.sectionBusMap = new Map();
+ this.trackFxBypassGains = new Map();
+
+ // Master bus chain: masterGain -> masterPan -> masterBypassGain -> masterAnalyser -> destination
+ this.masterGain = audioCtx.createGain();
+ this.masterGain.gain.setValueAtTime(1, audioCtx.currentTime);
+ this.masterPan = audioCtx.createStereoPanner();
+ this.masterPan.pan.setValueAtTime(0, audioCtx.currentTime);
+ this.masterBypassGain = audioCtx.createGain();
+ this.masterBypassGain.gain.setValueAtTime(1, audioCtx.currentTime);
+ this.masterAnalyser = audioCtx.createAnalyser();
+ this.masterAnalyser.fftSize = 128;
+ this.masterGain.connect(this.masterPan);
+ this.masterPan.connect(this.masterBypassGain);
+ this.masterBypassGain.connect(this.masterAnalyser);
+ this.masterAnalyser.connect(audioCtx.destination);
this.masterGain.gain.setValueAtTime(1, audioCtx.currentTime);
this.masterPan = audioCtx.createStereoPanner();
this.masterPan.pan.setValueAtTime(0, audioCtx.currentTime);
diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db
index b1d2ec94d1a0c8be003360e1202979abb9d62c6c..0ffa5a826df5e7beabb1bee853e683414f554125 100644
GIT binary patch
delta 1068
zcmV+{1k?L~-~@o+1dtmIp8x;=6951J5dZ)H29YQ?0iUs85FY^vlW`wHKsjPDIWspl
zH)J(pH#0ObH8f*2V_{=rW@b1zGG;b2HaIb6GB{#mH8eM3FfleWFl1(AWH2#dGdX5v
zHDfX{FthU?5nu`f50U^6k`L|=z_Sq`gb$Ozj|n9-F*!6aI5;^iF*rG8Ei`0iG%YwW
zV=^sdVlZQ2VKp=~VK8L_v%rr;FtY{V{0|Hb3zPr?2gCpjEeFK0fw%`*hfM<sUsXSi
zH3<R-bY*RDUo<f}G%z?gIV~|bIb|(0WM(uiI5A@~Eo5RaV_{)6G&Er_Wi&B4G%z?g
zIV~|bIb|(0WM(uiI5A@~Eo5RaV_{)6G&Er_Ws_^sC?j-bZE#<3a&Ky7V{{@cB5h@K
zVPs)+VIn$vB6MkVY-J)kB9m*;8I#h{<`-0QVPk6`Ffk%5B6N9hWg<EvK~+RaPm{UF
zLN#u6Y-}tdX>N0La&>KGZggL8a&KpHVQnHhZgp&IEFx)cb98cbZDnqBUv6P-Wg<Fm
zb!=>tK*$v?Zgp&IEFyDnb#7#4Z*Fv7X=EZgZgp&IEFyDnb#7#4Z*Fv7VqtD;B06q$
zY;2Pd)D=2zb!==bB58DGZF3?zTU~uDdm?FMB03^6A}k_qVQpn1IwDkZVPk6`Fft-6
zB6N9hWg<EvK~+RaPm{UFLN#u6Y-}tdX>N0La&>KGZggL8a&KpHVQnHhZgp&IEFx)c
zb98cbZDnqBUv6P-Wg<Fmb!=>tK*$v?Zgp&IEFyDnb#7#4Z*Fv7X=EZgZgp&IEFyDn
zb#7#4Z*Fv7VqtD;B06q$Y;2Pd)D;eHb!==bB58DGZF3?zTa(bwCJqBsLE4&AiE|l4
z0001)k%5&7hfo3uUsXSiJF^?nodFkAa$#d@ATTi^EFyGyaAhJoB0*I|Nl%lx$3iu3
zb!==bB57`ObaHiVWo~p|aB^>Fa$#*EI&O7rY%C&aZgX^Ub!}yCbYE^^ZDk@lZgp&I
zlR(H7E^c*fY%C&kZ*^{DW^ZnEUuk3_I&O7rY%C&kZ*^{DW^ZnEUt(cyYa%*sb!=>t
z5Y!bqZgp&IEFx)iWo>gJI$K?REPEnpWFk5uG9oM@ZeeX@B03^ea$#d@ATTl_EFyGy
zaAhJoB0*I|Nl%lx$3iu3b!==bB57`ObaHiVWo~p|aB^>Fa$#*EI&O7rY%C&aZgX^U
zb!}yCbYE^^ZDk@lZgp&IlR(H7E^c*fY%C&kZ*^{DW^ZnEUuk3_I&O7rY%C&kZ*^{D
zW^ZnEUt(cyYa%*sb!=>t5Y!b8Zgp&IEFx)iWo>gJI$M*_&n5~3S3%mESWSxm%(DUD
m1|Svy1poj55C8xG6951J5&!@I4*&oF4YL8@1|YK_aNZB((q(7>
delta 7203
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
diff --git a/app/templates/index.html b/app/templates/index.html
index 1207348..5fa01fe 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -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/audioMixerGraphManager.js?v=202607282218"></script>
- <script src="/static/js/app.precompiled.js?v=202607282359" defer></script>
+ <script src="/static/js/app.precompiled.js?v=20260729085936" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
--
2.47.3
diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx
index c53adde..8189c12 100644
--- a/app/static/js/app.jsx
+++ b/app/static/js/app.jsx
@@ -6025,7 +6025,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
keybedMouseDownRef.current = true;
try {
if (window.SonicSF) {
- window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null, kbCh, kbSynth);
+ window.SonicSF.playNote(pitch, 100, 60000, undefined, st.instrumentProgram, null, kbCh, kbSynth);
}
} catch (err) {
console.error('playNote error:', err);
@@ -6035,12 +6035,17 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (keybedMouseDownRef.current) {
try {
if (window.SonicSF) {
- window.SonicSF.playNote(pitch, 100, 200, undefined, st.instrumentProgram, null, kbCh, kbSynth);
+ window.SonicSF.playNote(pitch, 100, 60000, undefined, st.instrumentProgram, null, kbCh, kbSynth);
}
} catch (err) { console.error('playNote error:', err); }
}
},
- onMouseUp: () => { keybedMouseDownRef.current = false; }
+ onMouseUp: () => {
+ keybedMouseDownRef.current = false;
+ if (window.SonicSF && window.SonicSF.stopNote) {
+ window.SonicSF.stopNote(kbCh, pitch);
+ }
+ }
}, showLabel && label)
);
}