Skip to content

Commit

Permalink
Use WebMidi for button box
Browse files Browse the repository at this point in the history
  • Loading branch information
dtcooper committed Aug 4, 2024
1 parent 7dcaefc commit 0bcadd7
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 74 deletions.
93 changes: 82 additions & 11 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
4 changes: 2 additions & 2 deletions client/src/main/Player.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = () => {
Expand Down
10 changes: 5 additions & 5 deletions client/src/main/player/Buttons.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
112 changes: 58 additions & 54 deletions client/src/stores/midi.js
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions client/src/stores/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit 0bcadd7

Please sign in to comment.