feat(piano-roll): ghost notes + dropdown item switcher + session sync
- MIDI ghost notes: all non-selected items rendered at 25% opacity - Dropdown at tab title: switch active edit target across all tracks - Session Sync mode (default): viewport aligned to session bars - Isolated mode toggle: bar 0, no ghost notes - Ghost toggle: show/hide ghost layer (only in session mode) - Auto-scroll to session position in session sync mode - Bar labels show absolute session bar numbers
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// SonicForge Studio Ghost Note Extractor Service
|
||||
(function() {
|
||||
function extractGhostLayers(activeTracks, targetTrackId, targetItemId, bpm) {
|
||||
if (!activeTracks || !targetItemId) return [];
|
||||
|
||||
const secondsPerBeat = 60.0 / (parseInt(bpm) || 120);
|
||||
|
||||
let targetItem = null;
|
||||
for (var i = 0; i < activeTracks.length; i++) {
|
||||
var t = activeTracks[i];
|
||||
var found = (t.midiItems || []).find(function(m) { return m.id === targetItemId; });
|
||||
if (found) { targetItem = found; break; }
|
||||
}
|
||||
if (!targetItem) return [];
|
||||
|
||||
const windowStartBeat = targetItem.startTime / secondsPerBeat;
|
||||
const windowEndBeat = (targetItem.startTime + targetItem.duration) / secondsPerBeat;
|
||||
|
||||
const ghostLayers = [];
|
||||
|
||||
for (var i = 0; i < activeTracks.length; i++) {
|
||||
var track = activeTracks[i];
|
||||
if (!track.midiItems || !track.midiItems.length) continue;
|
||||
if (track.muted) continue;
|
||||
|
||||
var trackGhostNotes = [];
|
||||
|
||||
for (var j = 0; j < track.midiItems.length; j++) {
|
||||
var item = track.midiItems[j];
|
||||
if (item.id === targetItemId) continue;
|
||||
|
||||
var itemStartBeat = item.startTime / secondsPerBeat;
|
||||
var itemEndBeat = (item.startTime + item.duration) / secondsPerBeat;
|
||||
|
||||
if (itemStartBeat >= windowEndBeat || itemEndBeat <= windowStartBeat) continue;
|
||||
|
||||
var notes = item.notes || [];
|
||||
for (var k = 0; k < notes.length; k++) {
|
||||
var note = notes[k];
|
||||
var noteAbsStart = itemStartBeat + (note.start_beat || 0);
|
||||
var noteAbsEnd = noteAbsStart + (note.duration_beats || 1);
|
||||
|
||||
if (noteAbsStart >= windowEndBeat || noteAbsEnd <= windowStartBeat) continue;
|
||||
|
||||
var clampedStart = Math.max(noteAbsStart, windowStartBeat);
|
||||
var clampedEnd = Math.min(noteAbsEnd, windowEndBeat);
|
||||
var clampedDur = clampedEnd - clampedStart;
|
||||
|
||||
trackGhostNotes.push({
|
||||
id: 'ghost_' + (note.id || Math.random().toString(36).substr(2, 9)),
|
||||
pitch: note.pitch,
|
||||
relative_start_beat: clampedStart - windowStartBeat,
|
||||
duration_beats: clampedDur,
|
||||
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;
|
||||
}
|
||||
|
||||
window.SonicGhost = { extractGhostLayers: extractGhostLayers };
|
||||
})();
|
||||
Reference in New Issue
Block a user