@m4l-jweb/bridge 1.3.1 → 1.6.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/package.json +1 -1
- package/src/index.ts +141 -7
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -215,6 +215,139 @@ export const CHAIN_OUT = {
|
|
|
215
215
|
save_end: "save_end",
|
|
216
216
|
} as const;
|
|
217
217
|
|
|
218
|
+
/* ------------------------------------------------------------------ *
|
|
219
|
+
* The CONTROL SURFACE contract - the pads, declared with defineControls()
|
|
220
|
+
*
|
|
221
|
+
* A device that claims a Push control gets three things across the bridge, and
|
|
222
|
+
* they are the whole vocabulary: pad events IN, a frame OUT, and a running answer
|
|
223
|
+
* to "do we actually hold it right now".
|
|
224
|
+
*
|
|
225
|
+
* THE KEY IS PART OF THE SELECTOR ON THE WAY IN AND AN ARGUMENT ON THE WAY OUT,
|
|
226
|
+
* for the same reason a state slot's id is: the BRIDGE dispatches on the selector,
|
|
227
|
+
* so one binding per declared control means the app never unpacks a key; MAX
|
|
228
|
+
* dispatches on the first word, so an id baked into an outbound selector goes
|
|
229
|
+
* looking for a handler no device has. `pad_<key>` in, `controls_frame <key> ...`
|
|
230
|
+
* out. Neither name is yours to type - the hooks derive both from the declaration.
|
|
231
|
+
*
|
|
232
|
+
* Y COUNTS FROM THE TOP ON THIS WIRE. The hardware numbers rows downward and
|
|
233
|
+
* `defineControls`' API numbers them upward (see PUSH-USECASES.md), and the flip
|
|
234
|
+
* happens in exactly ONE place - `usePadGrid` - so everything either side of it is
|
|
235
|
+
* in its own natural orientation. Get that wrong somewhere else and every device on
|
|
236
|
+
* the grid is mirrored vertically with nothing to report it.
|
|
237
|
+
* ------------------------------------------------------------------ */
|
|
238
|
+
|
|
239
|
+
/** Selectors the TAKEOVER chain and the wrapper send to a device that declares controls. */
|
|
240
|
+
export const CONTROLS_IN = {
|
|
241
|
+
/**
|
|
242
|
+
* chain -> UI: `pad_<key> <value> <x> <yFromTop> <unknown>` - one event off the
|
|
243
|
+
* grabbed control's `value`, straight out of a `[live.observer]` with no `[js]`
|
|
244
|
+
* between the hardware and the page.
|
|
245
|
+
*
|
|
246
|
+
* MEASURED on a Push 3 (doc/MAX-FACTS.md): the value is four atoms,
|
|
247
|
+
* `<velocity> <x> <y> <1>`, y from the TOP, and nothing arrives between a press
|
|
248
|
+
* and its release - no pressure, no slide. The fourth atom has been `1` on every
|
|
249
|
+
* event observed and is passed through unnamed rather than guessed at.
|
|
250
|
+
*
|
|
251
|
+
* `live.observer` also emits the property's CURRENT value the moment it is
|
|
252
|
+
* pointed at an object, which for a control that has never been touched is not a
|
|
253
|
+
* press: the arity is what tells them apart, and `usePadGrid` drops anything
|
|
254
|
+
* shorter than three atoms.
|
|
255
|
+
*
|
|
256
|
+
* A `stream` control (a jog wheel, a touch strip) uses the same selector with
|
|
257
|
+
* whatever atoms its own `value` carries.
|
|
258
|
+
*/
|
|
259
|
+
pad: "pad_",
|
|
260
|
+
/**
|
|
261
|
+
* wrapper -> UI: `controls_role <key> <1 resolved | 0 not on this hardware>`.
|
|
262
|
+
*
|
|
263
|
+
* A Push 3 answers `get_control_names` with 176 names and they are NOT the Push 2
|
|
264
|
+
* set, so a role is resolved at runtime against that answer. A role that is not
|
|
265
|
+
* there is the one failure this API can actually report - say so, rather than
|
|
266
|
+
* grabbing nothing and looking grabbed.
|
|
267
|
+
*/
|
|
268
|
+
controls_role: "controls_role",
|
|
269
|
+
/**
|
|
270
|
+
* wrapper -> UI: `controls_held <0|1> <reason>` - do we own the declared controls
|
|
271
|
+
* RIGHT NOW, and if not, why not?
|
|
272
|
+
*
|
|
273
|
+
* It is not the same question as "is `takeover` on": the `focus` parameter decides
|
|
274
|
+
* whether an enabled device holds the grid while another track is selected, and two
|
|
275
|
+
* of these devices in one set is the normal case. Resent on `ui_ready`.
|
|
276
|
+
*
|
|
277
|
+
* THE REASON IS THE POINT. `off`, `no_surface`, `unresolved` and `not_focused` are
|
|
278
|
+
* four different problems that look identical on the hardware - a dark Push - and
|
|
279
|
+
* Live will not tell anyone apart from them, because a rejected grab is a console
|
|
280
|
+
* line and a normal return. This is the wrapper reporting the decision it made,
|
|
281
|
+
* which is the only half that can be known in code.
|
|
282
|
+
*/
|
|
283
|
+
controls_held: "controls_held",
|
|
284
|
+
} as const;
|
|
285
|
+
|
|
286
|
+
/** Selectors the wrapper RECEIVES from a device that declares controls. */
|
|
287
|
+
export const CONTROLS_OUT = {
|
|
288
|
+
/**
|
|
289
|
+
* UI -> wrapper: `controls_frame <key> <c0> <c1> ... <c63>` - the WHOLE grid, as
|
|
290
|
+
* palette indices, row-major from the TOP-LEFT in hardware order.
|
|
291
|
+
*
|
|
292
|
+
* One message per frame, not one per cell: sixty-four messages a frame is a data
|
|
293
|
+
* plane and `[js]` is a control plane. The wrapper keeps the last frame it
|
|
294
|
+
* actually sent to the hardware and issues `send_value` only for the cells that
|
|
295
|
+
* changed, so a device redraws freely - every tick, every state change - and pays
|
|
296
|
+
* for the pads that moved. A blinking cursor costs one cell per blink.
|
|
297
|
+
*/
|
|
298
|
+
controls_frame: "controls_frame",
|
|
299
|
+
/**
|
|
300
|
+
* UI -> wrapper: `controls_refresh` - forget the last frame and repaint everything.
|
|
301
|
+
*
|
|
302
|
+
* The page uses it after a re-grab, because Live repaints the matrix as it hands
|
|
303
|
+
* it over and the wrapper's idea of what is lit is then a lie.
|
|
304
|
+
*/
|
|
305
|
+
controls_refresh: "controls_refresh",
|
|
306
|
+
} as const;
|
|
307
|
+
|
|
308
|
+
/** The inbound selector one declared control's events arrive on. */
|
|
309
|
+
export const padSelector = (key: string): string => `${CONTROLS_IN.pad}${key}`;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Open a URL in the user's real web browser.
|
|
313
|
+
*
|
|
314
|
+
* A page inside `[jweb]` cannot do this itself. It is a `file://` document, and an
|
|
315
|
+
* ordinary `<a target="_blank">` either does nothing or navigates the device view - which
|
|
316
|
+
* replaces your UI with a web page inside a 169 px box, with no way back.
|
|
317
|
+
*
|
|
318
|
+
* So the page asks Max, and Max asks the shell: `; max launchbrowser <url>`. That message
|
|
319
|
+
* is measured to REACH the shell (doc/MAX-FACTS.md - a wrong `file://` path raised a real
|
|
320
|
+
* "cannot find the file" dialog naming it). What it cannot do is reveal a FOLDER, which is
|
|
321
|
+
* a different intent and is why `copyPath()` exists; opening an `http(s)` URL is the thing
|
|
322
|
+
* `launchbrowser` is actually for.
|
|
323
|
+
*/
|
|
324
|
+
export const SHELL_OUT = {
|
|
325
|
+
/** UI -> wrapper: `open_url <url>` - hand it to the default browser. */
|
|
326
|
+
open_url: "open_url",
|
|
327
|
+
} as const;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Open `url` in the user's browser - in Live, and in the dev harness.
|
|
331
|
+
*
|
|
332
|
+
* MAX SPLITS A MESSAGE ON WHITESPACE, and treats `,` and `;` as message separators, so a
|
|
333
|
+
* URL containing any of them arrives at the wrapper in pieces. Rather than guess at an
|
|
334
|
+
* escaping the receiving end would have to undo, this refuses such a URL and says so - a
|
|
335
|
+
* link that silently opened the wrong page would be worse than one that did not open.
|
|
336
|
+
*/
|
|
337
|
+
export function openUrl(url: string): boolean {
|
|
338
|
+
if (/[\s,;]/.test(url)) {
|
|
339
|
+
console.warn(`[m4l-jweb] openUrl: "${url}" contains whitespace, a comma or a semicolon, which Max would split the message on`);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
// In a browser there is no Max to ask, and a new tab is the same intent.
|
|
343
|
+
if (!inJweb) {
|
|
344
|
+
window.open(url, "_blank", "noopener");
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
outlet(SHELL_OUT.open_url, url);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
218
351
|
/**
|
|
219
352
|
* Selectors the WRAPPER handles for a device that declares `state` in its surface.
|
|
220
353
|
*
|
|
@@ -359,10 +492,7 @@ export const PARAM_IN = {
|
|
|
359
492
|
* behaviour, and is worth it only where a unit readout on the dial beats
|
|
360
493
|
* automation and macros on it.
|
|
361
494
|
*/
|
|
362
|
-
export function describeParam(
|
|
363
|
-
id: string,
|
|
364
|
-
desc: { name?: string; unit?: string; range?: [number, number]; widenRange?: boolean },
|
|
365
|
-
): void {
|
|
495
|
+
export function describeParam(id: string, desc: { name?: string; unit?: string; range?: [number, number]; widenRange?: boolean }): void {
|
|
366
496
|
if (desc.name) outlet(PARAM_OUT.param_label, id, desc.name);
|
|
367
497
|
if (desc.unit) outlet(PARAM_OUT.param_unit, id, desc.unit);
|
|
368
498
|
if (desc.widenRange && desc.range && desc.range[1] > desc.range[0]) {
|
|
@@ -654,8 +784,6 @@ export function onDeviceFolder(fn: (folder: string) => void): () => void {
|
|
|
654
784
|
};
|
|
655
785
|
}
|
|
656
786
|
|
|
657
|
-
|
|
658
|
-
|
|
659
787
|
/* ------------------------------------------------------------------ *
|
|
660
788
|
* Remote - the `remote` chain (live.remote~ modulation)
|
|
661
789
|
* ------------------------------------------------------------------ */
|
|
@@ -790,7 +918,13 @@ function bindClipRead(): void {
|
|
|
790
918
|
clearTimeout(p.timer);
|
|
791
919
|
// The wrapper says WHY: "no_clip" (track has none) or "no_selection" (highlighted
|
|
792
920
|
// slot is empty). Either way there is nothing to read.
|
|
793
|
-
p.reject(
|
|
921
|
+
p.reject(
|
|
922
|
+
new Error(
|
|
923
|
+
String(reason) === "no_selection"
|
|
924
|
+
? "no clip in the highlighted slot - click a clip first"
|
|
925
|
+
: "no clip on this track - create or play a MIDI clip first",
|
|
926
|
+
),
|
|
927
|
+
);
|
|
794
928
|
});
|
|
795
929
|
}
|
|
796
930
|
|