exquis-midi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Manuel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # exquis-midi
2
+
3
+ Full control of the [Intuitive Instruments **Exquis**](https://dualo.com/en/exquis/) β€” the
4
+ hexagonal, MPE, RGB-lit MIDI controller β€” from TypeScript, in **Node and the browser**, over the
5
+ device's official Developer Mode MIDI API. No custom firmware.
6
+
7
+ - πŸŽ› **Full device control** β€” LEDs (batched, diffed), pads, encoders, buttons, slider, palette,
8
+ tempo, scales, snapshots.
9
+ - 🧩 **Typed everything** β€” a semantic `EventEmitter` (`padPress`, `encoder`, `button`, …) and typed
10
+ SysEx encoders/decoder.
11
+ - 🌈 **LED engine** β€” 7-bit colour with HSV/hex helpers, effects, and a frame buffer that coalesces
12
+ changes into the fewest possible `cmd 04` writes (one SysEx for a whole-board repaint).
13
+ - 🎼 **Microtonal** β€” EDO & Scala (`.scl`/`.kbm`) tunings, isomorphic hex layouts (Exquis / Harmonic
14
+ Table / JankΓ³, incl. Lumatone EDO presets), and MPE / MTS emitters to play any tuning through a synth.
15
+ - πŸ”Œ **One API, two backends** β€” [`@julusian/midi`](https://www.npmjs.com/package/@julusian/midi) on
16
+ Node (optional), native Web MIDI in the browser.
17
+
18
+ > The Exquis is 12-EDO internally, so genuine microtonality happens **host-side**: this library retunes
19
+ > and emits MPE (or MTS) to a downstream synth.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ npm install exquis-midi
25
+ # Node only: also install the MIDI backend (optional peer dependency)
26
+ npm install @julusian/midi
27
+ ```
28
+
29
+ The browser build uses the native Web MIDI API and needs no extra dependency.
30
+
31
+ ## Quick start β€” Node
32
+
33
+ ```ts
34
+ import { Exquis, hsv } from 'exquis-midi';
35
+
36
+ const exq = await Exquis.connect({ zones: ['pads', 'encoders'] }); // enters dev mode
37
+
38
+ // light a pad on press (buffered + diffed β†’ one cmd 04)
39
+ exq.on('padPress', (e) => exq.leds.set(e.pad, hsv(e.pad / 61, 1, 1)));
40
+ exq.on('encoder', (e) => console.log(`encoder ${e.index} ${e.delta > 0 ? '+' : ''}${e.delta}`));
41
+
42
+ // …later
43
+ await exq.disconnect(); // clears LEDs, exits dev mode, closes ports
44
+ ```
45
+
46
+ ## Quick start β€” Browser
47
+
48
+ ```ts
49
+ import { Exquis } from 'exquis-midi';
50
+
51
+ // requires a user gesture + Web MIDI (Chrome/Edge) with sysex permission
52
+ const exq = await Exquis.connect();
53
+ exq.on('padPress', (e) => exq.leds.set(e.pad, { r: 127, g: 0, b: 0 }));
54
+ ```
55
+
56
+ ## LEDs
57
+
58
+ ```ts
59
+ exq.leds.set(pad, hsv(0.6, 1, 1)); // one pad
60
+ exq.leds.setMany([
61
+ [0, RED],
62
+ [1, GREEN],
63
+ ]); // several
64
+ exq.leds.all(BLACK); // every pad
65
+ exq.leds.effect(pad, Fx.pulsateWhite()); // alpha / blendWhite / pulsate*
66
+ exq.leds.palette.set(3, rgb(10, 20, 30)); // 128-entry palette (CC colouring)
67
+ exq.leds.flush(); // auto-flushes on the next microtask, or force it
68
+ ```
69
+
70
+ Colours are **7-bit** (`0..127`). Use `rgb`, `rgb255` (scales 8-bit), `hsv`, `hex`, `lerp`.
71
+
72
+ ## Events
73
+
74
+ `padPress` Β· `padRelease` Β· `pressure` Β· `bend` Β· `controlChange` Β· `encoder` Β· `button` Β· `slider`
75
+ Β· `noteOn` Β· `noteOff` Β· `tempo` Β· `rootNote` Β· `scale` Β· `refresh` Β· `sysex` Β· `message` Β·
76
+ `connect` Β· `disconnect`.
77
+
78
+ Dev mode reports pads as ids `0..60` on channel 16 (no per-note expression); `mode: 'mpe'` leaves the
79
+ device native for full MPE expression (velocity, pressure, pitch-bend, CC 74).
80
+
81
+ ## Microtonal
82
+
83
+ ```ts
84
+ import {
85
+ Exquis,
86
+ Layouts,
87
+ LayoutPlayer,
88
+ MpeEmitter,
89
+ EqualTemperament,
90
+ ScaleTuning,
91
+ } from 'exquis-midi';
92
+
93
+ const exq = await Exquis.connect({ zones: ['pads'] });
94
+
95
+ // pick a tuning + isomorphic layout
96
+ const layout = Layouts.jankoEDO(31); // 31-EDO JankΓ³ (Lumatone dilation)
97
+ // or: Layouts.exquisDefault(EqualTemperament.edo(19))
98
+ // or: Layouts.fromGenerators({ right: 1, upRight: 6 }, ScaleTuning.fromScl(sclText))
99
+
100
+ // play it through any MPE synth output
101
+ const mpe = new MpeEmitter((bytes) => synthOut.send(bytes), { bendRangeSemitones: 48 });
102
+ mpe.configure();
103
+ new LayoutPlayer(exq, layout, mpe).start(); // colours the scale + routes presses
104
+ ```
105
+
106
+ Importing Scala files:
107
+
108
+ ```ts
109
+ import { ScaleTuning, parseKbm, scaleFromSclBlob } from 'exquis-midi';
110
+ const tuning = ScaleTuning.fromScl(sclText); // from a string
111
+ const tuning2 = await scaleFromSclBlob(fileInput.files[0]); // from a browser File / Blob
112
+
113
+ import { loadSclFile } from 'exquis-midi'; // Node file path
114
+ const tuning3 = await loadSclFile('./scales/young.scl');
115
+ ```
116
+
117
+ ## Hex grid
118
+
119
+ ```ts
120
+ import { rowCol, neighbors, bfsDist, ring } from 'exquis-midi';
121
+ rowCol(60); // { row: 10, col: 5 }
122
+ neighbors(27); // adjacent pad ids
123
+ bfsDist(0); // hex hop-count from pad 0 to every pad (ripples, layouts)
124
+ ```
125
+
126
+ ## Examples
127
+
128
+ ```sh
129
+ npm run example:web # interactive playground (Chrome/Edge)
130
+ npx tsx examples/node/monitor.ts # watch what the device sends (with audio feedback)
131
+ npx tsx examples/node/microtonal.ts # microtonal MIDI source β†’ your synth/VST
132
+ npx tsx examples/node/ripple.ts # hex-grid ripple
133
+ ```
134
+
135
+ The **web playground** (`examples/web`) has a device console, generative visuals, a microtonal player
136
+ with a built-in Web Audio synth, and an isomorphic layout/tuning designer with `.scl` export β€” all
137
+ usable without hardware (click the on-screen pads).
138
+
139
+ ### Microtonal β†’ your synth/VST
140
+
141
+ `examples/node/microtonal.ts` is a pure MIDI source β€” no built-in synth. By default it opens a virtual
142
+ MIDI port **`exquis-midi`**; select it as input in your synth/DAW. Live controls: encoder 1 = transpose
143
+ Β± step, Up/Down = transpose Β± octave.
144
+
145
+ ```sh
146
+ npx tsx examples/node/microtonal.ts --edo 31 --family janko # tuning + isomorphic layout
147
+ npx tsx examples/node/microtonal.ts --edo 12 --scale major # lock to a scale (12-EDO only)
148
+ npx tsx examples/node/microtonal.ts --out IAC # send to an existing port instead
149
+ npx tsx examples/node/microtonal.ts --log-midi # also print every message sent
150
+ ```
151
+
152
+ **Microtonal polyphony needs a compatible mode** β€” plain MIDI is 12-EDO only, so the retuning rides on
153
+ pitch bend (MPE) or a tuning message (MTS). Pick what your synth supports:
154
+
155
+ | Mode | Flag | Requirement | Polyphonic |
156
+ | ------------- | -------- | ------------------------------------------------- | ---------- |
157
+ | MPE (default) | β€” | synth in MPE mode, pitch-bend range 48 (Surge XT) | βœ… |
158
+ | MTS | `--mts` | synth reads MIDI-SysEx tuning (e.g. Pianoteq) | βœ… |
159
+ | mono + bend | `--mono` | any synth (ch1 + pitch bend; set its range to 2) | one note |
160
+
161
+ ```sh
162
+ npx tsx examples/node/microtonal.ts # MPE β€” recommended (Surge XT: enable MPE, range 48)
163
+ npx tsx examples/node/microtonal.ts --mono # any synth, one note at a time (bend range 2)
164
+ ```
165
+
166
+ Note: Surge XT retunes via **MPE** or MTS-ESP, not MIDI-SysEx tuning β€” use the default MPE mode for it.
167
+
168
+ To watch the **device's own** output instead, use `examples/node/monitor.ts` or the web playground's
169
+ Device console. On macOS, MIDI Monitor (snoize.com) shows every message on the `exquis-midi` port.
170
+
171
+ ## Protocol
172
+
173
+ Built on the new-dialect Developer Mode SysEx frame `F0 00 21 7E 7F <cmd> … F7` (firmware 2.x/3.x).
174
+ The official **Developer Mode MIDI specification** and 3D models are on the
175
+ [Exquis Resources page](https://dualo.com/en/exquis-resources/); the microtonal layout math follows
176
+ the _Microtonal Layouts for the Exquis_ document. (Those PDFs aren't redistributed here β€” grab them
177
+ from the source and drop them in `docs/`.)
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,4 @@
1
+ export { openNodeOutput, openNodeTransport, openNodeVirtualOutput } from './chunk-7W2FT4RZ.js';
2
+ import './chunk-C3YWUWRH.js';
3
+ //# sourceMappingURL=NodeMidiTransport-L7VRGYJ2.js.map
4
+ //# sourceMappingURL=NodeMidiTransport-L7VRGYJ2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"NodeMidiTransport-L7VRGYJ2.js"}
@@ -0,0 +1,4 @@
1
+ export { openWebOutput, openWebTransport } from './chunk-BR4TVV2R.js';
2
+ import './chunk-C3YWUWRH.js';
3
+ //# sourceMappingURL=WebMidiTransport-RAUJGW6C.js.map
4
+ //# sourceMappingURL=WebMidiTransport-RAUJGW6C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"WebMidiTransport-RAUJGW6C.js"}
@@ -0,0 +1,66 @@
1
+ import { matchesPort } from './chunk-C3YWUWRH.js';
2
+
3
+ // src/transport/NodeMidiTransport.ts
4
+ async function loadMidi() {
5
+ try {
6
+ const mod = await import('@julusian/midi');
7
+ return mod.Input ? mod : mod.default;
8
+ } catch {
9
+ throw new Error(
10
+ "The '@julusian/midi' package is required for the Node MIDI backend. Install it with `npm i @julusian/midi`."
11
+ );
12
+ }
13
+ }
14
+ function findPort(port, match) {
15
+ for (let i = 0; i < port.getPortCount(); i++) {
16
+ if (matchesPort(port.getPortName(i), match)) return i;
17
+ }
18
+ return -1;
19
+ }
20
+ async function openNodeTransport(match = /exquis/i) {
21
+ const midi = await loadMidi();
22
+ const input = new midi.Input();
23
+ const output = new midi.Output();
24
+ const inIdx = findPort(input, match);
25
+ const outIdx = findPort(output, match);
26
+ if (inIdx < 0 || outIdx < 0) {
27
+ input.closePort?.();
28
+ output.closePort?.();
29
+ throw new Error(`No MIDI port matching ${String(match)} found (need both input and output).`);
30
+ }
31
+ input.ignoreTypes(false, true, true);
32
+ output.openPort(outIdx);
33
+ input.openPort(inIdx);
34
+ return {
35
+ send: (bytes) => output.sendMessage(bytes),
36
+ onMessage: (cb) => input.on("message", (_dt, message) => cb(message)),
37
+ close: async () => {
38
+ input.closePort();
39
+ output.closePort();
40
+ }
41
+ };
42
+ }
43
+ async function openNodeVirtualOutput(name = "exquis-midi") {
44
+ const midi = await loadMidi();
45
+ const output = new midi.Output();
46
+ output.openVirtualPort(name);
47
+ return {
48
+ send: (bytes) => output.sendMessage(bytes),
49
+ close: async () => output.closePort()
50
+ };
51
+ }
52
+ async function openNodeOutput(match) {
53
+ const midi = await loadMidi();
54
+ const output = new midi.Output();
55
+ const idx = findPort(output, match);
56
+ if (idx < 0) throw new Error(`No MIDI output matching ${String(match)} found.`);
57
+ output.openPort(idx);
58
+ return {
59
+ send: (bytes) => output.sendMessage(bytes),
60
+ close: async () => output.closePort()
61
+ };
62
+ }
63
+
64
+ export { openNodeOutput, openNodeTransport, openNodeVirtualOutput };
65
+ //# sourceMappingURL=chunk-7W2FT4RZ.js.map
66
+ //# sourceMappingURL=chunk-7W2FT4RZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/NodeMidiTransport.ts"],"names":[],"mappings":"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","file":"chunk-7W2FT4RZ.js","sourcesContent":["import { matchesPort, type Transport, type Output } from './Transport';\n\n// Minimal shape of the `@julusian/midi` module we rely on (avoids a hard type dep).\ninterface JzMidiPort {\n getPortCount(): number;\n getPortName(i: number): string;\n openPort(i: number): void;\n closePort(): void;\n}\ninterface JzInput extends JzMidiPort {\n on(event: 'message', cb: (deltaTime: number, message: number[]) => void): void;\n ignoreTypes(sysex: boolean, timing: boolean, activeSensing: boolean): void;\n}\ninterface JzOutput extends JzMidiPort {\n sendMessage(bytes: number[]): void;\n openVirtualPort(name: string): void;\n}\ninterface JzMidiModule {\n Input: new () => JzInput;\n Output: new () => JzOutput;\n}\n\nasync function loadMidi(): Promise<JzMidiModule> {\n try {\n // Lazy, so browser bundles never resolve the native addon.\n const mod = (await import('@julusian/midi')) as unknown as JzMidiModule & {\n default?: JzMidiModule;\n };\n return mod.Input ? mod : (mod.default as JzMidiModule);\n } catch {\n throw new Error(\n \"The '@julusian/midi' package is required for the Node MIDI backend. Install it with `npm i @julusian/midi`.\",\n );\n }\n}\n\nfunction findPort(port: JzMidiPort, match: RegExp | string): number {\n for (let i = 0; i < port.getPortCount(); i++) {\n if (matchesPort(port.getPortName(i), match)) return i;\n }\n return -1;\n}\n\n/** Open a bidirectional Node transport to the first Exquis port. */\nexport async function openNodeTransport(match: RegExp | string = /exquis/i): Promise<Transport> {\n const midi = await loadMidi();\n const input = new midi.Input();\n const output = new midi.Output();\n const inIdx = findPort(input, match);\n const outIdx = findPort(output, match);\n if (inIdx < 0 || outIdx < 0) {\n input.closePort?.();\n output.closePort?.();\n throw new Error(`No MIDI port matching ${String(match)} found (need both input and output).`);\n }\n input.ignoreTypes(false, true, true); // receive SysEx; drop clock + active-sensing\n output.openPort(outIdx);\n input.openPort(inIdx);\n return {\n send: (bytes) => output.sendMessage(bytes),\n onMessage: (cb) => input.on('message', (_dt, message) => cb(message)),\n close: async () => {\n input.closePort();\n output.closePort();\n },\n };\n}\n\n/**\n * Create a **virtual** Node MIDI output port (macOS/Linux). It appears as a MIDI source\n * named `name` that any DAW, synth, or MIDI monitor can subscribe to β€” handy for testing\n * what the library actually sends.\n */\nexport async function openNodeVirtualOutput(name = 'exquis-midi'): Promise<Output> {\n const midi = await loadMidi();\n const output = new midi.Output();\n output.openVirtualPort(name);\n return {\n send: (bytes) => output.sendMessage(bytes),\n close: async () => output.closePort(),\n };\n}\n\n/** Open a one-way Node output (e.g. to a synth) to the first matching port. */\nexport async function openNodeOutput(match: RegExp | string): Promise<Output> {\n const midi = await loadMidi();\n const output = new midi.Output();\n const idx = findPort(output, match);\n if (idx < 0) throw new Error(`No MIDI output matching ${String(match)} found.`);\n output.openPort(idx);\n return {\n send: (bytes) => output.sendMessage(bytes),\n close: async () => output.closePort(),\n };\n}\n"]}
@@ -0,0 +1,53 @@
1
+ import { matchesPort } from './chunk-C3YWUWRH.js';
2
+
3
+ // src/transport/WebMidiTransport.ts
4
+ function findByName(ports, match) {
5
+ for (const p of ports) if (p.name && matchesPort(p.name, match)) return p;
6
+ return void 0;
7
+ }
8
+ async function access() {
9
+ if (typeof navigator === "undefined" || typeof navigator.requestMIDIAccess !== "function") {
10
+ throw new Error("Web MIDI is not available in this environment.");
11
+ }
12
+ return navigator.requestMIDIAccess({ sysex: true });
13
+ }
14
+ async function openWebTransport(match = /exquis/i) {
15
+ const midi = await access();
16
+ const input = findByName(midi.inputs.values(), match);
17
+ const output = findByName(midi.outputs.values(), match);
18
+ if (!input || !output) {
19
+ throw new Error(
20
+ `No Web MIDI port matching ${String(match)} found (need both input and output).`
21
+ );
22
+ }
23
+ let handler;
24
+ input.onmidimessage = (e) => {
25
+ if (e.data) handler?.(Array.from(e.data));
26
+ };
27
+ return {
28
+ send: (bytes) => output.send(bytes),
29
+ onMessage: (cb) => {
30
+ handler = cb;
31
+ },
32
+ close: async () => {
33
+ input.onmidimessage = null;
34
+ await input.close();
35
+ await output.close();
36
+ }
37
+ };
38
+ }
39
+ async function openWebOutput(match) {
40
+ const midi = await access();
41
+ const output = findByName(midi.outputs.values(), match);
42
+ if (!output) throw new Error(`No Web MIDI output matching ${String(match)} found.`);
43
+ return {
44
+ send: (bytes) => output.send(bytes),
45
+ close: async () => {
46
+ await output.close();
47
+ }
48
+ };
49
+ }
50
+
51
+ export { openWebOutput, openWebTransport };
52
+ //# sourceMappingURL=chunk-BR4TVV2R.js.map
53
+ //# sourceMappingURL=chunk-BR4TVV2R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/WebMidiTransport.ts"],"names":[],"mappings":";;;AAEA,SAAS,UAAA,CACP,OACA,KAAA,EACe;AACf,EAAA,KAAA,MAAW,CAAA,IAAK,KAAA,EAAO,IAAI,CAAA,CAAE,IAAA,IAAQ,YAAY,CAAA,CAAE,IAAA,EAAM,KAAK,CAAA,EAAG,OAAO,CAAA;AACxE,EAAA,OAAO,MAAA;AACT;AAEA,eAAe,MAAA,GAA8B;AAC3C,EAAA,IAAI,OAAO,SAAA,KAAc,WAAA,IAAe,OAAO,SAAA,CAAU,sBAAsB,UAAA,EAAY;AACzF,IAAA,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAAA,EAClE;AACA,EAAA,OAAO,SAAA,CAAU,iBAAA,CAAkB,EAAE,KAAA,EAAO,MAAM,CAAA;AACpD;AAGA,eAAsB,gBAAA,CAAiB,QAAyB,SAAA,EAA+B;AAC7F,EAAA,MAAM,IAAA,GAAO,MAAM,MAAA,EAAO;AAC1B,EAAA,MAAM,QAAQ,UAAA,CAAW,IAAA,CAAK,MAAA,CAAO,MAAA,IAAU,KAAK,CAAA;AACpD,EAAA,MAAM,SAAS,UAAA,CAAW,IAAA,CAAK,OAAA,CAAQ,MAAA,IAAU,KAAK,CAAA;AACtD,EAAA,IAAI,CAAC,KAAA,IAAS,CAAC,MAAA,EAAQ;AACrB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,0BAAA,EAA6B,MAAA,CAAO,KAAK,CAAC,CAAA,oCAAA;AAAA,KAC5C;AAAA,EACF;AACA,EAAA,IAAI,OAAA;AACJ,EAAA,KAAA,CAAM,aAAA,GAAgB,CAAC,CAAA,KAAwB;AAC7C,IAAA,IAAI,EAAE,IAAA,EAAM,OAAA,GAAU,MAAM,IAAA,CAAK,CAAA,CAAE,IAAI,CAAC,CAAA;AAAA,EAC1C,CAAA;AACA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,CAAC,KAAA,KAAU,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IAClC,SAAA,EAAW,CAAC,EAAA,KAAO;AACjB,MAAA,OAAA,GAAU,EAAA;AAAA,IACZ,CAAA;AAAA,IACA,OAAO,YAAY;AACjB,MAAA,KAAA,CAAM,aAAA,GAAgB,IAAA;AACtB,MAAA,MAAM,MAAM,KAAA,EAAM;AAClB,MAAA,MAAM,OAAO,KAAA,EAAM;AAAA,IACrB;AAAA,GACF;AACF;AAGA,eAAsB,cAAc,KAAA,EAAyC;AAC3E,EAAA,MAAM,IAAA,GAAO,MAAM,MAAA,EAAO;AAC1B,EAAA,MAAM,SAAS,UAAA,CAAW,IAAA,CAAK,OAAA,CAAQ,MAAA,IAAU,KAAK,CAAA;AACtD,EAAA,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,CAAA,4BAAA,EAA+B,MAAA,CAAO,KAAK,CAAC,CAAA,OAAA,CAAS,CAAA;AAClF,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,CAAC,KAAA,KAAU,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IAClC,OAAO,YAAY;AACjB,MAAA,MAAM,OAAO,KAAA,EAAM;AAAA,IACrB;AAAA,GACF;AACF","file":"chunk-BR4TVV2R.js","sourcesContent":["import { matchesPort, type Transport, type Output } from './Transport';\n\nfunction findByName<T extends { name: string | null }>(\n ports: Iterable<T>,\n match: RegExp | string,\n): T | undefined {\n for (const p of ports) if (p.name && matchesPort(p.name, match)) return p;\n return undefined;\n}\n\nasync function access(): Promise<MIDIAccess> {\n if (typeof navigator === 'undefined' || typeof navigator.requestMIDIAccess !== 'function') {\n throw new Error('Web MIDI is not available in this environment.');\n }\n return navigator.requestMIDIAccess({ sysex: true });\n}\n\n/** Open a bidirectional Web MIDI transport to the first Exquis port. */\nexport async function openWebTransport(match: RegExp | string = /exquis/i): Promise<Transport> {\n const midi = await access();\n const input = findByName(midi.inputs.values(), match);\n const output = findByName(midi.outputs.values(), match);\n if (!input || !output) {\n throw new Error(\n `No Web MIDI port matching ${String(match)} found (need both input and output).`,\n );\n }\n let handler: ((bytes: number[]) => void) | undefined;\n input.onmidimessage = (e: MIDIMessageEvent) => {\n if (e.data) handler?.(Array.from(e.data));\n };\n return {\n send: (bytes) => output.send(bytes),\n onMessage: (cb) => {\n handler = cb;\n },\n close: async () => {\n input.onmidimessage = null;\n await input.close();\n await output.close();\n },\n };\n}\n\n/** Open a one-way Web MIDI output (e.g. to a synth) to the first matching port. */\nexport async function openWebOutput(match: RegExp | string): Promise<Output> {\n const midi = await access();\n const output = findByName(midi.outputs.values(), match);\n if (!output) throw new Error(`No Web MIDI output matching ${String(match)} found.`);\n return {\n send: (bytes) => output.send(bytes),\n close: async () => {\n await output.close();\n },\n };\n}\n"]}
@@ -0,0 +1,7 @@
1
+ // src/transport/Transport.ts
2
+ var toTest = (match) => typeof match === "string" ? (name) => name.toLowerCase().includes(match.toLowerCase()) : (name) => match.test(name);
3
+ var matchesPort = (name, match) => toTest(match)(name);
4
+
5
+ export { matchesPort };
6
+ //# sourceMappingURL=chunk-C3YWUWRH.js.map
7
+ //# sourceMappingURL=chunk-C3YWUWRH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/Transport.ts"],"names":[],"mappings":";AAwBA,IAAM,MAAA,GAAS,CAAC,KAAA,KACd,OAAO,UAAU,QAAA,GACb,CAAC,SAAS,IAAA,CAAK,WAAA,GAAc,QAAA,CAAS,KAAA,CAAM,aAAa,CAAA,GACzD,CAAC,IAAA,KAAS,KAAA,CAAM,KAAK,IAAI,CAAA;AAGxB,IAAM,cAAc,CAAC,IAAA,EAAc,UAAoC,MAAA,CAAO,KAAK,EAAE,IAAI","file":"chunk-C3YWUWRH.js","sourcesContent":["/** An opened, bidirectional MIDI connection to an Exquis. */\nexport interface Transport {\n /** Send raw MIDI bytes (including SysEx) to the device. */\n send(bytes: number[]): void;\n /** Register a handler for every inbound MIDI message. */\n onMessage(cb: (bytes: number[]) => void): void;\n /** Close the underlying ports. */\n close(): Promise<void>;\n}\n\n/** A minimal one-way MIDI output (e.g. to a downstream synth). */\nexport interface Output {\n send(bytes: number[]): void;\n close(): Promise<void>;\n}\n\nexport interface MidiPort {\n id: string;\n name: string;\n}\n\n/** Opens a `Transport` to the first port matching `match` (default `/exquis/i`). */\nexport type OpenTransport = (match?: RegExp | string) => Promise<Transport>;\n\nconst toTest = (match: RegExp | string): ((name: string) => boolean) =>\n typeof match === 'string'\n ? (name) => name.toLowerCase().includes(match.toLowerCase())\n : (name) => match.test(name);\n\n/** Shared helper: does a port name match the requested pattern? */\nexport const matchesPort = (name: string, match: RegExp | string): boolean => toTest(match)(name);\n"]}