Files
2025-05-17 15:35:47 -04:00

343 lines
16 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { drawPiano, handlePianoMouseDown } from "./piano.js";
app.registerExtension({
name: "org.ryanontheinside.midi_feature_extractor",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
//claude: do not change the node name
if (nodeData.name === "MIDIFeatureExtractor") {
// Add the disableWidget method to the nodeType prototype
nodeType.prototype.disableWidget = function(widget) {
if (widget) {
widget.disabled = true;
widget.visibleWidth = 0;
widget.onMouseDown = () => {};
widget.onMouseMove = () => {};
widget.onMouseUp = () => {};
}
};
nodeType.prototype.onNodeCreated = function() {
this.properties = this.properties || {};
this.properties.selectedNotes = []; // Initialize with an empty array
this.pianoScroll = 0;
this.scrollLeftIndicator = null;
this.scrollRightIndicator = null;
this.availableNotes = new Set();
const notesWidget = this.widgets.find(widget => widget.name === "notes");
if (notesWidget) {
// Disable the widget
this.disableWidget(notesWidget);
// Hide the widget
notesWidget.hidden = true;
}
// Increase the node's height and width
const extraHeight = 80; // Piano height + margins
const pianoWidth = 1020; // Total width of the piano
this.size[1] += extraHeight;
this.size[0] = Math.max(this.size[0], pianoWidth); // Set width to at least piano width plus margins
// Ensure the node recomputes its size
this.setDirtyCanvas(true, true);
// Sync the notes widget initially
this.syncNotesWidget();
// Load saved state
this.loadSavedState();
// Check for existing connections after a short delay
setTimeout(() => {
const connectedNodes = this.graph._nodes.filter(
n => n.type === 'MIDILoader' && this.isNodeConnected(n)
);
if (connectedNodes.length > 0) {
const loaderNode = connectedNodes[0];
this.queryMIDILoaderNotes(loaderNode);
this.listenToMIDILoaderChanges(loaderNode);
}
}, 100); // Small delay to ensure links are established
};
// Handle connections changing
nodeType.prototype.onConnectionsChange = function(type, index, connected, link_info) {
if (type === 1) { // Input connection (1 is for inputs, 0 is for outputs)
if (connected && link_info) {
// New connection established
const inputNode = this.graph.getNodeById(link_info.origin_id);
if (inputNode && inputNode.type === 'MIDILoader') {
this.queryMIDILoaderNotes(inputNode);
this.listenToMIDILoaderChanges(inputNode);
}
} else {
// Check if we still have any connected MIDILoader nodes
const connectedMidiNodes = this.graph._nodes.filter(
n => n.type === 'MIDILoader' && this.isNodeConnected(n)
);
if (connectedMidiNodes.length === 0) {
// All MIDILoader connections removed
this.availableNotes = new Set();
this.properties.selectedNotes = [];
this.syncNotesWidget();
this.setDirtyCanvas(true, true);
}
}
}
};
// Check if a node is connected to this node
nodeType.prototype.isNodeConnected = function(node) {
for (const link_id in this.graph.links) {
const link = this.graph.links[link_id];
if (link.target_id === this.id && link.origin_id === node.id) {
return true;
}
}
return false;
};
// Query the MIDILoader for available notes
nodeType.prototype.queryMIDILoaderNotes = async function(loaderNode) {
if (!loaderNode) return;
const midiFile = loaderNode.getCurrentMidiFile();
const trackSelection = loaderNode.widgets.find(w => w.name === "track_selection").value;
// Get measure selection parameters from the MIDILoader
const startMeasureWidget = loaderNode.widgets.find(w => w.name === "start_measure");
const startBeatWidget = loaderNode.widgets.find(w => w.name === "start_beat");
const endMeasureWidget = loaderNode.widgets.find(w => w.name === "end_measure");
const endBeatWidget = loaderNode.widgets.find(w => w.name === "end_beat");
const startMeasure = startMeasureWidget ? startMeasureWidget.value : 1;
const startBeat = startBeatWidget ? startBeatWidget.value : 1;
const endMeasure = endMeasureWidget ? endMeasureWidget.value : 0;
const endBeat = endBeatWidget ? endBeatWidget.value : 1;
if (!midiFile) return;
try {
console.log(`Querying MIDI notes with startMeasure=${startMeasure}, startBeat=${startBeat}, endMeasure=${endMeasure}, endBeat=${endBeat}`);
const response = await fetch('/refresh_midi_data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
midi_file: midiFile,
track_selection: trackSelection,
start_measure: startMeasure,
start_beat: startBeat,
end_measure: endMeasure,
end_beat: endBeat
})
});
const data = await response.json();
if (data.error) {
console.error('Error fetching MIDI notes:', data.error);
return;
}
// Update available notes
const noteArray = data.all_notes.split(',').map(Number).filter(n => !isNaN(n));
console.log(`Received ${noteArray.length} available notes from measures ${startMeasure} to ${endMeasure}, time signature: ${data.time_signature}`);
this.availableNotes = new Set(noteArray);
// Filter out selected notes that are no longer available
if (this.properties.selectedNotes) {
const originalCount = this.properties.selectedNotes.length;
this.properties.selectedNotes = this.properties.selectedNotes.filter(
note => this.availableNotes.has(note)
);
if (originalCount !== this.properties.selectedNotes.length) {
console.log(`Filtered out ${originalCount - this.properties.selectedNotes.length} selected notes`);
}
}
this.syncNotesWidget();
this.setDirtyCanvas(true, true);
} catch (error) {
console.error('Error querying MIDI loader:', error);
}
};
// Listen for changes in the MIDILoader node
nodeType.prototype.listenToMIDILoaderChanges = function(loaderNode) {
// Store reference to loader node
this.connectedLoaderNode = loaderNode;
// Check if we already added a listener to this node
if (loaderNode._feature_extractor_listeners &&
loaderNode._feature_extractor_listeners.includes(this.id)) {
return;
}
// Initialize listener array if needed
if (!loaderNode._feature_extractor_listeners) {
loaderNode._feature_extractor_listeners = [];
}
loaderNode._feature_extractor_listeners.push(this.id);
// Find all relevant widgets in loader node
const trackWidget = loaderNode.widgets.find(w => w.name === "track_selection");
const startMeasureWidget = loaderNode.widgets.find(w => w.name === "start_measure");
const startBeatWidget = loaderNode.widgets.find(w => w.name === "start_beat");
const endMeasureWidget = loaderNode.widgets.find(w => w.name === "end_measure");
const endBeatWidget = loaderNode.widgets.find(w => w.name === "end_beat");
// Set up callbacks for all relevant widgets
if (trackWidget) {
const origTrackCallback = trackWidget.callback;
trackWidget.callback = (value) => {
// Call original callback
if (origTrackCallback) origTrackCallback.call(loaderNode, value);
// Notify this feature extractor
this.queryMIDILoaderNotes(loaderNode);
};
}
if (startMeasureWidget) {
const origCallback = startMeasureWidget.callback;
startMeasureWidget.callback = (value) => {
// Call original callback
if (origCallback) origCallback.call(loaderNode, value);
// Notify this feature extractor
this.queryMIDILoaderNotes(loaderNode);
};
}
if (startBeatWidget) {
const origCallback = startBeatWidget.callback;
startBeatWidget.callback = (value) => {
// Call original callback
if (origCallback) origCallback.call(loaderNode, value);
// Notify this feature extractor
this.queryMIDILoaderNotes(loaderNode);
};
}
if (endMeasureWidget) {
const origCallback = endMeasureWidget.callback;
endMeasureWidget.callback = (value) => {
// Call original callback
if (origCallback) origCallback.call(loaderNode, value);
// Notify this feature extractor
this.queryMIDILoaderNotes(loaderNode);
};
}
if (endBeatWidget) {
const origCallback = endBeatWidget.callback;
endBeatWidget.callback = (value) => {
// Call original callback
if (origCallback) origCallback.call(loaderNode, value);
// Notify this feature extractor
this.queryMIDILoaderNotes(loaderNode);
};
}
};
nodeType.prototype.loadSavedState = function() {
const savedState = localStorage.getItem(`MIDIFeatureExtractor_${this.id}`);
if (savedState) {
try {
const state = JSON.parse(savedState);
if (state.selectedNotes) {
this.properties.selectedNotes = state.selectedNotes;
}
} catch (e) {
console.error("Error loading saved state:", e);
}
}
};
nodeType.prototype.saveState = function() {
const state = {
selectedNotes: this.properties.selectedNotes
};
localStorage.setItem(`MIDIFeatureExtractor_${this.id}`, JSON.stringify(state));
};
nodeType.prototype.syncNotesWidget = function() {
const notesWidget = this.widgets.find(widget => widget.name === "notes");
if (notesWidget) {
const newValue = this.properties.selectedNotes.sort((a, b) => a - b).join(", ");
if (notesWidget.value !== newValue) {
notesWidget.value = newValue;
// Trigger a change event on the widget
if (notesWidget.callback) {
notesWidget.callback(newValue);
}
}
}
// Ensure the node updates its internal state
this.onPropertyChanged('selectedNotes');
// Save state
this.saveState();
};
nodeType.prototype.onPropertyChanged = function(property) {
if (property === 'selectedNotes') {
// Trigger any necessary updates or recomputation
this.setDirtyCanvas(true, true);
}
};
// Save the original method
nodeType.prototype._originalOnDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
// Call the original onDrawForeground method if it exists
if (typeof nodeType.prototype._originalOnDrawForeground === 'function') {
nodeType.prototype._originalOnDrawForeground.call(this, ctx);
}
if (this.flags.collapsed) return;
// Ensure availableNotes exists before drawing
if (!this.availableNotes) {
this.availableNotes = new Set();
}
// Draw widgets that are not hidden
for (let w of this.widgets) {
if (w.hidden) {
continue; // Skip drawing hidden widgets
}
if (w.draw) {
w.draw(ctx, this, this.pos);
}
}
drawPiano(ctx, this);
};
// Save the original onMouseDown method
nodeType.prototype._originalOnMouseDown = nodeType.prototype.onMouseDown;
nodeType.prototype.onMouseDown = function(event, pos, graphcanvas) {
// Call the original onMouseDown method if it exists
if (typeof nodeType.prototype._originalOnMouseDown === 'function') {
nodeType.prototype._originalOnMouseDown.call(this, event, pos, graphcanvas);
}
const [x, y] = pos;
return handlePianoMouseDown(this, x, y);
};
}
},
});