Files
SonicForgeStudio/md/44_MIXER_EQUALIZER.md
T

21 KiB

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

[ 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).

+------------------------------------------------------------------------------------------------+
| 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)

{
  "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)

{
  "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):

// 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:

// 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 (
    <div className="flex flex-col h-64 bg-slate-900 border-t border-slate-700 text-slate-200 select-none">
      {/* Mixer Header / Tab Indicator */}
      <div className="flex items-center justify-between px-3 py-1 bg-slate-950 border-b border-slate-800 text-xs font-semibold">
        <div className="flex items-center gap-2">
          <span className={`w-2.5 h-2.5 rounded-full ${isSectionTab ? "bg-amber-400 animate-pulse" : "bg-indigo-500"}`} />
          <span className="text-white tracking-wide">{currentTitle}</span>
        </div>
        {isSectionTab && (
          <span className="text-[10px] text-amber-300/80 bg-amber-950/60 px-2 py-0.5 rounded border border-amber-800/50">
            Routed to Main Track: {activeTabContext.parent_track_name || "Track Host"}
          </span>
        )}
      </div>

      {/* Mixer Strips Container */}
      <div className="flex-1 flex overflow-x-auto p-2 gap-1.5 custom-scrollbar bg-slate-900/90">
        {/* 1. MASTER STRIP (Far Left) */}
        <MasterChannelStrip
          isSectionBus={isSectionTab}
          mixerGraphMgr={mixerGraphMgr}
        />

        <div className="w-[1px] bg-slate-700/60 mx-1 my-1" />

        {/* 2. TRACK STRIPS (Audio Channels) */}
        {currentTracks.map((track, idx) => (
          <TrackChannelStrip
            key={track.id}
            track={track}
            index={idx + 1}
            mixerGraphMgr={mixerGraphMgr}
          />
        ))}
      </div>
    </div>
  );
}

/**
 * Channel Strip Component representing a single Track
 */
function TrackChannelStrip({ track, index, mixerGraphMgr }) {
  const [volumeDb, setVolumeDb] = useState(track.volume_db || 0);
  const [pan, setPan] = useState(track.pan || 0);
  const [isMuted, setIsMuted] = useState(Boolean(track.mute));
  const [isSoloed, setIsSoloed] = useState(Boolean(track.solo));

  const handleVolumeChange = (e) => {
    const newDb = parseFloat(e.target.value);
    setVolumeDb(newDb);
    track.volume_db = newDb;
    mixerGraphMgr?.updateTrackVolumePan(track.id, newDb, pan, isMuted);
  };

  const handleToggleMute = () => {
    const newMute = !isMuted;
    setIsMuted(newMute);
    track.mute = newMute;
    mixerGraphMgr?.updateTrackVolumePan(track.id, volumeDb, pan, newMute);
  };

  return (
    <div className="w-24 bg-slate-950 border border-slate-800 rounded flex flex-col p-1.5 items-center justify-between shadow-lg">
      {/* 1. Pan Knob */}
      <div className="flex flex-col items-center w-full">
        <span className="text-[10px] text-slate-400 font-mono">
          {pan === 0 ? "center" : pan < 0 ? `L${Math.abs(Math.round(pan * 100))}` : `R${Math.round(pan * 100)}`}
        </span>
        <input
          type="range"
          min="-1"
          max="1"
          step="0.05"
          value={pan}
          onChange={(e) => {
            const p = parseFloat(e.target.value);
            setPan(p);
            track.pan = p;
            mixerGraphMgr?.updateTrackVolumePan(track.id, volumeDb, p, isMuted);
          }}
          className="w-12 h-1 accent-indigo-500 cursor-pointer"
        />
      </div>

      {/* 2. dB Readout */}
      <div className="text-[11px] font-mono text-slate-300 font-semibold my-0.5">
        {volumeDb <= -60 ? "-inf" : `${volumeDb > 0 ? "+" : ""}${volumeDb.toFixed(1)}dB`}
      </div>

      {/* 3. Mute / Solo Buttons */}
      <div className="flex gap-1 w-full my-1">
        <button
          onClick={handleToggleMute}
          className={`flex-1 text-[10px] font-bold py-0.5 rounded border transition-colors ${
            isMuted ? "bg-amber-600 text-white border-amber-500" : "bg-slate-800 text-slate-400 border-slate-700 hover:bg-slate-700"
          }`}
        >
          M
        </button>
        <button
          onClick={() => {
            const s = !isSoloed;
            setIsSoloed(s);
            track.solo = s;
          }}
          className={`flex-1 text-[10px] font-bold py-0.5 rounded border transition-colors ${
            isSoloed ? "bg-yellow-500 text-black border-yellow-400" : "bg-slate-800 text-slate-400 border-slate-700 hover:bg-slate-700"
          }`}
        >
          S
        </button>
      </div>

      {/* 4. Fader & Meter Section */}
      <div className="flex-1 flex items-center justify-center gap-1.5 w-full my-1 relative">
        {/* Fader Slider */}
        <input
          type="range"
          min="-60"
          max="12"
          step="0.5"
          value={volumeDb}
          onChange={handleVolumeChange}
          className="h-28 w-2 appearance-none bg-slate-800 rounded outline-none cursor-pointer [writing-mode:vertical-lr] [direction:rtl]"
        />

        {/* VU Peak Meter Bar */}
        <div className="w-2.5 h-28 bg-slate-900 border border-slate-800 rounded overflow-hidden flex flex-col justify-end p-0.5">
          <div className="w-full bg-gradient-to-t from-emerald-500 via-yellow-400 to-red-500 h-[40%]" />
        </div>
      </div>

      {/* 5. FX & Power Buttons */}
      <div className="flex gap-1 w-full my-1">
        <button className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[9px] py-0.5 rounded border border-slate-700 font-semibold">
          FX
        </button>
        <button className="px-1.5 bg-slate-800 hover:bg-slate-700 text-slate-400 text-[9px] py-0.5 rounded border border-slate-700">
          
        </button>
      </div>

      {/* 6. Footer Name */}
      <div className="w-full text-center bg-slate-900 py-1 rounded border border-slate-800/80 mt-1">
        <div className="text-[10px] font-bold text-white truncate px-1">{index}. {track.name}</div>
        <div className="text-[8px] text-slate-500 uppercase tracking-tighter truncate">{track.type}</div>
      </div>
    </div>
  );
}

/**
 * Master Channel Strip Component
 */
function MasterChannelStrip({ isSectionBus, mixerGraphMgr }) {
  return (
    <div className="w-28 bg-slate-950 border-2 border-indigo-900/60 rounded flex flex-col p-1.5 items-center justify-between shadow-2xl">
      <div className="text-[10px] font-bold text-indigo-400 uppercase tracking-wider">
        {isSectionBus ? "SEC BUS" : "MASTER"}
      </div>

      {/* Peak / RMS Canvas Meter */}
      <div className="w-full h-32 bg-black border border-indigo-950 rounded relative my-2 overflow-hidden flex items-center justify-center">
        <span className="text-[9px] font-mono text-emerald-400/80 font-semibold">-inf dB</span>
      </div>

      <div className="w-full text-center bg-indigo-950/80 border border-indigo-800/60 py-1 rounded mt-auto">
        <div className="text-[10px] font-bold text-indigo-200 uppercase">
          {isSectionBus ? "SUB-MIX" : "MAIN OUT"}
        </div>
      </div>
    </div>
  );
}


7. OPERATIONAL VERIFICATION CHECKLIST

  • Main Session Mixer Operations:

  • Adjust Track 1 Fader \rightarrow Track 1 volume increases/decreases as expected.

  • Mute Track 1 \rightarrow Track 1 output is silenced.

  • SECTION-TAB Mixer & Routing Operations:

  • Open a SECTION-TAB into a sub-tab \rightarrow Mixer Console dynamically switches header to MIXER: SECTION TAB displaying internal section tracks.

  • Adjust Section Track 1 Fader \rightarrow Guitar volume within the Section increases/decreases without affecting other Main Tracks.

  • Bus Routing Point Verification:

  • Switch back to MAIN SESSION tab.

  • Adjust Track 2 Fader (Track hosting Section Item) \rightarrow Entire SECTION-TAB volume adjusts synchronously.

  • Mute Track 2 \rightarrow Entire SECTION-TAB output is silenced completely.