@m4l-jweb/bridge 0.9.9 → 1.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.
Files changed (3) hide show
  1. package/README.md +52 -0
  2. package/package.json +1 -1
  3. package/src/index.ts +73 -1
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @m4l-jweb/bridge
2
+
3
+ The browser half of a device. Your React UI runs inside `[jweb~]`, Max's embedded Chromium; this is how it talks to the Max patcher around it - messages out, messages in, MIDI, clip I/O, and files.
4
+
5
+ Part of **[m4l-jweb](https://github.com/alienmind/m4l-jweb)** - build Ableton Live devices (`.amxd`) from a TypeScript repo: React UI, LiveAPI glue, CI builds, no Max editor.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @m4l-jweb/bridge
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { bindInlet, outlet, onNote, onNoteOff, sendNote, uiReady } from "@m4l-jweb/bridge";
17
+
18
+ // Tell the wrapper the page is up, and ask for current state.
19
+ uiReady();
20
+
21
+ // Incoming MIDI (needs the `midiin` chain). Both binders share one subscription.
22
+ onNote((pitch, velocity) => voiceOn(pitch, velocity));
23
+ onNoteOff((pitch) => voiceOff(pitch));
24
+
25
+ // Outgoing MIDI (needs the `midiout` chain). Max applies the delay, not the browser,
26
+ // so note timing does not depend on a Chromium timer.
27
+ sendNote({ pitch: 60, velocity: 100, durationMs: 250, delayMs: 80 });
28
+
29
+ // Anything else you route yourself.
30
+ bindInlet("tick", (playing, beats) => setTransport(Boolean(playing), Number(beats)));
31
+ outlet("my_selector", 1, "two");
32
+ ```
33
+
34
+ ## Notes
35
+
36
+ - **Audio does not go through here.** Under `[jweb~]` the page's Web Audio output is carried on the object's signal outlets, straight into the track. The bridge is a control plane; sound never crosses it as messages.
37
+ - `fetchToFile()` and `saveToFile()` move bytes between the page and disk via Max's `[maxurl]`, so large files never travel through the message bridge either.
38
+ - `tapMessages()` observes every message in both directions - the whole contract of a device, live.
39
+
40
+ ## Requirements
41
+
42
+ Ableton Live 12 with Max 9. Devices are built on `[jweb~]`, the browser view with signal outlets; older hosts are unverified.
43
+
44
+ ## Links
45
+
46
+ - [Repository and full README](https://github.com/alienmind/m4l-jweb)
47
+ - [Architecture](https://github.com/alienmind/m4l-jweb/blob/main/doc/ARCHITECTURE.md)
48
+ - [What Max actually does: the measured facts](https://github.com/alienmind/m4l-jweb/blob/main/doc/MAX-FACTS.md)
49
+
50
+ ## License
51
+
52
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m4l-jweb/bridge",
3
- "version": "0.9.9",
3
+ "version": "1.1.0",
4
4
  "description": "m4l-jweb: the browser-side bridge connecting a device's web UI to Max for Live.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -127,6 +127,22 @@ export function uiReady(): void {
127
127
  outlet("ui_ready");
128
128
  }
129
129
 
130
+ /**
131
+ * Send a message to the page in another of this device's windows.
132
+ *
133
+ * Two pages of one device are two Chromium contexts: no shared globals, no
134
+ * shared memory, no events between them. They talk through Max, and this is the
135
+ * message that does not persist - the receiving page gets `<selector> <value>` on
136
+ * the inlet it bound with `bindInlet`, and nothing is written to the Live set.
137
+ *
138
+ * Use a STATE SLOT instead for anything that must survive a save (the pattern,
139
+ * a preset). Use this for what is happening NOW: a transport change, a knob
140
+ * position, a nudge to re-read something.
141
+ */
142
+ export function sendToWindow(windowId: string, selector: string, value: unknown): void {
143
+ outlet("window_send", windowId, selector, value);
144
+ }
145
+
130
146
  /* ------------------------------------------------------------------ *
131
147
  * The chain contract
132
148
  *
@@ -218,14 +234,69 @@ export const STATE_OUT = {
218
234
  export const PARAM_OUT = {
219
235
  /** UI -> wrapper: `get_param_id <id>` - what is this parameter's LOM id? Reply on `param_id`. */
220
236
  get_param_id: "get_param_id",
237
+ /** UI -> wrapper: `param_label <id> <name>` - rename the dial on the device panel. */
238
+ param_label: "param_label",
239
+ /** UI -> wrapper: `param_unit <id> <unit>` - how Live PRINTS the value. */
240
+ param_unit: "param_unit",
241
+ /** UI -> wrapper: `param_range <id> <lo> <hi>` - the dial's travel. Answered. */
242
+ param_range: "param_range",
221
243
  } as const;
222
244
 
223
245
  /** ...and the reply. */
224
246
  export const PARAM_IN = {
225
247
  /** wrapper -> UI: `param_id <id> <lomId>` - 0 means no parameter of that name resolved. */
226
248
  param_id: "param_id",
249
+ /** wrapper -> UI: `param_range_ok <id>` - Live took the range; the parameter now carries it. */
250
+ param_range_ok: "param_range_ok",
251
+ /** wrapper -> UI: `param_range_failed <id>` - it did not; the parameter is still as declared. */
252
+ param_range_failed: "param_range_failed",
253
+ /** wrapper -> device view: `param_desc <id> <name>` - what some page called this parameter. */
254
+ param_desc: "param_desc",
255
+ /** wrapper -> device view: `param_desc_range <id> <lo> <hi> <took>` - and its travel. */
256
+ param_desc_range: "param_desc_range",
227
257
  } as const;
228
258
 
259
+ /**
260
+ * Say what a parameter IS, at runtime: its name, how Live should print it, and the
261
+ * travel it should have.
262
+ *
263
+ * A surface declares all three at BUILD time, which is right when the device knows
264
+ * what its controls do. It does not when the control is whatever the user's code
265
+ * just asked for - a pattern's `slider()`, a modulation slot pointed at something
266
+ * new - and then the dial reads `S1`, travels 0..1 and says nothing about the sound
267
+ * it is moving.
268
+ *
269
+ * ------------------------------------------------------------------------------
270
+ * WHAT TAKES, AND WHAT DOES NOT - measured in Live, not assumed.
271
+ *
272
+ * `name` reaches the DEVICE PANEL. It does NOT reach Live's parameter registry or
273
+ * the Rack macro picker, which keep the declared short name: a frozen
274
+ * device cannot rename a parameter there.
275
+ * `unit` takes. It is the unit STYLE, so the readout says "600 Hz" and not "600";
276
+ * anything outside Max's list becomes a custom unit and still prints.
277
+ * `range` takes, AND CHANGES THE DOMAIN THE PARAMETER REPORTS. That is the trap:
278
+ * a page that goes on normalizing 0..1 will scale a value that is already
279
+ * scaled, and the control sticks at its minimum. So the wrapper ANSWERS -
280
+ * `param_range_ok` or `param_range_failed` - and the caller must stop
281
+ * normalizing when it took. `onParamRange()` below is that answer.
282
+ */
283
+ export function describeParam(id: string, desc: { name?: string; unit?: string; range?: [number, number] }): void {
284
+ if (desc.name) outlet(PARAM_OUT.param_label, id, desc.name);
285
+ if (desc.unit) outlet(PARAM_OUT.param_unit, id, desc.unit);
286
+ if (desc.range && desc.range[1] > desc.range[0]) outlet(PARAM_OUT.param_range, id, desc.range[0], desc.range[1]);
287
+ }
288
+
289
+ /**
290
+ * Hear whether a `describeParam` range took, per parameter.
291
+ *
292
+ * Until this says yes, the parameter is still in its declared domain and the page
293
+ * owns the scaling. Exactly one scaling, wherever it ends up living.
294
+ */
295
+ export function onParamRange(fn: (id: string, took: boolean) => void): void {
296
+ bindInlet(PARAM_IN.param_range_ok, (id) => fn(String(id), true));
297
+ bindInlet(PARAM_IN.param_range_failed, (id) => fn(String(id), false));
298
+ }
299
+
229
300
  /**
230
301
  * Selectors the WRAPPER handles for reading and writing the CLIP on this device's
231
302
  * track (`read_notes`/`write_clip` in liveapi.ts). Wrapper-owned, like DEVICE_IN -
@@ -470,7 +541,8 @@ export function saveToFile(destPath: string, bytes: ArrayBuffer): Promise<{ byte
470
541
  * running set, not names: they are not stable across a set reload, so an id saved
471
542
  * yesterday points at whatever occupies that slot today - or at nothing. Persist how
472
543
  * you FOUND the parameter (the device's position, the parameter's name) and resolve it
473
- * again; that is the same rule Translate mode's reconciler follows (doc/TODO.md item 1).
544
+ * again. (Translate mode's reconciler followed the same rule; that mode was settled as
545
+ * adopt-only, permanently - Live's Browser is unreachable from [js].)
474
546
  */
475
547
  export function bindRemote(slot: number, lomId: number | string): void {
476
548
  outlet(CHAIN_OUT.remote_bind, slot, lomId);