diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index 91acb67..ce2fe1e 100644 Binary files a/app/storage/sonicforge.db and b/app/storage/sonicforge.db differ diff --git a/md/44_MIXER_EQUALIZER.md b/md/44_MIXER_EQUALIZER.md new file mode 100644 index 0000000..58faeda --- /dev/null +++ b/md/44_MIXER_EQUALIZER.md @@ -0,0 +1,532 @@ +# TECHNICAL SPECIFICATION & SYSTEM ARCHITECTURE FOR MAIN SESSION AND SECTION-TAB MIXER + +This document details the implementation of a docked Mixer Console (Mixer Control Panel - MCP) positioned at the bottom of the screen (similar to REAPER), alongside the hierarchical audio signal routing architecture when SECTION-TABs are nested within MAIN SESSION tracks. + +--- + +## 1. HIERARCHICAL AUDIO SIGNAL ROUTING ARCHITECTURE + +To allow a SECTION-TAB to maintain an independent mix while assigning final master control of its output to a single track in the MAIN SESSION, the system employs a **Sub-Mix Bus Piping** model via the Web Audio API. + +### Audio Signal Graph Tree + +```text +[ SECTION-TAB (Sub-Session) ] + ├── Track S1 (Audio/MIDI) ──► Gain ──► Pan ──┐ + ├── Track S2 (Audio/MIDI) ──► Gain ──► Pan ──┼──► [ SECTION SUB-MIX BUS ] + └── Track S3 (Audio/MIDI) ──► Gain ──► Pan ──┘ │ (Section Master Out) + │ + ▼ (Piped to host track) +[ MAIN SESSION (Root Session) ] │ + ├── Track 1 (Normal Audio) ──► Gain ──► Pan ─────────────┼──┐ + ├── Track 2 (SECTION ITEM Track) ◄───────────────────────┘ │ + │ └─ Gain Node ──► FX Chain ──► Pan Node ───────────────┼──► [ MAIN MASTER BUS ] + └── Track 3 (Normal MIDI) ──► Gain ──► Pan ─────────────┘ │ + ▼ + [ AudioContext Destination ] + (Speakers / Soundcard) + +``` + +### Audio Routing Principles: + +* **Inside SECTION-TAB:** Internal tracks ($S1, S2, S3$) are balanced via volume, pan, and Mute/Solo controls on the SECTION-TAB's dedicated mixer. Output signals from these tracks are summed at the `Section Sub-Mix Bus Node`. +* **Routing to MAIN SESSION:** The output of the `Section Sub-Mix Bus Node` does not connect directly to `audioCtx.destination`. Instead, it routes into the Input Node of Track 2 on the MAIN SESSION (the track hosting the `SECTION_ITEM`). +* **MAIN SESSION Management:** +* The Volume Fader on Track 2 of the MAIN SESSION controls the overall volume of the entire SECTION-TAB. +* Mute, Solo, and FX Chain controls on Track 2 of the MAIN SESSION directly process all audio originating from that SECTION-TAB before it reaches the Main Master Bus. + + + +--- + +## 2. DOCKED MIXER CONSOLE (MCP) UI STRUCTURE + +The Mixer Console is designed as a docked panel underneath the Timeline, split into two main sections: **Master Channel Strip** (Left) and **Track Channel Strips** (Right). + +```text ++------------------------------------------------------------------------------------------------+ +| DOCKED MIXER CONSOLE | ++-------------------+----------------------------------------------------------------------------+ +| MASTER STRIP | TRACK CHANNEL STRIPS (Track 1, Track 2 [SECTION], Track 3...) | +| | | +| (Knob) Pan | (Knob) Pan (Knob) Pan (Knob) Pan (Knob) Pan | +| [MONO] | -inf dB -inf dB -inf dB -inf dB | +| [M] [S] | [M] [S] [M] [S] [M] [S] [M] [S] | +| [ROUTE] | [|||] [|||] Peak [|||] [|||] Peak [|||] [|||] Peak [|||] [|||] Peak| +| [FX] [Power] | [===] Fader [===] Fader [===] Fader [===] Fader | +| [TRIM] | [FX] [Power] [FX] [Power] [FX] [Power] [FX] [Power] | +| [FADER] (VU) | [🔴] [ROUTE] [🔴] [ROUTE] [🔴] [ROUTE] [🔴] [ROUTE] | +| | ------------------ ------------------ ------------------ ----------------| +| RMS/Peak Meter | Track 1 (Kick) Track 2 (SECTION) Track 3 (Guitars) Track 4 (Lead) | +| MASTER | [🎵 SF2 Synth] [📦 Sec: Chorus] [🎸 Pedalboard] [🎹 Vital] | ++-------------------+----------------------------------------------------------------------------+ + +``` + +### Channel Strip Components: + +* **Top Pan Knob:** Rotatable control for Left/Right stereo balance (Pan Left/Right, $-1.0$ to $+1.0$). +* **Numeric dB Readout:** Displays current volume level in numeric dB (e.g., $0.0\text{ dB}$, $-6.5\text{ dB}$, $-inf$). +* **Mute [M] & Solo [S] Buttons:** +* Orange **[M]** button: Mutes track output. +* Yellow **[S]** button: Solos track output (mutes all non-soloed tracks). + + +* **Vertical Level Meter (VU Peak Meter):** Real-time LED signal amplitude meter (Green $\rightarrow$ Yellow $\rightarrow$ Red clipping indicator above $0\text{ dBFS}$). +* **Volume Fader Slider:** Vertical linear slider for gain control. +* **FX & Power Toggle:** +* **[FX]** button: Opens the Effects Plugin management window (Reverb, Delay, EQ, Compressor). +* **[Power]** button: Toggles bypass state for the track's entire FX chain. + + +* **Record Arm [🔴] & Routing/Trim Buttons:** +* Red circle **[🔴]**: Arms track for recording. +* **[ROUTE]** button: Opens the Input/Output Matrix configuration panel. + + +* **Track Identifier Footer:** Displays track index ($1, 2, 3...$) and track name (e.g., *Track 2 - Chorus Section*). + +--- + +## 3. STATE STORE SCHEMAS + +### 3.1 Main Session State (`main_session_mixer_state`) + +```json +{ + "session_id": "main_session_root", + "master_bus": { + "volume_db": 0.0, + "pan": 0.0, + "mute": false, + "is_mono": false, + "fx_chain": [] + }, + "tracks": [ + { + "id": "track_01", + "name": "Track 1 - Drums", + "type": "AUDIO", + "volume_db": -2.5, + "pan": 0.0, + "mute": false, + "solo": false, + "is_armed": false + }, + { + "id": "track_02_section_host", + "name": "Track 2 - Chorus Section", + "type": "SECTION", + "volume_db": 0.0, + "pan": 0.0, + "mute": false, + "solo": false, + "referenced_section_id": "section_chorus_001" + } + ] +} + +``` + +### 3.2 Section-Tab State (`section_store`) + +```json +{ + "section_store": { + "section_chorus_001": { + "id": "section_chorus_001", + "name": "Chorus Section Tab", + "is_root": false, + "parent_track_id": "track_02_section_host", + "sub_mix_bus": { + "volume_db": 0.0, + "pan": 0.0, + "mute": false, + "fx_chain": [] + }, + "tracks": [ + { + "id": "sec_track_1", + "name": "Sec Guitar 1", + "volume_db": -1.0, + "pan": -0.5, + "mute": false, + "solo": false + }, + { + "id": "sec_track_2", + "name": "Sec Vocal Lead", + "volume_db": +1.5, + "pan": 0.0, + "mute": false, + "solo": false + } + ] + } + } +} + +``` + +--- + +## 4. AUDIO GRAPH NODE ENGINE (WEB AUDIO API) + +To execute audio summing according to the architecture above, the application uses a manager class to maintain Web Audio nodes (`AudioNode Graph`): + +```javascript +// app/static/js/services/audioMixerGraphManager.js + +export class AudioMixerGraphManager { + constructor(audioCtx) { + this.audioCtx = audioCtx; + + // Main Session Master Nodes + this.mainMasterGain = this.audioCtx.createGain(); + this.mainMasterPan = this.audioCtx.createStereoPanner(); + this.mainMasterAnalyser = this.audioCtx.createAnalyser(); + + // Connect Master Graph -> Speakers + this.mainMasterGain + .connect(this.mainMasterPan) + .connect(this.mainMasterAnalyser) + .connect(this.audioCtx.destination); + + // Map storing track AudioNodes + this.trackNodesMap = new Map(); // trackId -> { inputGain, faderGain, panNode, analyserNode } + this.sectionBusesMap = new Map(); // sectionId -> { subMixGain, subMixPan, outputNode } + } + + /** + * Initializes Audio Nodes for a track on the Main Session + */ + createMainTrackNodes(track) { + const inputGain = this.audioCtx.createGain(); + const faderGain = this.audioCtx.createGain(); + const panNode = this.audioCtx.createStereoPanner(); + const analyserNode = this.audioCtx.createAnalyser(); + analyserNode.fftSize = 64; + + // Track internal signal chain connection: + // InputGain -> FaderGain -> PanNode -> Analyser -> Main Master Gain + inputGain + .connect(faderGain) + .connect(panNode) + .connect(analyserNode) + .connect(this.mainMasterGain); + + const nodeBundle = { inputGain, faderGain, panNode, analyserNode, track }; + this.trackNodesMap.set(track.id, nodeBundle); + + // Update initial Gain/Pan values + this.updateTrackVolumePan(track.id, track.volume_db, track.pan, track.mute); + return nodeBundle; + } + + /** + * Initializes a Sub-Mix Bus for a SECTION-TAB and PIPES it to a Main Session track + * @param {string} sectionId - Section ID + * @param {string} hostTrackId - Main Session Track ID hosting this section + */ + createSectionSubMixBus(sectionId, hostTrackId) { + const subMixGain = this.audioCtx.createGain(); + const subMixPan = this.audioCtx.createStereoPanner(); + + // Section Sub-Mix Bus internal connection + subMixGain.connect(subMixPan); + + // CRITICAL STEP: Locate host track on Main Session to pipe signal + const hostTrackNodes = this.trackNodesMap.get(hostTrackId); + + if (hostTrackNodes) { + // Pipe Output of Section Bus directly into InputGain of Main Track + subMixPan.connect(hostTrackNodes.inputGain); + console.log(`[MixerRouter] Sub-Mix Bus of Section "${sectionId}" successfully PIPED into Main Track "${hostTrackId}"`); + } else { + console.warn(`[MixerRouter] Host Track "${hostTrackId}" not found. Section bus falling back to Master.`); + subMixPan.connect(this.mainMasterGain); + } + + const sectionBundle = { subMixGain, subMixPan, hostTrackId }; + this.sectionBusesMap.set(sectionId, sectionBundle); + return sectionBundle; + } + + /** + * Initializes an INTERNAL track inside a SECTION-TAB + */ + createSectionTrackNodes(sectionId, secTrack) { + const sectionBundle = this.sectionBusesMap.get(sectionId); + if (!sectionBundle) { + throw new Error(`Section Sub-Mix Bus for "${sectionId}" has not been initialized!`); + } + + const inputGain = this.audioCtx.createGain(); + const faderGain = this.audioCtx.createGain(); + const panNode = this.audioCtx.createStereoPanner(); + const analyserNode = this.audioCtx.createAnalyser(); + + // Connect Track in Section to the Section's Sub-Mix Bus + inputGain + .connect(faderGain) + .connect(panNode) + .connect(analyserNode) + .connect(sectionBundle.subMixGain); // Direct to Section Sub-Mix Bus! + + const nodeBundle = { inputGain, faderGain, panNode, analyserNode, secTrack }; + this.trackNodesMap.set(secTrack.id, nodeBundle); + + this.updateTrackVolumePan(secTrack.id, secTrack.volume_db, secTrack.pan, secTrack.mute); + return nodeBundle; + } + + /** + * Updates Volume (dB) & Pan using standard audio formulas + */ + updateTrackVolumePan(trackId, volumeDb, panValue, isMuted) { + const bundle = this.trackNodesMap.get(trackId); + if (!bundle) return; + + // Convert dB to Linear Gain Factor: Gain = 10 ^ (dB / 20) + const linearGain = isMuted ? 0.0 : Math.pow(10, volumeDb / 20.0); + + bundle.faderGain.gain.setTargetAtTime(linearGain, this.audioCtx.currentTime, 0.01); + bundle.panNode.pan.setTargetAtTime(panValue, this.audioCtx.currentTime, 0.01); + } +} + +``` + +--- + +## 5. NESTED MUTE/SOLO LOGIC MATRIX + +When muting or soloing a track on the Main Session or within a Section Tab, the system applies hierarchical rules: + +| User Action | Main Session Impact | Internal Section Tracks Impact | +| --- | --- | --- | +| **Mute Track 2 (Section Host)** on Main Session | Track 2 is muted. Other tracks play normally. | Entire Section is muted (signal is blocked at Track 2's Fader Gain). | +| **Mute Section Track 1** inside Section Tab | No impact on other Main Tracks. | Only Section Track 1 is silenced. Other Section Tracks ($S2, S3$) continue routing to Track 2. | +| **Solo Section Track 1** inside Section Tab | No impact on Main Session. | Mutes $S2, S3$ within the Section. Only $S1$ outputs to Track 2 of the Main Session. | +| **Solo Track 2** on Main Session | Mutes Track 1 and Track 3 on Main Session. | The Section audio stream plays normally to the Main Master Bus. | + +--- + +## 6. STATE-DRIVEN REACT/CANVAS COMPONENT + +Below is the React implementation for the Mixer Console supporting dynamic switching between MAIN SESSION and SECTION-TAB mixers: + +```jsx +// app/static/js/components/MixerConsole.jsx +import React, { useState, useEffect } from 'react'; + +export function MixerConsole({ sessionState, activeTabContext, mixerGraphMgr }) { + // Determine if Mixer is rendering for MAIN SESSION or SECTION-TAB + const isSectionTab = activeTabContext?.type === 'SECTION_TAB'; + + // Retrieve target tracks list for Mixer Console display + const currentTracks = isSectionTab + ? sessionState.section_store[activeTabContext.referenced_section_id]?.tracks || [] + : sessionState.main_session.tracks; + + const currentTitle = isSectionTab + ? `MIXER: SECTION TAB (${activeTabContext.title})` + : "MIXER: MAIN SESSION"; + + return ( +