Merge pull request 'Feature: reconnectable MIDI inputs that keep track of last known CC state' (#1852) from unframework/strudel:nm/midi-input-state into main

Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1852
This commit is contained in:
froos 2026-02-08 11:22:27 +01:00
commit 2755a7eebf
3 changed files with 270 additions and 63 deletions

View file

@ -23,6 +23,8 @@ import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi';
import { getAudioContext } from '@strudel/webaudio';
import { scheduleAtTime } from '../superdough/helpers.mjs';
import { getMidiDeviceNamesString, getDevice } from './util.mjs';
import { MidiInput } from './input.mjs';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@ -31,10 +33,6 @@ function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
function getMidiDeviceNamesString(devices) {
return devices.map((o) => `'${o.name}'`).join(' | ');
}
export function enableWebMidi(options = {}) {
const { onReady, onConnected, onDisconnected, onEnabled } = options;
if (WebMidi.enabled) {
@ -72,29 +70,6 @@ export function enableWebMidi(options = {}) {
});
}
function getDevice(indexOrName, devices) {
if (!devices.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
if (typeof indexOrName === 'number') {
return devices[indexOrName];
}
const byName = (name) => devices.find((output) => output.name.includes(name));
if (typeof indexOrName === 'string') {
return byName(indexOrName);
}
// attempt to default to first IAC device if none is specified
const IACOutput = byName('IAC');
const device = IACOutput ?? devices[0];
if (!device) {
throw new Error(
`🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
);
}
return IACOutput ?? devices[0];
}
// send start/stop messages to outputs when repl starts/stops
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
@ -495,9 +470,9 @@ Pattern.prototype.midi = function (midiport, options = {}) {
};
/**
* Initialize a midi device
* Initialize a midi input device
*/
async function _initialize(input) {
async function _initializeInput(input) {
if (isPattern(input)) {
throw new Error(
`[midi] Midi input cannot be a pattern. Make sure to pass device name with single quotes. Example: midin('${
@ -505,24 +480,31 @@ async function _initialize(input) {
}')`,
);
}
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (!device) {
throw new Error(
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
const instance = midiInputs[input] || new MidiInput(input);
midiInputs[input] = instance;
if (initial) {
const device = instance.initialDevice;
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`[midi] Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
device
? `[midi] Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`
: `[midi] Midi enabled! Waiting for device "${input}"... Currently connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
return device;
return instance;
}
// MIDI input wrappers, by specified input string/index
const midiInputs = {};
/**
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
*
@ -543,31 +525,10 @@ async function _initialize(input) {
* note("c a f e").s("saw")
* .when(cc(0).gt(0), x => x.postgain(0))
*/
let listeners = {};
const refs = {};
const refsByChan = {};
export async function midin(input) {
const device = await _initialize(input);
refs[input] ??= {};
refsByChan[input] ??= {};
const cc = (cc, chan) => {
if (chan !== undefined) {
return ref(() => refsByChan[input][cc]?.[chan] || 0);
}
return ref(() => refs[input][cc] || 0);
};
const instance = await _initializeInput(input);
listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => {
const [ccNum, v] = e.dataBytes;
const chan = e.message.channel;
const scaled = v / 127;
refsByChan[input][ccNum] ??= {};
refsByChan[input][ccNum][chan] = scaled;
refs[input][ccNum] = scaled;
};
device.addListener('midimessage', listeners[input]);
return cc;
return instance.createCC.bind(instance);
}
/**
@ -622,7 +583,16 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
return true;
}
export async function midikeys(input) {
const device = await _initialize(input);
const instance = await _initializeInput(input);
// TODO: support unpluggable device usage
const device = instance.initialDevice;
if (!device) {
throw new Error(
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
if (!kHaps[input]) {
kHaps[input] = [];
}