383 lines
16 KiB
JavaScript
383 lines
16 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
import { ComfyWidgets } from '../../../scripts/widgets.js';
|
|
|
|
import {
|
|
hideWidget,
|
|
showWidget,
|
|
} from "./node_utils.js";
|
|
|
|
app.registerExtension({
|
|
name: "vrch.MidiDeviceLoader",
|
|
async nodeCreated(node) {
|
|
if (node.comfyClass === "VrchMidiDeviceLoaderNode") {
|
|
// Get required widgets
|
|
const deviceIdWidget = node.widgets.find(w => w.name === "device_id");
|
|
const nameWidget = node.widgets.find(w => w.name === "name");
|
|
const rawDataWidget = node.widgets.find(w => w.name === "raw_data");
|
|
const debugWidget = node.widgets.find(w => w.name === "debug");
|
|
|
|
// MIDI access object
|
|
let midiAccess = null;
|
|
// Current MIDI input device
|
|
let currentInput = null;
|
|
// Store current MIDI state
|
|
let midiState = {
|
|
cc: [], // CC controller state - will be used for both INT and FLOAT CC values
|
|
notes: [] // Note state - will be used for both BOOLEAN note values and INT note values
|
|
};
|
|
|
|
// Function to update widget visibility
|
|
const updateWidgetVisibility = () => {
|
|
if (debugWidget) {
|
|
if (debugWidget.value) {
|
|
showWidget(node, rawDataWidget);
|
|
} else {
|
|
hideWidget(node, rawDataWidget);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (debugWidget) {
|
|
debugWidget.callback = (value) => {
|
|
updateWidgetVisibility();
|
|
};
|
|
}
|
|
|
|
if (rawDataWidget) {
|
|
// Set initial visibility of raw data widget
|
|
hideWidget(node, rawDataWidget);
|
|
}
|
|
|
|
// Function to handle MIDI messages
|
|
const onMIDIMessage = (event) => {
|
|
try {
|
|
// MIDI message contains three bytes: [status byte, data byte 1, data byte 2]
|
|
const [statusByte, dataByte1, dataByte2] = event.data;
|
|
|
|
// Extract channel info and message type
|
|
const messageType = statusByte >> 4;
|
|
const channel = statusByte & 0xF;
|
|
|
|
if (debugWidget && debugWidget.value) {
|
|
console.log(`[MIDI] Message: type=${messageType}, channel=${channel}, data1=${dataByte1}, data2=${dataByte2}`);
|
|
}
|
|
|
|
switch (messageType) {
|
|
case 0x8: // Note Off
|
|
updateNoteStatus(dataByte1, 0, "noteOff");
|
|
break;
|
|
|
|
case 0x9: // Note On
|
|
updateNoteStatus(dataByte1, dataByte2, dataByte2 > 0 ? "noteOn" : "noteOff");
|
|
break;
|
|
|
|
case 0xB: // Control Change
|
|
updateCCValue(dataByte1, dataByte2);
|
|
break;
|
|
|
|
case 0xA: // Polyphonic Key Pressure (Aftertouch)
|
|
// Handle per-note aftertouch
|
|
updateAftertouch(dataByte1, dataByte2);
|
|
break;
|
|
|
|
case 0xC: // Program Change
|
|
// Handle program change
|
|
updateProgramChange(dataByte1);
|
|
break;
|
|
|
|
case 0xD: // Channel Pressure (Aftertouch)
|
|
// Handle channel-wide aftertouch
|
|
updateChannelPressure(dataByte1);
|
|
break;
|
|
|
|
case 0xE: // Pitch Bend
|
|
// Handle pitch bend
|
|
updatePitchBend(dataByte1, dataByte2);
|
|
break;
|
|
|
|
// Add more MIDI message type handlers as needed
|
|
}
|
|
|
|
// Update raw data widget
|
|
const json_data = JSON.stringify(midiState, null, 2);
|
|
if (json_data && rawDataWidget && rawDataWidget.value != json_data) {
|
|
// Only update if the data has changed
|
|
rawDataWidget.value = json_data;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error("[VrchMidiDeviceLoaderNode] Error processing MIDI message:", error);
|
|
}
|
|
};
|
|
|
|
// Update note status
|
|
const updateNoteStatus = (noteNumber, velocity, status) => {
|
|
// Find existing note record
|
|
const existingNoteIndex = midiState.notes.findIndex(note => note.number === noteNumber);
|
|
|
|
if (status === "noteOff" && velocity === 0) {
|
|
// If note off and velocity is 0, remove from array
|
|
if (existingNoteIndex !== -1) {
|
|
midiState.notes.splice(existingNoteIndex, 1);
|
|
}
|
|
} else {
|
|
// Update or add note info
|
|
const noteInfo = {
|
|
number: noteNumber,
|
|
velocity: velocity,
|
|
status: status
|
|
};
|
|
|
|
if (existingNoteIndex !== -1) {
|
|
midiState.notes[existingNoteIndex] = noteInfo;
|
|
} else {
|
|
midiState.notes.push(noteInfo);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Update CC controller value
|
|
const updateCCValue = (ccNumber, value) => {
|
|
// Find existing CC record
|
|
const existingCCIndex = midiState.cc.findIndex(cc => cc.number === ccNumber);
|
|
|
|
const ccInfo = {
|
|
number: ccNumber,
|
|
value: value
|
|
};
|
|
|
|
if (existingCCIndex !== -1) {
|
|
midiState.cc[existingCCIndex] = ccInfo;
|
|
} else {
|
|
midiState.cc.push(ccInfo);
|
|
}
|
|
};
|
|
|
|
// Handle polyphonic aftertouch
|
|
const updateAftertouch = (noteNumber, pressure) => {
|
|
// Store or process aftertouch data in the appropriate format
|
|
// Similar to notes but with pressure instead of velocity
|
|
const existingIndex = midiState.aftertouch?.findIndex(at => at.note === noteNumber) ?? -1;
|
|
|
|
if (!midiState.aftertouch) midiState.aftertouch = [];
|
|
|
|
const afterTouchInfo = {
|
|
note: noteNumber,
|
|
pressure: pressure
|
|
};
|
|
|
|
if (existingIndex !== -1) {
|
|
midiState.aftertouch[existingIndex] = afterTouchInfo;
|
|
} else {
|
|
midiState.aftertouch.push(afterTouchInfo);
|
|
}
|
|
};
|
|
|
|
// Handle program change
|
|
const updateProgramChange = (programNumber) => {
|
|
midiState.programChange = programNumber;
|
|
};
|
|
|
|
// Handle channel pressure (global aftertouch)
|
|
const updateChannelPressure = (pressure) => {
|
|
midiState.channelPressure = pressure;
|
|
};
|
|
|
|
// Handle pitch bend (combines two bytes for 14-bit resolution)
|
|
const updatePitchBend = (lsb, msb) => {
|
|
// Combine bytes to get the full pitch bend value (0-16383, centered at 8192)
|
|
const value = (msb << 7) + lsb;
|
|
midiState.pitchBend = value;
|
|
};
|
|
|
|
// Initialize MIDI access
|
|
const initMIDI = async () => {
|
|
try {
|
|
if (navigator.requestMIDIAccess) {
|
|
midiAccess = await navigator.requestMIDIAccess();
|
|
|
|
// List available MIDI devices
|
|
const inputs = [...midiAccess.inputs.values()];
|
|
|
|
if (debugWidget && debugWidget.value) {
|
|
console.log("[VrchMidiDeviceLoaderNode] Available MIDI devices:", inputs);
|
|
}
|
|
|
|
// If device ID exists, try to connect to specific device
|
|
if (deviceIdWidget.value) {
|
|
connectToDevice(deviceIdWidget.value);
|
|
} else if (inputs.length > 0) {
|
|
// Otherwise connect to first available device
|
|
connectToDevice(inputs[0].id);
|
|
deviceIdWidget.value = inputs[0].id;
|
|
} else if (inputs.length === 0) {
|
|
// If no devices are available, set device ID to empty
|
|
deviceIdWidget.value = "";
|
|
nameWidget.value = "";
|
|
updateStatusLabel("No devices available");
|
|
}
|
|
|
|
// Set up MIDI connection state change listener
|
|
midiAccess.onstatechange = (event) => {
|
|
if (debugWidget && debugWidget.value) {
|
|
console.log("[VrchMidiDeviceLoaderNode] MIDI connection state change:", event);
|
|
}
|
|
|
|
// If device is disconnected, clear device ID and name
|
|
if (deviceIdWidget.value === event.port.id && event.port.state === "disconnected") {
|
|
deviceIdWidget.value = "";
|
|
nameWidget.value = "";
|
|
updateStatusLabel("Device disconnected");
|
|
}
|
|
|
|
// If device reconnects, try to reconnect
|
|
if (event.port.type === "input" && event.port.state === "connected") {
|
|
if (deviceIdWidget.value === event.port.id || !currentInput) {
|
|
connectToDevice(event.port.id);
|
|
}
|
|
}
|
|
};
|
|
} else {
|
|
console.warn("[VrchMidiDeviceLoaderNode] Web MIDI API not supported in this browser");
|
|
}
|
|
} catch (error) {
|
|
console.error("[VrchMidiDeviceLoaderNode] Error initializing MIDI:", error);
|
|
}
|
|
};
|
|
|
|
// Modify connectToDevice to include status label updates directly
|
|
function connectToDevice(deviceId) {
|
|
try {
|
|
// Disconnect current connection
|
|
if (currentInput) {
|
|
currentInput.onmidimessage = null;
|
|
}
|
|
|
|
// Find and connect to new device
|
|
const device = midiAccess.inputs.get(deviceId);
|
|
|
|
if (device) {
|
|
device.onmidimessage = onMIDIMessage;
|
|
currentInput = device;
|
|
nameWidget.value = device.name || deviceId;
|
|
deviceIdWidget.value = deviceId;
|
|
|
|
if (debugWidget && debugWidget.value) {
|
|
console.log(`[VrchMidiDeviceLoaderNode] Connected to MIDI device: ${device.name || deviceId}`);
|
|
}
|
|
|
|
// Update status label
|
|
updateStatusLabel(`Connected to ${device.name || deviceId}`);
|
|
} else {
|
|
if (debugWidget && debugWidget.value) {
|
|
console.warn(`[VrchMidiDeviceLoaderNode] MIDI device not found: ${deviceId}`);
|
|
}
|
|
nameWidget.value = "Device not found";
|
|
currentInput = null;
|
|
|
|
// Update status label
|
|
updateStatusLabel("Device not found");
|
|
}
|
|
} catch (error) {
|
|
console.error("[VrchMidiDeviceLoaderNode] Error connecting to MIDI device:", error);
|
|
// Update status label
|
|
updateStatusLabel("Error connecting to device");
|
|
}
|
|
}
|
|
|
|
// Device ID widget callback
|
|
if (deviceIdWidget) {
|
|
deviceIdWidget.callback = () => {
|
|
if (midiAccess && deviceIdWidget.value) {
|
|
connectToDevice(deviceIdWidget.value);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Create a container for the Reload button and status label
|
|
const controlContainer = document.createElement("div");
|
|
|
|
// Create the Reload button
|
|
const reloadButton = document.createElement("button");
|
|
reloadButton.textContent = "Reload MIDI Devices";
|
|
reloadButton.classList.add("vrch-midi-reload-button");
|
|
reloadButton.onclick = () => {
|
|
console.log("Reloading MIDI devices...");
|
|
deviceIdWidget.value = "";
|
|
nameWidget.value = "";
|
|
initMIDI();
|
|
};
|
|
|
|
// Create the status label
|
|
const statusLabel = document.createElement("div");
|
|
statusLabel.classList.add("vrch-midi-status-label");
|
|
statusLabel.textContent = "Not connected";
|
|
|
|
// Update the status label based on connection state
|
|
const updateStatusLabel = (status) => {
|
|
statusLabel.textContent = `${status}`;
|
|
};
|
|
|
|
// Append the Reload button and status label to the control container
|
|
controlContainer.appendChild(reloadButton);
|
|
controlContainer.appendChild(statusLabel);
|
|
|
|
// Add the control container as a component to the node
|
|
node.addDOMWidget("midi_control_widget", "MIDI Control", controlContainer);
|
|
|
|
// Initialize MIDI
|
|
initMIDI();
|
|
|
|
// Set delayed initialization for widget visibility
|
|
setTimeout(() => {
|
|
updateWidgetVisibility();
|
|
}, 1000);
|
|
|
|
// Cleanup function, called when node is removed
|
|
const onRemoved = this.onRemoved;
|
|
this.onRemoved = function () {
|
|
if (currentInput) {
|
|
currentInput.onmidimessage = null;
|
|
currentInput = null;
|
|
}
|
|
return onRemoved?.();
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
// Inject stylesheet for MIDI nodes
|
|
const midiStyle = document.createElement("style");
|
|
midiStyle.textContent = `
|
|
.vrch-midi-reload-button {
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
border: none;
|
|
border-radius: 8px;
|
|
width: 100%;
|
|
height: 40px;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
transition: background-color 0.3s, transform 0.2s;
|
|
padding: 8px 16px;
|
|
}
|
|
|
|
.vrch-midi-reload-button:hover {
|
|
background-color: #45a049;
|
|
}
|
|
|
|
.vrch-midi-reload-button:active {
|
|
background-color: #3e8e41;
|
|
}
|
|
|
|
.vrch-midi-status-label {
|
|
font-size: 14px;
|
|
color: #666;
|
|
text-align: center;
|
|
margin-top: 10px;
|
|
}
|
|
`;
|
|
document.head.appendChild(midiStyle); |