Files
ryanontheinside-ComfyUI_Rya…/web/js/midi_extensions.js

395 lines
17 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { drawPiano, handlePianoMouseDown } from "./piano.js";
app.registerExtension({
name: "org.ryanontheinside.midi_load_and_extract",
// async setup() {
// console.log("MIDI Extension: Setup called");
// },
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MIDILoadAndExtract") {
// 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.uploadMIDI = function() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".mid,.midi";
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const formData = new FormData();
formData.append("file", file);
fetch("/upload_midi", {
method: "POST",
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update the midi_file widget with the new file path
const midiFileWidget = this.widgets.find(w => w.name === "midi_file");
if (midiFileWidget) {
midiFileWidget.value = data.file_path;
// Trigger any necessary updates
if (midiFileWidget.callback) {
midiFileWidget.callback(data.file_path);
}
}
// You might want to call updateTrackSelection here if it's needed
this.updateTrackSelection();
this.saveState(); // Save state after successful upload
} else {
console.error("Failed to upload MIDI file:", data.error);
}
})
.catch(error => {
console.error("Error uploading MIDI file:", error);
});
}
};
input.click();
};
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();
this.addWidget("button", "Upload MIDI", "upload", () => {
this.uploadMIDI();
});
this.addWidget("button", "Refresh", "refresh", () => {
this.refreshMIDIData();
});
const notesWidget = this.widgets.find(widget => widget.name === "notes");
if (notesWidget) {
// Disable the widget
this.disableWidget(notesWidget);
// Hide the widget
notesWidget.hidden = true;
}
// Add event listeners for midi_file and track_selection widgets
const midiFileWidget = this.widgets.find(w => w.name === "midi_file");
if (midiFileWidget) {
midiFileWidget.callback = (value) => {
this.updateTrackSelection();
this.saveState(); // Save state when MIDI file changes
};
}
const trackSelectionWidget = this.widgets.find(w => w.name === "track_selection");
if (trackSelectionWidget) {
trackSelectionWidget.callback = () => this.onTrackSelectionChange(trackSelectionWidget.value);
}
// 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();
// Set up a flag to indicate initial load
this.isInitialLoad = true;
// Instead of calling updateTrackSelection here, we'll set up a callback
// that will be called when the graph is available
this.onGraphAvailable = () => {
this.updateTrackSelection();
};
};
nodeType.prototype.loadSavedState = function() {
const savedState = localStorage.getItem(`MIDILoadAndExtract_${this.id}`);
if (savedState) {
this.loadedState = JSON.parse(savedState);
const midiFileWidget = this.widgets.find(w => w.name === "midi_file");
if (midiFileWidget && this.loadedState.midiFile) {
midiFileWidget.value = this.loadedState.midiFile;
}
}
};
nodeType.prototype.updateTrackSelection = async function() {
if (this.isUpdatingTrackSelection) {
console.log('Track selection update already in progress, skipping');
return;
}
this.isUpdatingTrackSelection = true;
try {
const midiFile = this.getCurrentMidiFile();
if (!midiFile) {
console.warn('No MIDI file selected');
return;
}
const response = await fetch('/get_track_notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ midi_file: midiFile })
});
const data = await response.json();
if (data.error) {
console.error('Error fetching track notes:', data.error);
return;
}
const trackSelectionWidget = this.widgets.find(w => w.name === "track_selection");
if (trackSelectionWidget) {
// Update the options
trackSelectionWidget.options.values = data.tracks;
}
// Update available notes
this.availableNotes = new Set(data.all_notes.split(',').map(Number));
// Apply loaded state if this is the initial load
if (this.isInitialLoad && this.loadedState) {
this.applyLoadedState();
this.isInitialLoad = false;
}
this.setDirtyCanvas(true, true);
} catch (error) {
console.error('Error updating track selection:', error);
this.availableNotes = new Set(); // Reset to empty Set in case of error
} finally {
this.isUpdatingTrackSelection = false;
}
};
nodeType.prototype.refreshMIDIData = async function() {
const midiFile = this.getCurrentMidiFile();
const trackSelection = this.widgets.find(w => w.name === "track_selection").value;
if (!midiFile) {
console.warn('No MIDI file selected');
return;
}
try {
const response = await fetch('/refresh_midi_data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ midi_file: midiFile, track_selection: trackSelection })
});
const data = await response.json();
if (data.error) {
console.error('Error refreshing MIDI data:', data.error);
return;
}
// Update available tracks
const trackSelectionWidget = this.widgets.find(w => w.name === "track_selection");
if (trackSelectionWidget) {
trackSelectionWidget.options.values = data.tracks;
}
// Update available notes
this.availableNotes = new Set(data.all_notes.split(',').map(Number));
// Filter out selected notes that are no longer available
if (this.properties.selectedNotes) {
this.properties.selectedNotes = this.properties.selectedNotes.filter(note => this.availableNotes.has(note));
} else {
this.properties.selectedNotes = [];
}
this.syncNotesWidget();
// Trigger redraw
this.setDirtyCanvas(true, true);
} catch (error) {
console.error('Error refreshing MIDI data:', error);
}
};
nodeType.prototype.applyLoadedState = function() {
if (this.loadedState) {
const trackSelectionWidget = this.widgets.find(w => w.name === "track_selection");
if (trackSelectionWidget && this.loadedState.trackSelection) {
if (trackSelectionWidget.options.values.includes(this.loadedState.trackSelection)) {
trackSelectionWidget.value = this.loadedState.trackSelection;
} else {
console.log(`Loaded track selection ${this.loadedState.trackSelection} is no longer valid`);
}
}
if (this.loadedState.selectedNotes) {
this.properties.selectedNotes = this.loadedState.selectedNotes.filter(note => this.availableNotes.has(note));
}
this.syncNotesWidget();
delete this.loadedState;
this.saveState(); // Save the applied state
}
};
nodeType.prototype.saveState = function() {
const state = {
midiFile: this.getCurrentMidiFile(),
trackSelection: this.widgets.find(w => w.name === "track_selection")?.value,
selectedNotes: this.properties.selectedNotes
};
localStorage.setItem(`MIDILoadAndExtract_${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');
// Only call saveState if it's available
if (typeof this.saveState === 'function') {
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) {
console.warn('availableNotes is undefined, initializing as empty Set');
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);
};
nodeType.prototype.getCurrentMidiFile = function() {
const midiFileWidget = this.widgets.find(w => w.name === "midi_file");
return midiFileWidget ? midiFileWidget.value : null;
};
nodeType.prototype.onTrackSelectionChange = async function(selectedTrack) {
try {
const midiFile = this.getCurrentMidiFile();
const response = await fetch('/get_track_notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ midi_file: midiFile })
});
const data = await response.json();
if (data.error) {
console.error('Error fetching track notes:', data.error);
return;
}
const notesInTrack = selectedTrack === "all"
? data.all_notes
: data.track_notes[selectedTrack.split(':')[0]];
this.availableNotes = new Set(notesInTrack.split(',').map(Number));
// Filter out selected notes that are no longer available
if (this.properties.selectedNotes) {
this.properties.selectedNotes = this.properties.selectedNotes.filter(note => this.availableNotes.has(note));
} else {
this.properties.selectedNotes = [];
}
this.syncNotesWidget();
// Trigger redraw
this.setDirtyCanvas(true, true);
} catch (error) {
console.error('Error updating piano notes:', error);
}
};
// Add this at the end of the function
const onAdded = app.graph.onNodeAdded;
app.graph.onNodeAdded = function(node) {
if (onAdded) {
onAdded.call(this, node);
}
if (node.type === "MIDILoadAndExtract" && node.onGraphLoaded) {
node.onGraphLoaded();
}
};
}
},
});