diff --git a/client/package-lock.json b/client/package-lock.json index 326cba08..fd65ac05 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -30,6 +30,7 @@ "esbuild-svelte": "^0.8.1", "eslint": "^8.57.0", "eslint-plugin-svelte": "^2.43.0", + "eventemitter3": "^5.0.1", "fs-extra": "^11.2.0", "material-ui-colors": "^1.0.0", "md5-file": "^5.0.0", @@ -46,7 +47,8 @@ "tailwindcss": "^3.4.7", "uuid": "^10.0.0", "wait-on": "^7.2.0", - "weak-ref-collections": "^1.2.4" + "weak-ref-collections": "^1.2.4", + "webmidi": "^3.1.9" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -83,6 +85,18 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@electron-forge/cli": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.4.0.tgz", @@ -2205,6 +2219,13 @@ "@types/node": "*" } }, + "node_modules/@types/webmidi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/webmidi/-/webmidi-2.1.0.tgz", + "integrity": "sha512-k898MjEUSHB+6rSeCPQk/kLgie0wgWZ2t78GlWj86HbTQ+XmtbBafYg5LNjn8bVHfItEhPGZPf579Xfc6keV6w==", + "dev": true, + "optional": true + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -3978,6 +3999,15 @@ "p-limit": "^3.1.0 " } }, + "node_modules/djipevents": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/djipevents/-/djipevents-2.0.7.tgz", + "integrity": "sha512-KNFYaU85imxOCKOUsIR70Iz9E19r96/X7LSH+u0tSoZdpWcBdzoqtTsU+wuLhc6GMpSFob+KInkZAbfKi01Bjg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.20.6" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -5216,9 +5246,9 @@ } }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, "node_modules/execa": { @@ -6465,6 +6495,12 @@ "node": ">= 6" } }, + "node_modules/http-proxy/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -7159,6 +7195,16 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jazz-midi": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/jazz-midi/-/jazz-midi-1.7.9.tgz", + "integrity": "sha512-c8c4BBgwxdsIr1iVm53nadCrtH7BUlnX3V95ciK/gbvXN/ndE5+POskBalXgqlc/r9p2XUbdLTrgrC6fou5p9w==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", @@ -7261,6 +7307,17 @@ "node": ">=8" } }, + "node_modules/jzz": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/jzz/-/jzz-1.8.5.tgz", + "integrity": "sha512-/4suyf9d6/LhF6Hh2OOCI7FZfOcdwCqh/bXYe0nPJ0/0Zg4BiTMD1eiGacom1ZsvRKnLBURHrV0rJtiVjVlhpA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/webmidi": "^2.1.0", + "jazz-midi": "^1.7.9" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7378,13 +7435,6 @@ "dev": true, "license": "MIT" }, - "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, - "license": "MIT" - }, "node_modules/listr2/node_modules/is-fullwidth-code-point": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", @@ -9978,6 +10028,12 @@ "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==", "dev": true }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -12127,6 +12183,21 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/webmidi": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/webmidi/-/webmidi-3.1.9.tgz", + "integrity": "sha512-KwhkwnGGxE49yGruEVFeCRA8Zsx5i/4NEa/Gtcx34SKIh8NU6OsWo3xVtDu0MY7/wdDScq/Ymh+/v+XDbnBA1A==", + "dev": true, + "dependencies": { + "djipevents": "^2.0.7" + }, + "engines": { + "node": ">=8.5" + }, + "optionalDependencies": { + "jzz": "^1.5.6" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/client/package.json b/client/package.json index 5173d888..f13a5aa3 100644 --- a/client/package.json +++ b/client/package.json @@ -56,6 +56,7 @@ "esbuild-svelte": "^0.8.1", "eslint": "^8.57.0", "eslint-plugin-svelte": "^2.43.0", + "eventemitter3": "^5.0.1", "fs-extra": "^11.2.0", "material-ui-colors": "^1.0.0", "md5-file": "^5.0.0", @@ -72,6 +73,7 @@ "tailwindcss": "^3.4.7", "uuid": "^10.0.0", "wait-on": "^7.2.0", - "weak-ref-collections": "^1.2.4" + "weak-ref-collections": "^1.2.4", + "webmidi": "^3.1.9" } } diff --git a/client/src/main/Player.svelte b/client/src/main/Player.svelte index 98cf2db4..09ddc1f4 100644 --- a/client/src/main/Player.svelte +++ b/client/src/main/Player.svelte @@ -15,7 +15,7 @@ import { db } from "../stores/db" import { log } from "../stores/client-logs" import { Wait } from "../stores/player" - import { setLED, LED_OFF } from "../stores/midi" + import { midiSetLED, LED_OFF } from "../stores/midi" import { alert } from "../stores/alerts" // Object automatically updates on change @@ -43,7 +43,7 @@ $: overtime = items.length > 0 && items[0].type === "wait" && items[0].overtime $: if (items.length === 0) { - setLED(LED_OFF) + midiSetLED(LED_OFF) } const doneWaiting = () => { diff --git a/client/src/main/player/Buttons.svelte b/client/src/main/player/Buttons.svelte index c445eda4..4a5d3ee0 100644 --- a/client/src/main/player/Buttons.svelte +++ b/client/src/main/player/Buttons.svelte @@ -9,8 +9,8 @@ import { userConfig } from "../../stores/config" import { blockSpacebarPlay } from "../../stores/player" import { - setLED, - registerButtonPressCallback, + midiSetLED, + midiButtonPresses, LED_OFF, LED_ON, LED_FLASH, @@ -52,10 +52,10 @@ ledState = LED_ON } - $: setLED(ledState) - setTimeout(() => setLED(ledState), 500) // Allow 500ms for midi system to initialize + $: midiSetLED(ledState) + setTimeout(() => midiSetLED(ledState), 500) // Allow 500ms for midi system to initialize - registerButtonPressCallback(() => { + midiButtonPresses.addListener("pressed", () => { if (playDisabled) { console.log("Got MIDI press, but currently not eligible to play!") } else { diff --git a/client/src/stores/midi.js b/client/src/stores/midi.js index 891ef5de..6413e113 100644 --- a/client/src/stores/midi.js +++ b/client/src/stores/midi.js @@ -1,77 +1,81 @@ -import { noop } from "svelte/internal" -import { get } from "svelte/store" -import { userConfig } from "./config" - -// TODO: convert to webmidi.js, much easier to work with +import { EventEmitter } from "eventemitter3" +import { WebMidi } from "webmidi" -let midi -let outputs = [] -let buttonPressCallback = noop +import { alert } from "./alerts" +import { userConfig } from "./config" -let lastLEDValue = 0 +const MIDI_BTN_CTRL = 0x10 +const MIDI_LED_CTRL = 0x11 +const BTN_PRESSED = 0x7f export const LED_OFF = 0 export const LED_ON = 1 export const LED_FLASH = 2 export const LED_PULSATE_SLOW = 3 export const LED_PULSATE_FAST = 4 +const ledStrings = ["off", "on", "flash", "pulsate/slow", "pulsate/fast"] -const updateMidiDevices = (enabled = null) => { - if (enabled === null) { - enabled = get(userConfig).enableMIDIButtonBox - } +export const midiButtonPresses = new EventEmitter() - if (!enabled) { - setLED(LED_OFF, true) - } +let lastLEDValue = LED_OFF +let enabled = false - outputs = [] +window.addEventListener("beforeunload", () => { + midiSetLED(LED_OFF, true) +}) - for (const ports of [midi.inputs, midi.outputs]) { - for (const [, port] of ports) { - const input = port instanceof MIDIInput - if (enabled) { - port.open() - if (input) { - port.onmidimessage = ({ data }) => { - if (data.length === 3 && data[0] === 0xb0 && data[1] === 0x10 && data[2] === 0x7f) { - buttonPressCallback(data) - } - } - } else { - port.send([0xb0, 0x11, lastLEDValue]) - outputs.push(port) +const enableListeners = () => { + WebMidi.addListener("connected", ({ port }) => { + if (port.type === "input") { + console.log(`Got midi input: ${port.name} (installing press listener)`) + port.channels[1].addListener("controlchange", (event) => { + if (event.controller.number === MIDI_BTN_CTRL) { + midiButtonPresses.emit(event.rawValue === BTN_PRESSED ? "pressed" : "released") } - } else { - port.close() - } + }) + } else if (port.type === "output") { + console.log(`Got midi output: ${port.name} (set LED to ${ledStrings[lastLEDValue]})`) + port.channels[1].sendControlChange(MIDI_LED_CTRL, lastLEDValue) } - } + }) } -userConfig.subscribe(({ enableMIDIButtonBox }) => { - if (midi) { - updateMidiDevices(enableMIDIButtonBox) - } -}) - -export let setLED = (value, skipSave = false) => { - console.log(`Set LED to ${value}`) - for (const port of outputs) { - port.send([0xb0, 0x11, value]) +export const midiSetLED = (value, skipSave = false) => { + if (enabled) { + console.log(`Set LED to ${ledStrings[value]}`) + for (const output of WebMidi.outputs) { + output.channels[1].sendControlChange(MIDI_LED_CTRL, value) + } } + // Save state, to re-enable if device is plugged in if (!skipSave) { lastLEDValue = value } } -export let registerButtonPressCallback = (callback) => (buttonPressCallback = callback) -;(async () => { - midi = await navigator.requestMIDIAccess({ sysex: true }) - updateMidiDevices() - midi.onstatechange = () => updateMidiDevices() -})() - -window.addEventListener("beforeunload", () => { - setLED(LED_OFF, true) +userConfig.subscribe(({ enableMIDIButtonBox }) => { + if (enableMIDIButtonBox && !enabled) { + enableListeners() + WebMidi.enable({ + sysex: true, + callback: (error) => { + if (error) { + enabled = false + console.error("Error enabling WebMidi!", error) + alert("Error enabling MIDI subsystem for button box", "error") + userConfig.update(($config) => ({ ...$config, enableMIDIButtonBox: false })) + } else { + console.log("WebMidi enabled") + enabled = true + } + } + }) + } else if (!enableMIDIButtonBox && enabled) { + midiSetLED(LED_OFF, true) + WebMidi.disable() + enabled = false + console.log("WebMidi disabled") + } }) + +window.WebMidi = WebMidi diff --git a/client/src/stores/player.js b/client/src/stores/player.js index 89fd3791..6510bbb0 100644 --- a/client/src/stores/player.js +++ b/client/src/stores/player.js @@ -574,6 +574,8 @@ navigator.mediaDevices.ondevicechange = async () => { config.subscribe(($config) => { setCompression($config.BROADCAST_COMPRESSION || false) }) + +// Async code called at startup ;(async () => { setCompression(get(config).BROADCAST_COMPRESSION || false) await updateSpeakers() diff --git a/docs/client.md b/docs/client.md index b5d5c183..00990c31 100644 --- a/docs/client.md +++ b/docs/client.md @@ -126,7 +126,7 @@ Prerequisites: * [Git](https://git-scm.com/) * On Windows, install [Git for Windows](https://gitforwindows.org/) and **make sure to use its included "Git Bash" terminal.** - * [Node.js v18+](https://nodejs.org/) + * [Node.js v20+](https://nodejs.org/) To get the development code running, in your terminal run the following (use "Git Bash" on Windows),