From 597c94199118adcc597e3e437b0f7174192c8bb5 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 10 Dec 2025 12:01:04 -0600 Subject: [PATCH] Add keyboard function --- packages/midi/midi.mjs | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index cd38d3587..477da51cd 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -543,3 +543,75 @@ export async function midin(input) { device.addListener('midimessage', listeners[input]); return cc; } + +/** + * MIDI keyboard: Opens a MIDI input port to receive MIDI keyboard messages. + * + * The note length is fixed as Superdough is not currently set up for undetermined + * note durations + * + * @param {string | number} input MIDI device name or index defaulting to 0 + * @returns {function(): Pattern} A function that produces a pattern. + * When queried, the pattern will produces the most recently played midi notes and velocities, + * lasting for the specified duration + * @example + * const cc = await midin('IAC Driver Bus 1') + * note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth") + * @example + * const allCC = await midin('IAC Driver Bus 1') + * const cc = (ccNum) => allCC(ccNum, 2) // just channel 2 + * note("c a f e").s("saw") + * .when(cc(0).gt(0), x => x.postgain(0)) + */ +const kHaps = {}; +const kListeners = {}; +export async function keyboard(input, noteLength = 0.5) { + const device = await _initialize(input); + if (!kHaps[input]) { + kHaps[input] = {}; + } + kListeners[input] && device.removeListener('midimessage', kListeners[input]); + kListeners[input] = (e) => { + const { dataBytes, message } = e; + const [cc, v] = dataBytes; + const noteoff = message.command === 8; + const key = `${input}_${cc}`; + const t = getTime() + 0.1; // slight delay so it's not too late for cyclist to catch + const span = new TimeSpan(t, t + noteLength); + let value = { midikey: key }; + if (noteoff) { + /* TODO: It's a big effort, but we could modify superdough to allow for situations where + we don't know the hap duration in advance. This would mean, for example, that if the hap + is flagged as such a special note-on event, we have all effects be persistent & all ADSR + envelopes stop at the S stage [and store references to them by note+pattern] + If this is implemented, then getting full keyboard functionality should be as simple + as sending the corresponding note-off event below and triggering `release` on each of those + referenced effects/envelopes + + value = { ...value, noteoff: true }; + + If this is achieved, we can remove the noteLength parameter + */ + return; + } else { + value = { ...value, note: Math.round(cc), velocity: v / 127 }; + } + kHaps[input][key] = new Hap(span, span, value, {}); + }; + device.addListener('midimessage', kListeners[input]); + const kb = () => { + const query = (state) => { + const haps = []; + for (const [key, hap] of Object.entries(kHaps[input])) { + haps.push(hap); + } + if (state.controls.cyclist) { + // Notes have been sent; clear them + kHaps[input] = {}; + } + return haps; + }; + return new Pattern(query); + }; + return kb; +}