UNPKG

@strudel/midi

Version:

Midi API for strudel

275 lines (274 loc) 9.57 kB
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 };