@strudel/midi
Version:
Midi API for strudel
275 lines (274 loc) • 9.57 kB
JavaScript
import * as V from "webmidi";
import { Note as Y } from "webmidi";
import { Pattern as F, isPattern as q, logger as m, getEventOffsetMs as G, getControlName as R, noteToMidi as H, ref as J } from "@strudel/core";
const { WebMidi: s } = V;
function K() {
return typeof navigator.requestMIDIAccess == "function";
}
function g(e) {
return e.map((n) => `'${n.name}'`).join(" | ");
}
function T(e = {}) {
const { onReady: n, onConnected: t, onDisconnected: i, onEnabled: r } = e;
if (!s.enabled) {
if (!K())
throw new Error("Your Browser does not support WebMIDI.");
return s.addListener("connected", () => {
t?.(s);
}), s.addListener("enabled", () => {
r?.(s);
}), s.addListener("disconnected", (o) => {
i?.(s, o);
}), new Promise((o, c) => {
if (s.enabled) {
o(s);
return;
}
s.enable(
(l) => {
l && c(l), n?.(s), o(s);
},
{ sysex: !0 }
);
});
}
}
function $(e, n) {
if (!n.length)
throw new Error("🔌 No MIDI devices found. Connect a device or enable IAC Driver.");
if (typeof e == "number")
return n[e];
const t = (o) => n.find((c) => c.name.includes(o));
if (typeof e == "string")
return t(e);
const i = t("IAC"), r = i ?? n[0];
if (!r)
throw new Error(
`🔌 MIDI device '${r || ""}' not found. Use one of ${g(n)}`
);
return i ?? n[0];
}
typeof window < "u" && window.addEventListener("message", (e) => {
s?.enabled && e.data === "strudel-stop" && s.outputs.forEach((n) => n.sendStop());
});
const h = /* @__PURE__ */ new Map();
function _(e) {
return Object.fromEntries(
Object.entries(e).map(([n, t]) => (typeof t == "number" && (t = { ccn: t }), [R(n), t]))
);
}
function Q(e, n = "") {
if (!e.startsWith("github:"))
throw new Error('expected "github:" at the start of pseudoUrl');
let [t, i] = e.split("github:");
return i = i.endsWith("/") ? i.slice(0, -1) : i, i.split("/").length === 2 && (i += "/main"), `https://raw.githubusercontent.com/${i}/${n}`;
}
function ae(e) {
h.set("default", _(e));
}
let C = {};
async function de(e) {
typeof e == "string" && (e.startsWith("github:") && (e = Q(e, "midimap.json")), C[e] || (C[e] = fetch(e).then((n) => n.json())), e = await C[e]), typeof e == "object" && Object.entries(e).forEach(([n, t]) => h.set(n, _(t)));
}
const fe = /* @__PURE__ */ new Map();
function X(e = 0, n = 0, t = 1, i = 1) {
if (n === t)
throw new Error("min and max cannot be the same value");
let r = (e - n) / (t - n);
return r = Math.min(1, Math.max(0, r)), Math.pow(r, i);
}
function Z(e, n) {
return Object.keys(n).filter((t) => !!e[R(t)]).map((t) => {
const { ccn: i, min: r = 0, max: o = 1, exp: c = 1 } = e[t], l = X(n[t], r, o, c);
return { ccn: i, ccv: l };
});
}
function A(e, n, t, i, r) {
if (typeof n != "number" || n < 0 || n > 1)
throw new Error("expected ccv to be a number between 0 and 1");
if (!["string", "number"].includes(typeof e))
throw new Error("expected ccn to be a number or a string");
const o = Math.round(n * 127);
t.sendControlChange(e, o, i, { time: r });
}
function L(e, n, t, i) {
if (typeof e != "number" || e < 0 || e > 127)
throw new Error("expected progNum (program change) to be a number between 0 and 127");
n.sendProgramChange(e, t, { time: i });
}
function U(e, n, t, i) {
if (Array.isArray(e)) {
if (!e.every((r) => Number.isInteger(r) && r >= 0 && r <= 255))
throw new Error("all sysexid bytes must be integers between 0 and 255");
} else if (!Number.isInteger(e) || e < 0 || e > 255)
throw new Error("A:sysexid must be an number between 0 and 255 or an array of such integers");
if (!Array.isArray(n))
throw new Error("expected sysex to be an array of numbers (0-255)");
if (!n.every((r) => Number.isInteger(r) && r >= 0 && r <= 255))
throw new Error("all sysex bytes must be integers between 0 and 255");
t.sendSysex(e, n, { time: i });
}
function ee(e, n, t, i, r) {
if (Array.isArray(e)) {
if (!e.every((o) => Number.isInteger(o) && o >= 0 && o <= 255))
throw new Error("all nrpnn bytes must be integers between 0 and 255");
} else if (!Number.isInteger(n) || n < 0 || n > 255)
throw new Error("A:sysexid must be an number between 0 and 255 or an array of such integers");
t.sendNRPN(e, n, i, { time: r });
}
function ne(e, n, t, i) {
if (typeof e != "number" || e < -1 || e > 1)
throw new Error("expected midibend to be a number between -1 and 1");
n.sendPitchBend(e, t, { time: i });
}
function te(e, n, t, i) {
if (typeof e != "number" || e < 0 || e > 1)
throw new Error("expected miditouch to be a number between 0 and 1");
n.sendChannelAftertouch(e, t, { time: i });
}
function ie(e, n, t, i, r, o) {
if (e == null || e === "")
throw new Error("note cannot be null or empty");
if (n != null && (typeof n != "number" || n < 0 || n > 1))
throw new Error("velocity must be a number between 0 and 1");
if (t != null && (typeof t != "number" || t < 0))
throw new Error("duration must be a positive number");
const c = typeof e == "number" ? e : H(e), l = new Y(c, { attack: n, duration: t });
i.playNote(l, r, {
time: o
});
}
F.prototype.midi = function(e, n = {}) {
if (q(e))
throw new Error(
`.midi does not accept Pattern input for midiport. Make sure to pass device name with single quotes. Example: .midi('${s.outputs?.[0]?.name || "IAC Driver Bus 1"}')`
);
if (typeof e == "object") {
const { port: i, isController: r = !1, ...o } = e;
n = {
isController: r,
...o,
...n
// Keep any options passed separately
}, e = i;
}
let t = {
// Default configuration values
isController: !1,
// Disable sending notes for midi controllers
latencyMs: 34,
// Default latency to get audio engine to line up in ms
noteOffsetMs: 10,
// Default note-off offset to prevent glitching in ms
midichannel: 1,
// Default MIDI channel
velocity: 0.9,
// Default velocity
gain: 1,
// Default gain
midimap: "default",
// Default MIDI map
midiport: e,
// Store the port in the config
...n
// Override defaults with provided options
};
return T({
onEnabled: ({ outputs: i }) => {
const r = $(t.midiport, i), o = i.filter((c) => c.name !== r.name);
m(
`Midi enabled! Using "${r.name}". ${o?.length ? `Also available: ${g(o)}` : ""}`
);
},
onDisconnected: ({ outputs: i }) => m(`Midi device disconnected! Available: ${g(i)}`)
}), this.onTrigger((i, r, o, c) => {
if (!s.enabled) {
m("Midi not enabled");
return;
}
i.ensureObjectValue();
const l = t.latencyMs, d = `+${G(c, r) + l}`;
let {
note: I,
nrpnn: N,
nrpv: x,
ccn: j,
ccv: P,
midichan: u = t.midichannel,
midicmd: f,
midibend: D,
miditouch: O,
polyTouch: re,
gain: z = t.gain,
velocity: E = t.velocity,
progNum: S,
sysexid: k,
sysexdata: B,
midimap: p = t.midimap,
midiport: W = t.midiport
} = i.value;
const a = $(W, s.outputs);
if (!a) {
m(
`[midi] midiport "${W}" not found! available: ${s.outputs.map((b) => `'${b.name}'`).join(", ")}`
);
return;
}
if (E = z * E, h.has(p) ? Z(h.get(p), i.value).forEach(({ ccn: M, ccv: v }) => A(M, v, a, u, d)) : p !== "default" && m(`[midi] midimap "${p}" not found! Available maps: ${[...h.keys()].join(", ")}`), I !== void 0 && !t.isController) {
const b = i.duration.valueOf() / o * 1e3 - t.noteOffsetMs;
ie(I, E, b, a, u, d);
}
if (S !== void 0 && L(S, a, u, d), k !== void 0 && B !== void 0 && U(k, B, a, d), P !== void 0 && j !== void 0 && A(j, P, a, u, d), N !== void 0 && x !== void 0 && ee(N, x, a, u, d), D !== void 0 && ne(D, a, u, d), O !== void 0 && te(O, a, u, d), i.whole.begin + 0 === 0 && a.sendStart({ time: d }), ["clock", "midiClock"].includes(f))
a.sendClock({ time: d });
else if (["start"].includes(f))
a.sendStart({ time: d });
else if (["stop"].includes(f))
a.sendStop({ time: d });
else if (["continue"].includes(f))
a.sendContinue({ time: d });
else if (Array.isArray(f)) {
if (f[0] === "progNum")
L(f[1], a, u, d);
else if (f[0] === "cc")
f.length === 2 && A(f[0], f[1] / 127, a, u, d);
else if (f[0] === "sysex" && f.length === 3) {
const [b, M, v] = f;
U(M, v, a, d);
}
}
});
};
let y = {};
const w = {};
async function ce(e) {
if (q(e))
throw new Error(
`midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${s.outputs?.[0]?.name || "IAC Driver Bus 1"}')`
);
const n = await T(), t = $(e, s.inputs);
if (!t)
throw new Error(
`midiin: device "${e}" not found.. connected devices: ${g(s.inputs)}`
);
if (n) {
const r = s.inputs.filter((o) => o.name !== t.name);
m(
`Midi enabled! Using "${t.name}". ${r?.length ? `Also available: ${g(r)}` : ""}`
);
}
w[e] || (w[e] = {});
const i = (r) => J(() => w[e][r] || 0);
return y[e] && t.removeListener("midimessage", y[e]), y[e] = (r) => {
const o = r.dataBytes[0], c = r.dataBytes[1];
w[e] && (w[e][o] = c / 127);
}, t.addListener("midimessage", y[e]), i;
}
export {
s as WebMidi,
ae as defaultmidimap,
T as enableWebMidi,
h as midicontrolMap,
de as midimaps,
ce as midin,
fe as midisoundMap
};