# TECHNICAL SPECIFICATION: GHOST NOTES FEATURE IN PIANO ROLL TAB This document details the workflow and processing algorithms for opening any `MIDIItem` (for example, MIDI Item 1 spanning Bar 0 to Bar 4) on the main Timeline in a Piano Roll Tab, while extracting and displaying all notes from other tracks occupying the same time interval (Bar 0 - Bar 4) as Ghost Notes (faded reference notes that cannot be interactively edited). --- ## 1. DATA FLOW & MODULE ARCHITECTURE DIAGRAM ```text [ USER DBL-CLICK ITEM 1 ] (Track A, Bar 0 - 4) | v [ 1. Context Extractor Engine ] ├── Target Item: Active Editing Item ├── Window Bounds: [Bar 0.0 -> Bar 4.0] └── Scan All Other Tracks (B, C, D...) | v [ 2. Overlap Filtering Algorithm ] ├── Track B (Bass): Item B1 (Bar 0 - 8) -> Overlap! Slice [Bar 0 -> 4] ├── Track C (Pads): Item C1 (Bar 2 - 6) -> Overlap! Slice [Bar 2 -> 4] └── Track D (Lead): Item D1 (Bar 5 - 8) -> Out of bounds! Ignore | v [ 3. Piano Roll State Store ] ├── activeItem: MIDI Item 1 (Full Edit Access) └── ghostLayers: [ Track B Notes, Track C Notes ] (Read-Only) | v [ 4. Multi-Layer Canvas Renderer ] ├── Layer 1: Background Grid & Pitch Keys ├── Layer 2: Ghost Notes (Opacity 25%, Muted Color, Pointer Events OFF) └── Layer 3: Active Notes (Full Opacity, Drag/Drop/Resize Allowed) ``` --- ## 2. TIME-WINDOW OVERLAP FILTERING ALGORITHM To determine whether a `MIDIItem` on another track overlaps with the interval $[start\_bar, end\_bar]$ of MIDI Item 1, a 1D geometric condition is applied: ### Overlap Condition Two time intervals $[A_{start}, A_{end}]$ and $[B_{start}, B_{end}]$ intersect if and only if: $$B_{start} < A_{end} \quad \text{AND} \quad B_{end} > A_{start}$$ Where: * $A_{start} = \text{Item1.start\_bar} = 0.0$ * $A_{end} = \text{Item1.start\_bar} + \text{Item1.duration\_bars} = 4.0$ ```javascript // app/static/js/services/ghostNoteExtractor.js /** * Extracts a list of Ghost Notes from other tracks within the specified Bar window * @param {Object} sessionState - Full Main Session state object * @param {string} targetTrackId - ID of the currently active editing Track (Track A) * @param {number} windowStartBar - Start bar position of the active Item (e.g., 0.0) * @param {number} windowDurationBars - Bar duration of the active Item (e.g., 4.0) * @returns {Array} List of normalized Ghost Layers */ export function extractGhostLayers(sessionState, targetTrackId, windowStartBar, windowDurationBars) { const windowEndBar = windowStartBar + windowDurationBars; const timeSigNumerator = sessionState.metadata.time_signature_numerator || 4; const ghostLayers = []; // Iterate over all tracks in Main Session sessionState.main_session.tracks.forEach((track) => { // Ignore active editing track and non-MIDI or muted tracks if (track.id === targetTrackId || track.type !== "MIDI" || track.mute) { return; } const trackGhostNotes = []; // Iterate over all items in target candidate tracks track.items.forEach((item) => { if (item.type !== "MIDI_ITEM") return; const itemStartBar = item.start_bar; const itemEndBar = item.start_bar + item.duration_bars; // Evaluate Overlap Condition if (itemStartBar < windowEndBar && itemEndBar > windowStartBar) { const itemOffsetBar = item.clip_start_offset_bars || 0.0; item.source_data.notes.forEach((note) => { // Convert internal note start_beat to absolute timeline beats const noteAbsoluteBeat = (itemStartBar * timeSigNumerator) + note.start_beat - (itemOffsetBar * timeSigNumerator); const noteEndAbsoluteBeat = noteAbsoluteBeat + note.duration_beats; const windowStartBeat = windowStartBar * timeSigNumerator; const windowEndBeat = windowEndBar * timeSigNumerator; // Retain notes truly within the visible bounds [windowStartBeat -> windowEndBeat] if (noteAbsoluteBeat < windowEndBeat && noteEndAbsoluteBeat > windowStartBeat) { trackGhostNotes.push({ id: `ghost_${note.id}`, pitch: note.pitch, // Convert beat position to relative coordinates of the Piano Roll Window (0 -> duration_beats) relative_start_beat: noteAbsoluteBeat - windowStartBeat, duration_beats: note.duration_beats, velocity: note.velocity, original_track_name: track.name, original_track_color: track.color || "#888888" }); } }); } }); if (trackGhostNotes.length > 0) { ghostLayers.push({ track_id: track.id, track_name: track.name, track_color: track.color || "#6b7280", notes: trackGhostNotes }); } }); return ghostLayers; } ``` --- ## 3. STATE STRUCTURE FOR PIANO ROLL TAB When the user opens MIDI Item 1, the Tab Controller generates an isolated context payload for the Piano Roll: ```javascript // Data Payload passed into the PianoRollTab Component const pianoRollTabContext = { tab_id: "tab_pianoroll_item_1", title: "Piano Roll - MIDI Item 1", type: "PIANO_ROLL_TAB", parent_tab_id: "tab_main_session", // 1. Target Item open for direct interactive editing active_context: { track_id: "track_A", item_id: "item_1", item_name: "MIDI Item 1", start_bar: 0.0, duration_bars: 4.0, notes: [/* Original source MIDI notes of Item 1 */] }, // 2. Read-only Ghost Layers displayed as background reference indicators ghost_layers: [ { track_id: "track_B_bass", track_name: "Track B (Bass)", track_color: "#3b82f6", // Green / Blue notes: [ { pitch: 36, relative_start_beat: 0.0, duration_beats: 4.0, velocity: 0.9 }, { relative_start_beat: 4.0, pitch: 38, duration_beats: 4.0, velocity: 0.8 } ] }, { track_id: "track_C_pads", track_name: "Track C (Pads)", track_color: "#ec4899", // Pink notes: [ { pitch: 60, relative_start_beat: 8.0, duration_beats: 8.0, velocity: 0.6 } ] } ] }; ``` --- ## 4. MULTI-LAYER CANVAS RENDERER WORKFLOW Inside the Piano Roll canvas rendering module, elements are drawn sequentially by z-index order to position Ghost Notes behind Active Notes: ```javascript // app/static/js/views/pianoRollRenderer.js export function renderPianoRollCanvas(ctx, canvasWidth, canvasHeight, viewState, activeItem, ghostLayers) { const { zoomX, zoomY, scrollX, scrollY, noteHeight } = viewState; // ----------------------------------------------------------------- // LAYER 1: BACKGROUND & GRID LINES // ----------------------------------------------------------------- drawPianoGrid(ctx, canvasWidth, canvasHeight, viewState); // ----------------------------------------------------------------- // LAYER 2: GHOST NOTES (BACKGROUND REFERENCE FROM OTHER TRACKS) // ----------------------------------------------------------------- if (ghostLayers && ghostLayers.length > 0) { ghostLayers.forEach((layer) => { ctx.save(); // Set faded opacity (20% - 30% opacity) ctx.globalAlpha = 0.25; ctx.fillStyle = layer.track_color; ctx.strokeStyle = layer.track_color; layer.notes.forEach((note) => { const x = (note.relative_start_beat - scrollX) * zoomX; const y = (127 - note.pitch - scrollY) * noteHeight; const w = note.duration_beats * zoomX; const h = noteHeight - 1; // 1px border gap // Draw Ghost note body (Dashed border or light stroke) ctx.fillRect(x, y, w, h); ctx.setLineDash([2, 2]); // Dashed lines indicating non-interactive status ctx.lineWidth = 1; ctx.strokeRect(x, y, w, h); }); ctx.restore(); }); } // ----------------------------------------------------------------- // LAYER 3: ACTIVE NOTES (EDITABLE NOTES FOR ITEM 1) // ----------------------------------------------------------------- activeItem.notes.forEach((note) => { ctx.save(); ctx.globalAlpha = 1.0; // 100% full opacity const x = (note.start_beat - scrollX) * zoomX; const y = (127 - note.pitch - scrollY) * noteHeight; const w = note.duration_beats * zoomX; const h = noteHeight - 1; // Bright fill colors for active notes ctx.fillStyle = note.selected ? "#f59e0b" : "#10b981"; // Orange if selected, green if default ctx.fillRect(x, y, w, h); ctx.strokeStyle = "#ffffff"; ctx.lineWidth = 1; ctx.strokeRect(x, y, w, h); ctx.restore(); }); } ``` --- ## 5. POINTER INTERACTION RULES To prevent accidental modification or drag interactions with Ghost Notes: * **Hit-Testing Isolation:** On pointer events (`mousedown`, `mousemove`, or marquee selection), the Hit-Test algorithm queries `activeItem.notes` exclusively. `ghostLayers` items are omitted from event evaluation. * **Toggle Ghost Notes Visibility:** The Piano Roll toolbar includes an Eye / Ghost icon button (`[👻 Ghost Notes]`) allowing operators to toggle the visibility of the reference background layer. * **Live Synchronous Updates:** Modifying a note on Track B within the Main Session or an adjacent tab dispatches an `EVENT_SESSION_UPDATED` Event Bus signal. The active Piano Roll Tab receives this event and invokes `extractGhostLayers()` to dynamically redraw updated Ghost Note positions.