From e1439623916b4ee5f118e2f8398493d4d5b1ba2b Mon Sep 17 00:00:00 2001 From: FloppyDisk_OSC Date: Tue, 3 Feb 2026 15:48:37 -0500 Subject: [PATCH 1/3] Update MIDI.js --- static/extensions/electricfuzzball_pm/MIDI.js | 388 +++++++++--------- 1 file changed, 198 insertions(+), 190 deletions(-) diff --git a/static/extensions/electricfuzzball_pm/MIDI.js b/static/extensions/electricfuzzball_pm/MIDI.js index 318fadfce..a4f161723 100644 --- a/static/extensions/electricfuzzball_pm/MIDI.js +++ b/static/extensions/electricfuzzball_pm/MIDI.js @@ -1,3 +1,5 @@ +/* global Scratch, jwArray */ + // jwArray placeholder let jwArray = { Type: class {}, @@ -5,216 +7,222 @@ let jwArray = { Argument: {} }; -(function (Scratch) { - 'use strict'; - - class MIDIExtension { - constructor() { - // Inject jwArray - if (!Scratch.vm.jwArray) Scratch.vm.extensionManager.loadExtensionIdSync('jwArray'); - jwArray = Scratch.vm.jwArray; - - this.midiAccess = null; - this.inputs = []; - this.listening = false; - - this.noteMap = { - 60: 'kick', - 62: 'snare', - 64: 'hihat', - 65: 'clap', - 36: 'pad1', - 37: 'pad2', - 38: 'pad3', - 39: 'pad4', - 40: 'pad5', - 41: 'pad6', - 42: 'pad7', - 43: 'pad8' - }; - - this._currentNote = 0; - this._currentVelocity = 0; - this._currentPad = 0; - this._currentPadVelocity = 0; - - this._notesPressed = new Set(); - - this.visualizerEl = null; - } +class MIDI { + constructor() { + if (!Scratch.vm.jwArray) Scratch.vm.extensionManager.loadExtensionIdSync('jwArray'); + jwArray = Scratch.vm.jwArray; + + this.activeNotes = new Map(); + this.activePads = new Map(); + + this.lastNote = ''; + this.lastVelocity = 0; + this.lastChannel = -1; + this.lastPad = ''; + this.lastPadVelocity = 0; + + this.visualizerEl = null; + this.inputs = []; - getInfo() { - return { - id: 'midi', - name: 'MIDI', - color1: '#960000', - iconURI: 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIyOC41OTE4MyIgaGVpZ2h0PSIyNS4zODk0NCIgdmlld0JveD0iMCwwLDI4LjU5MTgzLDI1LjM4OTQ0Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjI1LjcwNDA4LC0xNjcuMzA1MjgpIj48ZyBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggZD0iTTIyNi4yMDQwOCwxNzQuMzk1MDl2LTYuNTg5ODFoMjcuNTkxODN2Ni41ODk4MXoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMjI3LjY0NDY0LDE5Mi4xOTQ3MmMtMC43OTU2LDAgLTEuNDQwNTUsLTAuODE3NTggLTEuNDQwNTUsLTEuODI2MTF2LTE1Ljk3MzUzaDYuODk3OTZ2MTUuOTczNTNjMCwxLjAwODUzIC0wLjY0NDk2LDEuODI2MTEgLTEuNDQwNTUsMS44MjYxMXoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMjM0LjU0MjYsMTkyLjE5NDcyYy0wLjc5NTYsMCAtMS40NDA1NSwtMC44MTc1OCAtMS40NDA1NSwtMS44MjYxMXYtMTUuOTczNTNoNi44OTc5NnYxNS45NzM1M2MwLDEuMDA4NTMgLTAuNjQ0OTYsMS44MjYxMSAtMS40NDA1NSwxLjgyNjExeiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0yNDEuNDQwNTUsMTkyLjE5NDcyYy0wLjc5NTYsMCAtMS40NDA1NSwtMC44MTc1OCAtMS40NDA1NSwtMS44MjYxMXYtMTUuOTczNTNoNi44OTc5NnYxNS45NzM1M2MwLDEuMDA4NTMgLTAuNjQ0OTYsMS44MjYxMSAtMS40NDA1NSwxLjgyNjExeiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0yNDguMzM4NTEsMTkyLjE5NDcyYy0wLjc5NTYsMCAtMS40NDA1NiwtMC44MTc1OCAtMS40NDA1NiwtMS44MjYxMXYtMTUuOTczNTNoNi44OTc5NnYxNS45NzM1M2MwLDEuMDA4NTMgLTAuNjQ0OTYsMS44MjYxMSAtMS40NDA1NSwxLjgyNjExeiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0yMzEuMDgxMTgsMTgzLjU3NDYydi05LjE3OTUzaDQuMDQxNzN2OS4xNzk1M3oiIGZpbGw9IiNmZmZmZmYiLz48cGF0aCBkPSJNMjQ0Ljg3NzA5LDE4My41NzQ2MnYtOS4xNzk1NGg0LjA0MTczdjkuMTc5NTR6IiBmaWxsPSIjZmZmZmZmIi8+PHBhdGggZD0iTTIzNy45NzkxMywxODMuNTc0NjJ2LTkuMTc5NTRoNC4wNDE3M3Y5LjE3OTU0eiIgZmlsbD0iI2ZmZmZmZiIvPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjE0LjI5NTkxNjcyMDYzODc1ODoxMi42OTQ3MjIyMTgzNDU1MDQtLT4=', - blocks: [ - { opcode: 'startListening', blockType: Scratch.BlockType.COMMAND, text: 'start MIDI listener' }, - { opcode: 'stopListening', blockType: Scratch.BlockType.COMMAND, text: 'stop MIDI listener' }, - { - opcode: 'setNote', - blockType: Scratch.BlockType.COMMAND, - text: 'map note [NOTE] to event [EVENT]', - arguments: { - NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 }, - EVENT: { type: Scratch.ArgumentType.STRING, defaultValue: 'kick' } - } - }, - { opcode: 'currentNote', blockType: Scratch.BlockType.REPORTER, text: 'current note number' }, - { opcode: 'currentVelocity', blockType: Scratch.BlockType.REPORTER, text: 'current note velocity' }, - { opcode: 'currentPad', blockType: Scratch.BlockType.REPORTER, text: 'current pad ID' }, - { opcode: 'currentPadVelocity', blockType: Scratch.BlockType.REPORTER, text: 'current pad velocity' }, - { - opcode: 'notesPressed', - blockType: Scratch.BlockType.REPORTER, - text: 'notes currently pressed', - ...jwArray.Block - }, - { opcode: 'showVisualizer', blockType: Scratch.BlockType.COMMAND, text: 'show visualizer' }, - { opcode: 'hideVisualizer', blockType: Scratch.BlockType.COMMAND, text: 'hide visualizer' } - ] - }; + if (navigator.requestMIDIAccess) { + navigator.requestMIDIAccess().then(access => { + this._onMIDISuccess(access); + }, err => { + console.error('MIDI failed', err); + }); } + } - async startListening() { - if (this.listening) return; - this.listening = true; + getInfo() { + return { + id: 'midi', + name: 'MIDI', + color1: '#960000', + color2: '#960000', + blocks: [ + { opcode: 'startListening', blockType: Scratch.BlockType.COMMAND, text: 'start MIDI listener' }, + { opcode: 'stopListening', blockType: Scratch.BlockType.COMMAND, text: 'stop MIDI listener' }, + { opcode: 'setNote', blockType: Scratch.BlockType.COMMAND, text: 'map note [NOTE] to event [EVENT]', + arguments: { + NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 }, + EVENT: { type: Scratch.ArgumentType.STRING, defaultValue: 'kick' } + } + }, + { opcode: 'currentNote', blockType: Scratch.BlockType.REPORTER, text: 'current note number' }, + { opcode: 'currentVelocity', blockType: Scratch.BlockType.REPORTER, text: 'current note velocity' }, + { opcode: 'currentChannel', blockType: Scratch.BlockType.REPORTER, text: 'current channel' }, + { opcode: 'currentPad', blockType: Scratch.BlockType.REPORTER, text: 'current pad ID' }, + { opcode: 'currentPadVelocity', blockType: Scratch.BlockType.REPORTER, text: 'current pad velocity' }, + { opcode: 'notesPressed', blockType: Scratch.BlockType.REPORTER, text: 'notes currently pressed', ...jwArray.Block }, + { opcode: 'notesByChannel', blockType: Scratch.BlockType.REPORTER, text: 'notes by channel', ...jwArray.Block }, + { opcode: 'notesOnChannel', blockType: Scratch.BlockType.REPORTER, text: 'notes on channel [CHANNEL]', + arguments: { CHANNEL: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 } }, + ...jwArray.Block + }, + { opcode: 'noteNumberToName', blockType: Scratch.BlockType.REPORTER, text: 'note name of [NOTE]', + arguments: { NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 } } + }, + { opcode: 'showVisualizer', blockType: Scratch.BlockType.COMMAND, text: 'show visualizer' }, + { opcode: 'hideVisualizer', blockType: Scratch.BlockType.COMMAND, text: 'hide visualizer' } + ] + }; + } - if (!navigator.requestMIDIAccess) { - console.log('MIDI not supported in this browser!'); - return; - } + /* ======= MIDI Setup ======= */ + _onMIDISuccess(access) { + this.inputs = Array.from(access.inputs.values()); + this.inputs.forEach(input => input.onmidimessage = e => this._onMIDIMessage(e)); + } - this.midiAccess = await navigator.requestMIDIAccess(); - this.inputs = Array.from(this.midiAccess.inputs.values()); - this.inputs.forEach(input => input.onmidimessage = this.handleMIDIMessage.bind(this)); - console.log('MIDI listener started!'); + _onMIDIMessage(event) { + const [status, note, velocity] = event.data; + const command = status & 0xf0; + const channel = (status & 0x0f) + 1; + const isPad = (note >= 36 && note <= 51); // MPK Mini pads + + if (command === 0x90 && velocity > 0) { + if (isPad) this._padOn(channel, note, velocity); + else this._noteOn(channel, note, velocity); + } else if (command === 0x80 || (command === 0x90 && velocity === 0)) { + if (isPad) this._padOff(channel, note); + else this._noteOff(channel, note); } + } + + _noteOn(channel, note, velocity) { + if (!this.activeNotes.has(channel)) this.activeNotes.set(channel, new Map()); + this.activeNotes.get(channel).set(note, velocity); + this.lastNote = note; + this.lastVelocity = velocity; + this.lastChannel = channel; + this.updateVisualizer(); + } - stopListening() { - this.listening = false; - if (this.inputs.length > 0) { - this.inputs.forEach(input => input.onmidimessage = null); - } - this._notesPressed.clear(); - this.updateVisualizer(); - console.log('MIDI listener stopped!'); + _noteOff(channel, note) { + if (this.activeNotes.has(channel)) { + this.activeNotes.get(channel).delete(note); + if (this.activeNotes.get(channel).size === 0) this.activeNotes.delete(channel); } - setNote(args) { - const note = args.NOTE; - const event = args.EVENT; - this.noteMap[note] = event; - console.log(`Mapped note ${note} → event "${event}"`); + if (!this.activeNotes.has(channel) || this.activeNotes.get(channel).size === 0) { + this.lastNote = ''; } - handleMIDIMessage(event) { - if (!this.listening) return; - - const [status, noteValue, velocityValue] = event.data; - const isNoteOn = (status & 0xF0) === 0x90 && velocityValue > 0; - const isNoteOff = ((status & 0xF0) === 0x80) || ((status & 0xF0) === 0x90 && velocityValue === 0); - - if (isNoteOn) { - const isPad = (noteValue >= 36 && noteValue <= 51); - if (isPad) { - this._currentPad = noteValue; - this._currentPadVelocity = velocityValue; - } else { - this._currentNote = noteValue; - this._currentVelocity = velocityValue; - } - - this._notesPressed.add(noteValue); - - const mappedEvent = this.noteMap[noteValue]; - if (mappedEvent) { - Scratch.vm.runtime.emit('MIDI_NOTE', { - note: noteValue, - velocity: velocityValue, - event: mappedEvent, - isPad: isPad - }); - } - } - - if (isNoteOff) { - this._notesPressed.delete(noteValue); - } - - this.updateVisualizer(); + this.lastVelocity = 0; + this.lastChannel = channel; + this.updateVisualizer(); + } + + _padOn(channel, note, velocity) { + if (!this.activePads.has(channel)) this.activePads.set(channel, new Map()); + this.activePads.get(channel).set(note, velocity); + this.lastPad = note; + this.lastPadVelocity = velocity; + this.updateVisualizer(); + } + + _padOff(channel, note) { + if (this.activePads.has(channel)) { + this.activePads.get(channel).delete(note); + if (this.activePads.get(channel).size === 0) this.activePads.delete(channel); } - noteNumberToName(number) { - const names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; - const octave = Math.floor(number / 12) - 1; - const name = names[number % 12]; - return `${name}${octave}`; + if (!this.activePads.has(channel) || this.activePads.get(channel).size === 0) { + this.lastPad = ''; } - showVisualizer() { - if (this.visualizerEl) return; - this.visualizerEl = document.createElement('div'); - this.visualizerEl.style.position = 'fixed'; - this.visualizerEl.style.top = '50px'; - this.visualizerEl.style.left = '50px'; - this.visualizerEl.style.background = '#222'; - this.visualizerEl.style.color = '#fff'; - this.visualizerEl.style.padding = '10px'; - this.visualizerEl.style.borderRadius = '8px'; - this.visualizerEl.style.fontFamily = 'monospace'; - this.visualizerEl.style.zIndex = 9999; - this.visualizerEl.style.cursor = 'move'; - this.visualizerEl.innerText = 'Notes: []'; - document.body.appendChild(this.visualizerEl); - - let isDragging = false; - let offsetX = 0; - let offsetY = 0; - - this.visualizerEl.addEventListener('mousedown', (e) => { - isDragging = true; - offsetX = e.clientX - this.visualizerEl.offsetLeft; - offsetY = e.clientY - this.visualizerEl.offsetTop; - }); + this.lastPadVelocity = 0; + this.updateVisualizer(); + } - document.addEventListener('mousemove', (e) => { - if (isDragging) { - this.visualizerEl.style.left = (e.clientX - offsetX) + 'px'; - this.visualizerEl.style.top = (e.clientY - offsetY) + 'px'; - } - }); + /* ======= Listeners ======= */ + startListening() { this.inputs.forEach(input => input.onmidimessage = e => this._onMIDIMessage(e)); } + stopListening() { + this.inputs.forEach(input => input.onmidimessage = null); + this.activeNotes.clear(); + this.activePads.clear(); + this.lastNote = ''; + this.lastVelocity = 0; + this.lastChannel = -1; + this.lastPad = ''; + this.lastPadVelocity = 0; + this.updateVisualizer(); + } - document.addEventListener('mouseup', () => { - isDragging = false; - }); + setNote(args) { console.log(`Map note ${args.NOTE} → ${args.EVENT}`); } + + /* ======= Visualizer ======= */ + showVisualizer() { + if (this.visualizerEl) return; + this.visualizerEl = document.createElement('div'); + this.visualizerEl.style.position = 'fixed'; + this.visualizerEl.style.top = '50px'; + this.visualizerEl.style.left = '50px'; + this.visualizerEl.style.background = '#222'; + this.visualizerEl.style.color = '#fff'; + this.visualizerEl.style.padding = '10px'; + this.visualizerEl.style.borderRadius = '8px'; + this.visualizerEl.style.fontFamily = 'monospace'; + this.visualizerEl.style.zIndex = 9999; + this.visualizerEl.style.cursor = 'move'; + this.visualizerEl.innerText = 'Notes: []'; + document.body.appendChild(this.visualizerEl); + + let dragging = false, offsetX = 0, offsetY = 0; + this.visualizerEl.addEventListener('mousedown', e => { dragging=true; offsetX=e.clientX-this.visualizerEl.offsetLeft; offsetY=e.clientY-this.visualizerEl.offsetTop; }); + document.addEventListener('mousemove', e => { if(dragging){ this.visualizerEl.style.left=(e.clientX-offsetX)+'px'; this.visualizerEl.style.top=(e.clientY-offsetY)+'px'; } }); + document.addEventListener('mouseup', () => dragging=false); + this.updateVisualizer(); + } - this.updateVisualizer(); - } + hideVisualizer() { if(this.visualizerEl){ document.body.removeChild(this.visualizerEl); this.visualizerEl=null; } } - hideVisualizer() { - if (this.visualizerEl) { - document.body.removeChild(this.visualizerEl); - this.visualizerEl = null; - } - } + updateVisualizer() { + if(!this.visualizerEl) return; + const arr = []; + for(const [ch, notes] of this.activeNotes.entries()){ for(const note of notes.keys()) arr.push(`N Ch${ch}:${note}`); } + for(const [ch, pads] of this.activePads.entries()){ for(const pad of pads.keys()) arr.push(`P Ch${ch}:${pad}`); } + this.visualizerEl.innerText = `Notes: [${arr.join(', ')}]`; + } - updateVisualizer() { - if (!this.visualizerEl) return; - const names = Array.from(this._notesPressed).map(n => this.noteNumberToName(n)); - this.visualizerEl.innerText = `Notes: [${names.join(', ')}]`; - } + /* ======= REPORTERS ======= */ + currentNote() { return this.lastNote; } + currentVelocity() { return this.lastVelocity; } + currentChannel() { return this.lastChannel; } + currentPad() { return this.lastPad; } + currentPadVelocity() { return this.lastPadVelocity; } + + notesPressed() { + const arr = []; + for(const notes of this.activeNotes.values()){ for(const note of notes.keys()) arr.push(note); } + for(const pads of this.activePads.values()){ for(const pad of pads.keys()) arr.push(pad); } + return new jwArray.Type(arr); + } - currentNote() { return this._currentNote; } - currentVelocity() { return this._currentVelocity; } - currentPad() { return this._currentPad; } - currentPadVelocity() { return this._currentPadVelocity; } - notesPressed() { - // Return formatted as jwArray type - return new jwArray.Type(Array.from(this._notesPressed)); - } + notesByChannel() { + const arr = []; + for(const [ch, notes] of this.activeNotes.entries()){ for(const note of notes.keys()) arr.push(note); } + for(const [ch, pads] of this.activePads.entries()){ for(const pad of pads.keys()) arr.push(pad); } + return new jwArray.Type(arr); + } + + notesOnChannel(args) { + const channel = args.CHANNEL; + const arr = []; + if(this.activeNotes.has(channel)) arr.push(...this.activeNotes.get(channel).keys()); + if(this.activePads.has(channel)) arr.push(...this.activePads.get(channel).keys()); + return new jwArray.Type(arr); + } + + noteNumberToName(args) { + const noteNumber = args.NOTE; + if (!Number.isInteger(noteNumber) || noteNumber < 0 || noteNumber > 127) return ''; + const names = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']; + const octave = Math.floor(noteNumber / 12) - 1; + return names[noteNumber % 12] + octave; } +} + +Scratch.extensions.register(new MIDI()); - Scratch.extensions.register(new MIDIExtension()); -})(Scratch); +// Hello ddededodediamante From 9dd30ca844dfb6b182c880a8fbca8889f589e0d0 Mon Sep 17 00:00:00 2001 From: ElectricFuzzball Date: Wed, 11 Feb 2026 20:15:03 -0500 Subject: [PATCH 2/3] Update MIDI.js --- static/extensions/electricfuzzball_pm/MIDI.js | 342 +++++++++++++----- 1 file changed, 257 insertions(+), 85 deletions(-) diff --git a/static/extensions/electricfuzzball_pm/MIDI.js b/static/extensions/electricfuzzball_pm/MIDI.js index a4f161723..00738efa7 100644 --- a/static/extensions/electricfuzzball_pm/MIDI.js +++ b/static/extensions/electricfuzzball_pm/MIDI.js @@ -9,61 +9,88 @@ let jwArray = { class MIDI { constructor() { - if (!Scratch.vm.jwArray) Scratch.vm.extensionManager.loadExtensionIdSync('jwArray'); + if (!Scratch.vm.jwArray) { + Scratch.vm.extensionManager.loadExtensionIdSync("jwArray"); + } + jwArray = Scratch.vm.jwArray; this.activeNotes = new Map(); this.activePads = new Map(); - this.lastNote = ''; + this.lastNote = ""; this.lastVelocity = 0; this.lastChannel = -1; - this.lastPad = ''; + + this.lastPad = ""; this.lastPadVelocity = 0; this.visualizerEl = null; this.inputs = []; if (navigator.requestMIDIAccess) { - navigator.requestMIDIAccess().then(access => { - this._onMIDISuccess(access); - }, err => { - console.error('MIDI failed', err); - }); + navigator.requestMIDIAccess().then( + (access) => { + this._onMIDISuccess(access); + }, + (err) => { + console.error("MIDI failed", err); + } + ); } } getInfo() { return { - id: 'midi', - name: 'MIDI', - color1: '#960000', - color2: '#960000', + id: "midi", + name: "MIDI", + color1: "#960000", + color2: "#960000", blocks: [ - { opcode: 'startListening', blockType: Scratch.BlockType.COMMAND, text: 'start MIDI listener' }, - { opcode: 'stopListening', blockType: Scratch.BlockType.COMMAND, text: 'stop MIDI listener' }, - { opcode: 'setNote', blockType: Scratch.BlockType.COMMAND, text: 'map note [NOTE] to event [EVENT]', - arguments: { - NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 }, - EVENT: { type: Scratch.ArgumentType.STRING, defaultValue: 'kick' } - } + { opcode: "startListening", blockType: Scratch.BlockType.COMMAND, text: "start MIDI listener" }, + { opcode: "stopListening", blockType: Scratch.BlockType.COMMAND, text: "stop MIDI listener" }, + + { + opcode: "setNote", + blockType: Scratch.BlockType.COMMAND, + text: "map note [NOTE] to event [EVENT]", + arguments: { + NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 }, + EVENT: { type: Scratch.ArgumentType.STRING, defaultValue: "kick" } + } }, - { opcode: 'currentNote', blockType: Scratch.BlockType.REPORTER, text: 'current note number' }, - { opcode: 'currentVelocity', blockType: Scratch.BlockType.REPORTER, text: 'current note velocity' }, - { opcode: 'currentChannel', blockType: Scratch.BlockType.REPORTER, text: 'current channel' }, - { opcode: 'currentPad', blockType: Scratch.BlockType.REPORTER, text: 'current pad ID' }, - { opcode: 'currentPadVelocity', blockType: Scratch.BlockType.REPORTER, text: 'current pad velocity' }, - { opcode: 'notesPressed', blockType: Scratch.BlockType.REPORTER, text: 'notes currently pressed', ...jwArray.Block }, - { opcode: 'notesByChannel', blockType: Scratch.BlockType.REPORTER, text: 'notes by channel', ...jwArray.Block }, - { opcode: 'notesOnChannel', blockType: Scratch.BlockType.REPORTER, text: 'notes on channel [CHANNEL]', - arguments: { CHANNEL: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 } }, - ...jwArray.Block + + { opcode: "currentNote", blockType: Scratch.BlockType.REPORTER, text: "current note number" }, + { opcode: "currentVelocity", blockType: Scratch.BlockType.REPORTER, text: "current note velocity" }, + { opcode: "currentChannel", blockType: Scratch.BlockType.REPORTER, text: "current channel" }, + + { opcode: "currentPad", blockType: Scratch.BlockType.REPORTER, text: "current pad ID" }, + { opcode: "currentPadVelocity", blockType: Scratch.BlockType.REPORTER, text: "current pad velocity" }, + + { opcode: "notesPressed", blockType: Scratch.BlockType.REPORTER, text: "notes currently pressed", ...jwArray.Block }, + { opcode: "notesByChannel", blockType: Scratch.BlockType.REPORTER, text: "notes by channel", ...jwArray.Block }, + + { + opcode: "notesOnChannel", + blockType: Scratch.BlockType.REPORTER, + text: "notes on channel [CHANNEL]", + arguments: { + CHANNEL: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 } + }, + ...jwArray.Block }, - { opcode: 'noteNumberToName', blockType: Scratch.BlockType.REPORTER, text: 'note name of [NOTE]', - arguments: { NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 } } + + { + opcode: "noteNumberToName", + blockType: Scratch.BlockType.REPORTER, + text: "note name of [NOTE]", + arguments: { + NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 } + } }, - { opcode: 'showVisualizer', blockType: Scratch.BlockType.COMMAND, text: 'show visualizer' }, - { opcode: 'hideVisualizer', blockType: Scratch.BlockType.COMMAND, text: 'hide visualizer' } + + { opcode: "showVisualizer", blockType: Scratch.BlockType.COMMAND, text: "show visualizer" }, + { opcode: "hideVisualizer", blockType: Scratch.BlockType.COMMAND, text: "hide visualizer" } ] }; } @@ -71,154 +98,298 @@ class MIDI { /* ======= MIDI Setup ======= */ _onMIDISuccess(access) { this.inputs = Array.from(access.inputs.values()); - this.inputs.forEach(input => input.onmidimessage = e => this._onMIDIMessage(e)); + + // Just reuse the listener function instead of duplicating logic + this.startListening(); } _onMIDIMessage(event) { const [status, note, velocity] = event.data; + + // MIDI constants (no magic numbers) + const MIDI_NOTE_ON = 0x90; + const MIDI_NOTE_OFF = 0x80; + + const PAD_NOTE_MIN = 36; + const PAD_NOTE_MAX = 51; + const command = status & 0xf0; const channel = (status & 0x0f) + 1; - const isPad = (note >= 36 && note <= 51); // MPK Mini pads - if (command === 0x90 && velocity > 0) { - if (isPad) this._padOn(channel, note, velocity); - else this._noteOn(channel, note, velocity); - } else if (command === 0x80 || (command === 0x90 && velocity === 0)) { - if (isPad) this._padOff(channel, note); - else this._noteOff(channel, note); + const isPad = note >= PAD_NOTE_MIN && note <= PAD_NOTE_MAX; + + const isNoteOn = command === MIDI_NOTE_ON && velocity > 0; + const isNoteOff = command === MIDI_NOTE_OFF || (command === MIDI_NOTE_ON && velocity === 0); + + if (isNoteOn) { + if (isPad) { + this._padOn(channel, note, velocity); + } else { + this._noteOn(channel, note, velocity); + } + } + + if (isNoteOff) { + if (isPad) { + this._padOff(channel, note); + } else { + this._noteOff(channel, note); + } } } _noteOn(channel, note, velocity) { - if (!this.activeNotes.has(channel)) this.activeNotes.set(channel, new Map()); + if (!this.activeNotes.has(channel)) { + this.activeNotes.set(channel, new Map()); + } + this.activeNotes.get(channel).set(note, velocity); + this.lastNote = note; this.lastVelocity = velocity; this.lastChannel = channel; + this.updateVisualizer(); } _noteOff(channel, note) { if (this.activeNotes.has(channel)) { this.activeNotes.get(channel).delete(note); - if (this.activeNotes.get(channel).size === 0) this.activeNotes.delete(channel); + + if (this.activeNotes.get(channel).size === 0) { + this.activeNotes.delete(channel); + } } if (!this.activeNotes.has(channel) || this.activeNotes.get(channel).size === 0) { - this.lastNote = ''; + this.lastNote = ""; } this.lastVelocity = 0; this.lastChannel = channel; + this.updateVisualizer(); } _padOn(channel, note, velocity) { - if (!this.activePads.has(channel)) this.activePads.set(channel, new Map()); + if (!this.activePads.has(channel)) { + this.activePads.set(channel, new Map()); + } + this.activePads.get(channel).set(note, velocity); + this.lastPad = note; this.lastPadVelocity = velocity; + this.updateVisualizer(); } _padOff(channel, note) { if (this.activePads.has(channel)) { this.activePads.get(channel).delete(note); - if (this.activePads.get(channel).size === 0) this.activePads.delete(channel); + + if (this.activePads.get(channel).size === 0) { + this.activePads.delete(channel); + } } if (!this.activePads.has(channel) || this.activePads.get(channel).size === 0) { - this.lastPad = ''; + this.lastPad = ""; } this.lastPadVelocity = 0; + this.updateVisualizer(); } /* ======= Listeners ======= */ - startListening() { this.inputs.forEach(input => input.onmidimessage = e => this._onMIDIMessage(e)); } + startListening() { + this.inputs.forEach((input) => { + input.onmidimessage = (event) => { + this._onMIDIMessage(event); + }; + }); + } + stopListening() { - this.inputs.forEach(input => input.onmidimessage = null); + this.inputs.forEach((input) => { + input.onmidimessage = null; + }); + this.activeNotes.clear(); this.activePads.clear(); - this.lastNote = ''; + + this.lastNote = ""; this.lastVelocity = 0; this.lastChannel = -1; - this.lastPad = ''; + + this.lastPad = ""; this.lastPadVelocity = 0; + this.updateVisualizer(); } - setNote(args) { console.log(`Map note ${args.NOTE} → ${args.EVENT}`); } + setNote(args) { + console.log(`Map note ${args.NOTE} → ${args.EVENT}`); + } /* ======= Visualizer ======= */ showVisualizer() { if (this.visualizerEl) return; - this.visualizerEl = document.createElement('div'); - this.visualizerEl.style.position = 'fixed'; - this.visualizerEl.style.top = '50px'; - this.visualizerEl.style.left = '50px'; - this.visualizerEl.style.background = '#222'; - this.visualizerEl.style.color = '#fff'; - this.visualizerEl.style.padding = '10px'; - this.visualizerEl.style.borderRadius = '8px'; - this.visualizerEl.style.fontFamily = 'monospace'; + + this.visualizerEl = document.createElement("div"); + + this.visualizerEl.style.position = "fixed"; + this.visualizerEl.style.top = "50px"; + this.visualizerEl.style.left = "50px"; + + this.visualizerEl.style.background = "#222"; + this.visualizerEl.style.color = "#fff"; + + this.visualizerEl.style.padding = "10px"; + this.visualizerEl.style.borderRadius = "8px"; + + this.visualizerEl.style.fontFamily = "monospace"; this.visualizerEl.style.zIndex = 9999; - this.visualizerEl.style.cursor = 'move'; - this.visualizerEl.innerText = 'Notes: []'; + + this.visualizerEl.style.cursor = "move"; + this.visualizerEl.innerText = "Notes: []"; + document.body.appendChild(this.visualizerEl); - let dragging = false, offsetX = 0, offsetY = 0; - this.visualizerEl.addEventListener('mousedown', e => { dragging=true; offsetX=e.clientX-this.visualizerEl.offsetLeft; offsetY=e.clientY-this.visualizerEl.offsetTop; }); - document.addEventListener('mousemove', e => { if(dragging){ this.visualizerEl.style.left=(e.clientX-offsetX)+'px'; this.visualizerEl.style.top=(e.clientY-offsetY)+'px'; } }); - document.addEventListener('mouseup', () => dragging=false); + let dragging = false; + let offsetX = 0; + let offsetY = 0; + + this.visualizerEl.addEventListener("mousedown", (e) => { + dragging = true; + offsetX = e.clientX - this.visualizerEl.offsetLeft; + offsetY = e.clientY - this.visualizerEl.offsetTop; + }); + + document.addEventListener("mousemove", (e) => { + if (!dragging) return; + + this.visualizerEl.style.left = e.clientX - offsetX + "px"; + this.visualizerEl.style.top = e.clientY - offsetY + "px"; + }); + + document.addEventListener("mouseup", () => { + dragging = false; + }); + this.updateVisualizer(); } - hideVisualizer() { if(this.visualizerEl){ document.body.removeChild(this.visualizerEl); this.visualizerEl=null; } } + hideVisualizer() { + if (this.visualizerEl) { + document.body.removeChild(this.visualizerEl); + this.visualizerEl = null; + } + } updateVisualizer() { - if(!this.visualizerEl) return; + if (!this.visualizerEl) return; + const arr = []; - for(const [ch, notes] of this.activeNotes.entries()){ for(const note of notes.keys()) arr.push(`N Ch${ch}:${note}`); } - for(const [ch, pads] of this.activePads.entries()){ for(const pad of pads.keys()) arr.push(`P Ch${ch}:${pad}`); } - this.visualizerEl.innerText = `Notes: [${arr.join(', ')}]`; + + for (const [ch, notes] of this.activeNotes.entries()) { + for (const note of notes.keys()) { + arr.push(`N Ch${ch}:${note}`); + } + } + + for (const [ch, pads] of this.activePads.entries()) { + for (const pad of pads.keys()) { + arr.push(`P Ch${ch}:${pad}`); + } + } + + this.visualizerEl.innerText = `Notes: [${arr.join(", ")}]`; } /* ======= REPORTERS ======= */ - currentNote() { return this.lastNote; } - currentVelocity() { return this.lastVelocity; } - currentChannel() { return this.lastChannel; } - currentPad() { return this.lastPad; } - currentPadVelocity() { return this.lastPadVelocity; } + currentNote() { + return this.lastNote; + } + + currentVelocity() { + return this.lastVelocity; + } + + currentChannel() { + return this.lastChannel; + } + + currentPad() { + return this.lastPad; + } + + currentPadVelocity() { + return this.lastPadVelocity; + } notesPressed() { const arr = []; - for(const notes of this.activeNotes.values()){ for(const note of notes.keys()) arr.push(note); } - for(const pads of this.activePads.values()){ for(const pad of pads.keys()) arr.push(pad); } + + for (const notes of this.activeNotes.values()) { + for (const note of notes.keys()) { + arr.push(note); + } + } + + for (const pads of this.activePads.values()) { + for (const pad of pads.keys()) { + arr.push(pad); + } + } + return new jwArray.Type(arr); } notesByChannel() { const arr = []; - for(const [ch, notes] of this.activeNotes.entries()){ for(const note of notes.keys()) arr.push(note); } - for(const [ch, pads] of this.activePads.entries()){ for(const pad of pads.keys()) arr.push(pad); } + + for (const [ch, notes] of this.activeNotes.entries()) { + for (const note of notes.keys()) { + arr.push(note); + } + } + + for (const [ch, pads] of this.activePads.entries()) { + for (const pad of pads.keys()) { + arr.push(pad); + } + } + return new jwArray.Type(arr); } notesOnChannel(args) { const channel = args.CHANNEL; const arr = []; - if(this.activeNotes.has(channel)) arr.push(...this.activeNotes.get(channel).keys()); - if(this.activePads.has(channel)) arr.push(...this.activePads.get(channel).keys()); + + if (this.activeNotes.has(channel)) { + arr.push(...this.activeNotes.get(channel).keys()); + } + + if (this.activePads.has(channel)) { + arr.push(...this.activePads.get(channel).keys()); + } + return new jwArray.Type(arr); } noteNumberToName(args) { const noteNumber = args.NOTE; - if (!Number.isInteger(noteNumber) || noteNumber < 0 || noteNumber > 127) return ''; - const names = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']; + + if (!Number.isInteger(noteNumber) || noteNumber < 0 || noteNumber > 127) { + return ""; + } + + const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; const octave = Math.floor(noteNumber / 12) - 1; + return names[noteNumber % 12] + octave; } } @@ -226,3 +397,4 @@ class MIDI { Scratch.extensions.register(new MIDI()); // Hello ddededodediamante +// ughhhhhh sightreadability... this aint a gd level :sob: From fdc6176a08349195ed579816bee041410a9b7eed Mon Sep 17 00:00:00 2001 From: ElectricFuzzball Date: Sat, 14 Feb 2026 11:43:22 -0500 Subject: [PATCH 3/3] Update MIDI.js --- static/extensions/electricfuzzball_pm/MIDI.js | 430 ++++++------------ 1 file changed, 140 insertions(+), 290 deletions(-) diff --git a/static/extensions/electricfuzzball_pm/MIDI.js b/static/extensions/electricfuzzball_pm/MIDI.js index 00738efa7..0ad84383b 100644 --- a/static/extensions/electricfuzzball_pm/MIDI.js +++ b/static/extensions/electricfuzzball_pm/MIDI.js @@ -1,6 +1,5 @@ /* global Scratch, jwArray */ -// jwArray placeholder let jwArray = { Type: class {}, Block: {}, @@ -9,10 +8,7 @@ let jwArray = { class MIDI { constructor() { - if (!Scratch.vm.jwArray) { - Scratch.vm.extensionManager.loadExtensionIdSync("jwArray"); - } - + if (!Scratch.vm.jwArray) Scratch.vm.extensionManager.loadExtensionIdSync("jwArray"); jwArray = Scratch.vm.jwArray; this.activeNotes = new Map(); @@ -27,16 +23,27 @@ class MIDI { this.visualizerEl = null; this.inputs = []; + this.midiAccess = null; + + this.lastNotePressed = null; + this.notePressedFlag = false; + this.specificNotePressedFlags = {}; + this.notePressTimes = new Map(); + this.lastNNotes = []; + + this.pitchBend = 0; + this.modWheel = 0; + + this.sequenceQueue = []; if (navigator.requestMIDIAccess) { - navigator.requestMIDIAccess().then( - (access) => { - this._onMIDISuccess(access); - }, - (err) => { - console.error("MIDI failed", err); - } - ); + navigator.requestMIDIAccess().then(access => { + this.midiAccess = access; + this.inputs = Array.from(access.inputs.values()); + access.onstatechange = () => { + this.inputs = Array.from(access.inputs.values()); + }; + }).catch(err => console.error("MIDI init failed", err)); } } @@ -50,15 +57,9 @@ class MIDI { { opcode: "startListening", blockType: Scratch.BlockType.COMMAND, text: "start MIDI listener" }, { opcode: "stopListening", blockType: Scratch.BlockType.COMMAND, text: "stop MIDI listener" }, - { - opcode: "setNote", - blockType: Scratch.BlockType.COMMAND, - text: "map note [NOTE] to event [EVENT]", - arguments: { - NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 }, - EVENT: { type: Scratch.ArgumentType.STRING, defaultValue: "kick" } - } - }, + { opcode: "setNote", blockType: Scratch.BlockType.COMMAND, text: "map note [NOTE] to event [EVENT]", + arguments: { NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 }, + EVENT: { type: Scratch.ArgumentType.STRING, defaultValue: "kick" } } }, { opcode: "currentNote", blockType: Scratch.BlockType.REPORTER, text: "current note number" }, { opcode: "currentVelocity", blockType: Scratch.BlockType.REPORTER, text: "current note velocity" }, @@ -70,331 +71,180 @@ class MIDI { { opcode: "notesPressed", blockType: Scratch.BlockType.REPORTER, text: "notes currently pressed", ...jwArray.Block }, { opcode: "notesByChannel", blockType: Scratch.BlockType.REPORTER, text: "notes by channel", ...jwArray.Block }, - { - opcode: "notesOnChannel", - blockType: Scratch.BlockType.REPORTER, - text: "notes on channel [CHANNEL]", - arguments: { - CHANNEL: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 } - }, - ...jwArray.Block - }, - - { - opcode: "noteNumberToName", - blockType: Scratch.BlockType.REPORTER, - text: "note name of [NOTE]", - arguments: { - NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 } - } - }, - - { opcode: "showVisualizer", blockType: Scratch.BlockType.COMMAND, text: "show visualizer" }, - { opcode: "hideVisualizer", blockType: Scratch.BlockType.COMMAND, text: "hide visualizer" } - ] - }; - } - - /* ======= MIDI Setup ======= */ - _onMIDISuccess(access) { - this.inputs = Array.from(access.inputs.values()); - - // Just reuse the listener function instead of duplicating logic - this.startListening(); - } - - _onMIDIMessage(event) { - const [status, note, velocity] = event.data; - - // MIDI constants (no magic numbers) - const MIDI_NOTE_ON = 0x90; - const MIDI_NOTE_OFF = 0x80; - - const PAD_NOTE_MIN = 36; - const PAD_NOTE_MAX = 51; - - const command = status & 0xf0; - const channel = (status & 0x0f) + 1; - - const isPad = note >= PAD_NOTE_MIN && note <= PAD_NOTE_MAX; - - const isNoteOn = command === MIDI_NOTE_ON && velocity > 0; - const isNoteOff = command === MIDI_NOTE_OFF || (command === MIDI_NOTE_ON && velocity === 0); - - if (isNoteOn) { - if (isPad) { - this._padOn(channel, note, velocity); - } else { - this._noteOn(channel, note, velocity); - } - } - - if (isNoteOff) { - if (isPad) { - this._padOff(channel, note); - } else { - this._noteOff(channel, note); - } - } - } - - _noteOn(channel, note, velocity) { - if (!this.activeNotes.has(channel)) { - this.activeNotes.set(channel, new Map()); - } + { opcode: "notesOnChannel", blockType: Scratch.BlockType.REPORTER, text: "notes on channel [CHANNEL]", + arguments: { CHANNEL: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 } }, ...jwArray.Block }, - this.activeNotes.get(channel).set(note, velocity); + { opcode: "noteNumberToName", blockType: Scratch.BlockType.REPORTER, text: "note name of [NOTE]", + arguments: { NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 } } }, - this.lastNote = note; - this.lastVelocity = velocity; - this.lastChannel = channel; + { opcode: "whenNotePressed", blockType: Scratch.BlockType.HAT, text: "when MIDI note pressed" }, + { opcode: "whenSpecificNotePressed", blockType: Scratch.BlockType.HAT, text: "when note [NOTE] pressed", + arguments: { NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 } } }, - this.updateVisualizer(); - } + { opcode: "getPitchBend", blockType: Scratch.BlockType.REPORTER, text: "pitch bend" }, + { opcode: "getModWheel", blockType: Scratch.BlockType.REPORTER, text: "mod wheel" }, - _noteOff(channel, note) { - if (this.activeNotes.has(channel)) { - this.activeNotes.get(channel).delete(note); + { opcode: "lastNNotesPressed", blockType: Scratch.BlockType.REPORTER, text: "last [N] notes pressed", + arguments: { N: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 } }, ...jwArray.Block }, - if (this.activeNotes.get(channel).size === 0) { - this.activeNotes.delete(channel); - } - } + { opcode: "noteHeldTime", blockType: Scratch.BlockType.REPORTER, text: "time note [NOTE] is held", + arguments: { NOTE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 60 } } }, - if (!this.activeNotes.has(channel) || this.activeNotes.get(channel).size === 0) { - this.lastNote = ""; - } + { opcode: "anyNoteHeldTime", blockType: Scratch.BlockType.REPORTER, text: "time note is held" }, - this.lastVelocity = 0; - this.lastChannel = channel; + { opcode: "deviceName", blockType: Scratch.BlockType.REPORTER, text: "MIDI device name [INDEX]", + arguments: { INDEX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } } }, - this.updateVisualizer(); - } + { opcode: "deviceManufacturer", blockType: Scratch.BlockType.REPORTER, text: "MIDI device manufacturer [INDEX]", + arguments: { INDEX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } } }, - _padOn(channel, note, velocity) { - if (!this.activePads.has(channel)) { - this.activePads.set(channel, new Map()); - } + { opcode: "deviceChannels", blockType: Scratch.BlockType.REPORTER, text: "MIDI device channels [INDEX]", + arguments: { INDEX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } } }, - this.activePads.get(channel).set(note, velocity); + { opcode: "whenSequencePlayed", blockType: Scratch.BlockType.HAT, text: "when sequence [SEQUENCE] played", + arguments: { SEQUENCE: { type: Scratch.ArgumentType.STRING, defaultValue: "60 62 64" } } }, - this.lastPad = note; - this.lastPadVelocity = velocity; - - this.updateVisualizer(); - } - - _padOff(channel, note) { - if (this.activePads.has(channel)) { - this.activePads.get(channel).delete(note); - - if (this.activePads.get(channel).size === 0) { - this.activePads.delete(channel); + { opcode: "visualizerControl", blockType: Scratch.BlockType.COMMAND, text: "[ACTION] visualizer", + arguments: { ACTION: { type: Scratch.ArgumentType.STRING, menu: "showHide", defaultValue: "show" } } } + ], + menus: { + showHide: ["show", "hide"] } - } - - if (!this.activePads.has(channel) || this.activePads.get(channel).size === 0) { - this.lastPad = ""; - } - - this.lastPadVelocity = 0; - - this.updateVisualizer(); + }; } - /* ======= Listeners ======= */ startListening() { - this.inputs.forEach((input) => { - input.onmidimessage = (event) => { - this._onMIDIMessage(event); - }; - }); + if (!this.inputs || this.inputs.length === 0) { + alert("MIDI device not detected!"); + throw new Error("MIDI device not detected!"); + } + this.inputs.forEach(input => input.onmidimessage = e => this._onMIDIMessage(e)); } stopListening() { - this.inputs.forEach((input) => { - input.onmidimessage = null; - }); - + this.inputs.forEach(input => input.onmidimessage = null); this.activeNotes.clear(); this.activePads.clear(); - this.lastNote = ""; this.lastVelocity = 0; this.lastChannel = -1; - this.lastPad = ""; this.lastPadVelocity = 0; - this.updateVisualizer(); } - setNote(args) { - console.log(`Map note ${args.NOTE} → ${args.EVENT}`); - } - - /* ======= Visualizer ======= */ - showVisualizer() { - if (this.visualizerEl) return; - - this.visualizerEl = document.createElement("div"); - - this.visualizerEl.style.position = "fixed"; - this.visualizerEl.style.top = "50px"; - this.visualizerEl.style.left = "50px"; - - this.visualizerEl.style.background = "#222"; - this.visualizerEl.style.color = "#fff"; - - this.visualizerEl.style.padding = "10px"; - this.visualizerEl.style.borderRadius = "8px"; - - this.visualizerEl.style.fontFamily = "monospace"; - this.visualizerEl.style.zIndex = 9999; - - this.visualizerEl.style.cursor = "move"; - this.visualizerEl.innerText = "Notes: []"; - - document.body.appendChild(this.visualizerEl); - - let dragging = false; - let offsetX = 0; - let offsetY = 0; - - this.visualizerEl.addEventListener("mousedown", (e) => { - dragging = true; - offsetX = e.clientX - this.visualizerEl.offsetLeft; - offsetY = e.clientY - this.visualizerEl.offsetTop; - }); - - document.addEventListener("mousemove", (e) => { - if (!dragging) return; - - this.visualizerEl.style.left = e.clientX - offsetX + "px"; - this.visualizerEl.style.top = e.clientY - offsetY + "px"; - }); + _onMIDIMessage(event) { + const [status, note, velocity] = event.data; + const MIDI_NOTE_ON = 0x90; + const MIDI_NOTE_OFF = 0x80; + const PAD_NOTE_MIN = 36; + const PAD_NOTE_MAX = 51; + const CONTROL_CHANGE = 0xB0; + const PITCH_BEND = 0xE0; - document.addEventListener("mouseup", () => { - dragging = false; - }); + const command = status & 0xf0; + const channel = (status & 0x0f) + 1; - this.updateVisualizer(); - } + if (command === CONTROL_CHANGE && note === 1) this.modWheel = velocity; + if (command === PITCH_BEND) this.pitchBend = ((velocity << 7) | note) - 8192; - hideVisualizer() { - if (this.visualizerEl) { - document.body.removeChild(this.visualizerEl); - this.visualizerEl = null; - } - } + const isPad = note >= PAD_NOTE_MIN && note <= PAD_NOTE_MAX; + const isNoteOn = command === MIDI_NOTE_ON && velocity > 0; + const isNoteOff = command === MIDI_NOTE_OFF || (command === MIDI_NOTE_ON && velocity === 0); - updateVisualizer() { - if (!this.visualizerEl) return; + if (isNoteOn) { + this.lastNotePressed = note; + this.notePressedFlag = true; + this.specificNotePressedFlags[note] = true; + this.notePressTimes.set(note, Date.now()); + this.lastNNotes.push(note); + if (this.lastNNotes.length > 50) this.lastNNotes.shift(); - const arr = []; + this.sequenceQueue.push(note); + if (this.sequenceQueue.length > 20) this.sequenceQueue.shift(); - for (const [ch, notes] of this.activeNotes.entries()) { - for (const note of notes.keys()) { - arr.push(`N Ch${ch}:${note}`); - } + if (isPad) this._padOn(channel, note, velocity); + else this._noteOn(channel, note, velocity); } - for (const [ch, pads] of this.activePads.entries()) { - for (const pad of pads.keys()) { - arr.push(`P Ch${ch}:${pad}`); - } + if (isNoteOff) { + if (isPad) this._padOff(channel, note); + else this._noteOff(channel, note); + this.notePressTimes.delete(note); } - - this.visualizerEl.innerText = `Notes: [${arr.join(", ")}]`; - } - - /* ======= REPORTERS ======= */ - currentNote() { - return this.lastNote; - } - - currentVelocity() { - return this.lastVelocity; - } - - currentChannel() { - return this.lastChannel; } - currentPad() { - return this.lastPad; + whenSequencePlayed(args) { + const target = args.SEQUENCE.trim().split(" ").map(Number); + const qlen = this.sequenceQueue.length; + if (qlen < target.length) return false; + const recent = this.sequenceQueue.slice(qlen - target.length); + return recent.every((n, i) => n === target[i]); } - currentPadVelocity() { - return this.lastPadVelocity; + whenNotePressed() { + if (this.notePressedFlag) { + this.notePressedFlag = false; + return true; + } + return false; } - notesPressed() { - const arr = []; - - for (const notes of this.activeNotes.values()) { - for (const note of notes.keys()) { - arr.push(note); - } + whenSpecificNotePressed(args) { + const n = args.NOTE; + if (this.specificNotePressedFlags[n]) { + this.specificNotePressedFlags[n] = false; + return true; } - - for (const pads of this.activePads.values()) { - for (const pad of pads.keys()) { - arr.push(pad); - } - } - - return new jwArray.Type(arr); + return false; } - notesByChannel() { - const arr = []; - - for (const [ch, notes] of this.activeNotes.entries()) { - for (const note of notes.keys()) { - arr.push(note); - } - } + getPitchBend() { return this.pitchBend; } + getModWheel() { return this.modWheel; } - for (const [ch, pads] of this.activePads.entries()) { - for (const pad of pads.keys()) { - arr.push(pad); - } - } + lastNNotesPressed(args) { return new jwArray.Type(this.lastNNotes.slice(-args.N)); } - return new jwArray.Type(arr); + noteHeldTime(args) { + const n = args.NOTE; + return this.notePressTimes.has(n) ? ((Date.now() - this.notePressTimes.get(n))/1000).toFixed(2) : 0; } - notesOnChannel(args) { - const channel = args.CHANNEL; - const arr = []; - - if (this.activeNotes.has(channel)) { - arr.push(...this.activeNotes.get(channel).keys()); - } + anyNoteHeldTime() { + return this.lastNotePressed && this.notePressTimes.has(this.lastNotePressed) ? + ((Date.now() - this.notePressTimes.get(this.lastNotePressed))/1000).toFixed(2) : 0; + } - if (this.activePads.has(channel)) { - arr.push(...this.activePads.get(channel).keys()); - } + deviceName(args) { return this.inputs[args.INDEX]?.name || ""; } + deviceManufacturer(args) { return this.inputs[args.INDEX]?.manufacturer || ""; } + deviceChannels(args) { return this.inputs[args.INDEX]?.channels || 16; } - return new jwArray.Type(arr); + visualizerControl(args) { + if (args.ACTION === "show") this.showVisualizer(); + else this.hideVisualizer(); } - noteNumberToName(args) { - const noteNumber = args.NOTE; + _noteOn(channel,note,velocity){ if(!this.activeNotes.has(channel)) this.activeNotes.set(channel,new Map()); this.activeNotes.get(channel).set(note,velocity); this.lastNote=note; this.lastVelocity=velocity; this.lastChannel=channel; this.updateVisualizer(); } + _noteOff(channel,note){ if(this.activeNotes.has(channel)){ this.activeNotes.get(channel).delete(note); if(this.activeNotes.get(channel).size===0) this.activeNotes.delete(channel); } if(!this.activeNotes.has(channel)||this.activeNotes.get(channel).size===0)this.lastNote=""; this.lastVelocity=0; this.lastChannel=channel; this.updateVisualizer(); } + _padOn(channel,note,velocity){ if(!this.activePads.has(channel)) this.activePads.set(channel,new Map()); this.activePads.get(channel).set(note,velocity); this.lastPad=note; this.lastPadVelocity=velocity; this.updateVisualizer(); } + _padOff(channel,note){ if(this.activePads.has(channel)){ this.activePads.get(channel).delete(note); if(this.activePads.get(channel).size===0) this.activePads.delete(channel); } if(!this.activePads.has(channel)||this.activePads.get(channel).size===0)this.lastPad=""; this.lastPadVelocity=0; this.updateVisualizer(); } - if (!Number.isInteger(noteNumber) || noteNumber < 0 || noteNumber > 127) { - return ""; - } + currentNote(){ return this.lastNote; } + currentVelocity(){ return this.lastVelocity; } + currentChannel(){ return this.lastChannel; } + currentPad(){ return this.lastPad; } + currentPadVelocity(){ return this.lastPadVelocity; } - const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; - const octave = Math.floor(noteNumber / 12) - 1; + notesPressed(){ const arr=[]; for(const notes of this.activeNotes.values()) for(const note of notes.keys()) arr.push(note); for(const pads of this.activePads.values()) for(const pad of pads.keys()) arr.push(pad); return new jwArray.Type(arr); } + notesByChannel(){ const arr=[]; for(const [ch,notes] of this.activeNotes.entries()) for(const note of notes.keys()) arr.push(note); for(const [ch,pads] of this.activePads.entries()) for(const pad of pads.keys()) arr.push(pad); return new jwArray.Type(arr); } + notesOnChannel(args){ const arr=[]; const channel=args.CHANNEL; if(this.activeNotes.has(channel)) arr.push(...this.activeNotes.get(channel).keys()); if(this.activePads.has(channel)) arr.push(...this.activePads.get(channel).keys()); return new jwArray.Type(arr); } + noteNumberToName(args){ const noteNumber=args.NOTE; if(!Number.isInteger(noteNumber)||noteNumber<0||noteNumber>127) return ""; const names=["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]; const octave=Math.floor(noteNumber/12)-1; return names[noteNumber%12]+octave; } - return names[noteNumber % 12] + octave; - } + setNote(args){ console.log(`Map note ${args.NOTE} → ${args.EVENT}`); } + + showVisualizer(){ if(this.visualizerEl)return; this.visualizerEl=document.createElement("div"); this.visualizerEl.style.position="fixed"; this.visualizerEl.style.top="50px"; this.visualizerEl.style.left="50px"; this.visualizerEl.style.background="#222"; this.visualizerEl.style.color="#fff"; this.visualizerEl.style.padding="10px"; this.visualizerEl.style.borderRadius="8px"; this.visualizerEl.style.fontFamily="monospace"; this.visualizerEl.style.zIndex=9999; this.visualizerEl.style.cursor="move"; this.visualizerEl.innerText="Notes: []"; document.body.appendChild(this.visualizerEl); let dragging=false; let offsetX=0; let offsetY=0; this.visualizerEl.addEventListener("mousedown",(e)=>{dragging=true; offsetX=e.clientX-this.visualizerEl.offsetLeft; offsetY=e.clientY-this.visualizerEl.offsetTop;}); document.addEventListener("mousemove",(e)=>{if(!dragging)return; this.visualizerEl.style.left=e.clientX-offsetX+"px"; this.visualizerEl.style.top=e.clientY-offsetY+"px";}); document.addEventListener("mouseup",()=>dragging=false); this.updateVisualizer(); } + hideVisualizer(){ if(this.visualizerEl){document.body.removeChild(this.visualizerEl); this.visualizerEl=null;} } + updateVisualizer(){ if(!this.visualizerEl) return; const arr=[]; for(const [ch,notes] of this.activeNotes.entries()) for(const note of notes.keys()) arr.push(`N Ch${ch}:${note} v${notes.get(note)}`); for(const [ch,pads] of this.activePads.entries()) for(const pad of pads.keys()) arr.push(`P Ch${ch}:${pad} v${pads.get(pad)}`); this.visualizerEl.innerText=`Notes: [${arr.join(", ")}]`; } } Scratch.extensions.register(new MIDI()); - -// Hello ddededodediamante -// ughhhhhh sightreadability... this aint a gd level :sob: +// Howdy! I'm Flowey! Flowey the Flower!