@m4l-jweb/wrapper 0.1.0 → 0.2.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 Jaime Lopez (alienmind)
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m4l-jweb/wrapper",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "m4l-jweb: the Max for Live glue layer connecting a device to LiveAPI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,6 +18,7 @@
18
18
  },
19
19
  "files": [
20
20
  "src",
21
- "sources.mjs"
21
+ "sources.mjs",
22
+ "LICENSE"
22
23
  ]
23
24
  }
package/src/core.ts CHANGED
@@ -28,7 +28,7 @@ post("m4l-jweb: wrapper loaded (build " + buildStamp() + ", mode " + MODE + ")\n
28
28
 
29
29
  /** The build this device instance actually is. Injected by the build. */
30
30
  function buildStamp(): string {
31
- return typeof BUILD_STAMP !== "undefined" ? BUILD_STAMP : "dev";
31
+ return typeof BUILD_STAMP !== "undefined" ? BUILD_STAMP : "dev";
32
32
  }
33
33
 
34
34
  /* ------------------------------------------------------------------ *
@@ -45,28 +45,28 @@ function buildStamp(): string {
45
45
 
46
46
  /** live.thisdevice -> the device is fully loaded. Everything LiveAPI starts here. */
47
47
  function bang(): void {
48
- post("m4l-jweb: bang (device ready)\n");
49
- extractExtraPayloads();
50
- loadWebview();
51
- setupTempoObserver(); // liveapi.ts
52
- startTickPoll(); // liveapi.ts
53
- // A device's own wrapper/device.ts hooks in here: this is the ONLY safe place
54
- // to create LiveAPI objects (see the loadbang trap above).
55
- if (typeof onDeviceReady === "function") onDeviceReady();
48
+ post("m4l-jweb: bang (device ready)\n");
49
+ extractExtraPayloads();
50
+ loadWebview();
51
+ setupTempoObserver(); // liveapi.ts
52
+ startTickPoll(); // liveapi.ts
53
+ // A device's own wrapper/device.ts hooks in here: this is the ONLY safe place
54
+ // to create LiveAPI objects (see the loadbang trap above).
55
+ if (typeof onDeviceReady === "function") onDeviceReady();
56
56
  }
57
57
 
58
58
  /** Patcher loaded. File work is safe here; LiveAPI is NOT. */
59
59
  function loadbang(): void {
60
- post("m4l-jweb: loadbang\n");
61
- extractExtraPayloads();
62
- loadWebview();
60
+ post("m4l-jweb: loadbang\n");
61
+ extractExtraPayloads();
62
+ loadWebview();
63
63
  }
64
64
 
65
65
  /** Manual re-init, handy while developing. */
66
66
  function reload(): void {
67
- loadWebview();
68
- setupTempoObserver();
69
- startTickPoll();
67
+ loadWebview();
68
+ setupTempoObserver();
69
+ startTickPoll();
70
70
  }
71
71
 
72
72
  /**
@@ -81,14 +81,14 @@ function anything(): void {}
81
81
  * assume it was listening when state last changed - resend all of it.
82
82
  */
83
83
  function ui_ready(): void {
84
- outlet(0, "mode", MODE);
85
- // The UI shows this next to its own baked-in version: a mismatch means a
86
- // mixed install (stale .amxd instance vs newer extracted UI, or vice versa).
87
- outlet(0, "build", buildStamp());
88
- sendCurrentTempo(); // liveapi.ts
89
- // The device resends its own state here. The page loads asynchronously, so
90
- // anything sent before it was listening is simply gone.
91
- if (typeof onUiReady === "function") onUiReady();
84
+ outlet(0, "mode", MODE);
85
+ // The UI shows this next to its own baked-in version: a mismatch means a
86
+ // mixed install (stale .amxd instance vs newer extracted UI, or vice versa).
87
+ outlet(0, "build", buildStamp());
88
+ sendCurrentTempo(); // liveapi.ts
89
+ // The device resends its own state here. The page loads asynchronously, so
90
+ // anything sent before it was listening is simply gone.
91
+ if (typeof onUiReady === "function") onUiReady();
92
92
  }
93
93
 
94
94
  /* ------------------------------------------------------------------ *
@@ -102,39 +102,39 @@ function ui_ready(): void {
102
102
  * ------------------------------------------------------------------ */
103
103
 
104
104
  function loadWebview(): void {
105
- try {
106
- var url = resolveUiUrl();
107
- if (!url) return;
108
- outlet(0, "url", url);
109
- post("m4l-jweb: sent url " + url + "\n");
110
- } catch (e) {
111
- post("m4l-jweb: loadWebview error " + (e as Error).message + "\n");
112
- }
105
+ try {
106
+ var url = resolveUiUrl();
107
+ if (!url) return;
108
+ outlet(0, "url", url);
109
+ post("m4l-jweb: sent url " + url + "\n");
110
+ } catch (e) {
111
+ post("m4l-jweb: loadWebview error " + (e as Error).message + "\n");
112
+ }
113
113
  }
114
114
 
115
115
  function resolveUiUrl(): string | null {
116
- var folder = deviceFolder();
117
- if (!folder) {
118
- post("m4l-jweb: patcher not saved yet - UI path unknown\n");
119
- return null;
120
- }
121
- var name = typeof UI_PAYLOAD_NAME !== "undefined" ? UI_PAYLOAD_NAME : "ui.html";
122
- var target = folder + "/" + name;
116
+ var folder = deviceFolder();
117
+ if (!folder) {
118
+ post("m4l-jweb: patcher not saved yet - UI path unknown\n");
119
+ return null;
120
+ }
121
+ var name = typeof UI_PAYLOAD_NAME !== "undefined" ? UI_PAYLOAD_NAME : "ui.html";
122
+ var target = folder + "/" + name;
123
123
 
124
- if (typeof UI_PAYLOAD_B64 !== "undefined" && typeof UI_PAYLOAD_BYTES !== "undefined") {
125
- extractPayload(target, UI_PAYLOAD_B64, UI_PAYLOAD_BYTES);
126
- } else {
127
- post("m4l-jweb: no embedded payload (dev build) - using " + target + "\n");
128
- }
129
- // Cache-buster: the URL changes per build, so Chromium can never serve a page
130
- // it cached from a previous build of the same file path.
131
- return encodeURI("file:///" + target) + "?v=" + encodeURIComponent(buildStamp());
124
+ if (typeof UI_PAYLOAD_B64 !== "undefined" && typeof UI_PAYLOAD_BYTES !== "undefined") {
125
+ extractPayload(target, UI_PAYLOAD_B64, UI_PAYLOAD_BYTES);
126
+ } else {
127
+ post("m4l-jweb: no embedded payload (dev build) - using " + target + "\n");
128
+ }
129
+ // Cache-buster: the URL changes per build, so Chromium can never serve a page
130
+ // it cached from a previous build of the same file path.
131
+ return encodeURI("file:///" + target) + "?v=" + encodeURIComponent(buildStamp());
132
132
  }
133
133
 
134
134
  /** The folder the .amxd lives in, derived from the patcher's own path. */
135
135
  function deviceFolder(): string | null {
136
- var fp: string = this.patcher.filepath;
137
- return fp && fp.length ? fp.replace(/\/[^\/]*$/, "") : null;
136
+ var fp: string = this.patcher.filepath;
137
+ return fp && fp.length ? fp.replace(/\/[^\/]*$/, "") : null;
138
138
  }
139
139
 
140
140
  /**
@@ -146,23 +146,23 @@ function deviceFolder(): string | null {
146
146
  * size and build stamp already match.
147
147
  */
148
148
  function extractExtraPayloads(): void {
149
- // The build emits all three together or none at all; bind them locally so the
150
- // compiler can see that too.
151
- if (typeof EXTRA_PAYLOAD_NAMES === "undefined" || typeof EXTRA_PAYLOAD_B64 === "undefined" || typeof EXTRA_PAYLOAD_BYTES === "undefined") {
152
- return;
153
- }
154
- var names = EXTRA_PAYLOAD_NAMES;
155
- var blobs = EXTRA_PAYLOAD_B64;
156
- var sizes = EXTRA_PAYLOAD_BYTES;
149
+ // The build emits all three together or none at all; bind them locally so the
150
+ // compiler can see that too.
151
+ if (typeof EXTRA_PAYLOAD_NAMES === "undefined" || typeof EXTRA_PAYLOAD_B64 === "undefined" || typeof EXTRA_PAYLOAD_BYTES === "undefined") {
152
+ return;
153
+ }
154
+ var names = EXTRA_PAYLOAD_NAMES;
155
+ var blobs = EXTRA_PAYLOAD_B64;
156
+ var sizes = EXTRA_PAYLOAD_BYTES;
157
157
 
158
- var folder = deviceFolder();
159
- if (!folder) {
160
- post("m4l-jweb: patcher path unknown - cannot extract payloads\n");
161
- return;
162
- }
163
- for (var i = 0; i < names.length; i++) {
164
- extractPayload(folder + "/" + names[i], blobs[i], sizes[i]);
165
- }
158
+ var folder = deviceFolder();
159
+ if (!folder) {
160
+ post("m4l-jweb: patcher path unknown - cannot extract payloads\n");
161
+ return;
162
+ }
163
+ for (var i = 0; i < names.length; i++) {
164
+ extractPayload(folder + "/" + names[i], blobs[i], sizes[i]);
165
+ }
166
166
  }
167
167
 
168
168
  /**
@@ -173,72 +173,72 @@ function extractExtraPayloads(): void {
173
173
  * and leave a stale file that no longer matches the wrapper driving it.
174
174
  */
175
175
  function extractPayload(targetPath: string, b64chunks: string[], byteCount: number): void {
176
- try {
177
- var existing = new File(targetPath);
178
- if (existing.isopen) {
179
- var sameSize = existing.eof === byteCount;
180
- existing.close();
181
- if (sameSize && readTextFile(targetPath + ".stamp") === buildStamp()) return;
182
- }
183
- } catch (e) {
184
- /* fall through and (re)write */
185
- }
186
- try {
187
- var out = new File(targetPath, "write");
188
- if (!out.isopen) out.open();
189
- if (!out.isopen) {
190
- post("m4l-jweb: cannot write " + targetPath + "\n");
191
- return;
192
- }
193
- out.eof = 0;
194
- // File.writebytes silently truncates large calls (observed ~16 KB cap), so
195
- // write in small slices and verify the byte count afterwards.
196
- var SLICE = 4096;
197
- for (var i = 0; i < b64chunks.length; i++) {
198
- var bytes = b64decode(b64chunks[i]);
199
- for (var off = 0; off < bytes.length; off += SLICE) {
200
- out.writebytes(bytes.slice(off, off + SLICE));
201
- }
202
- }
203
- out.close();
176
+ try {
177
+ var existing = new File(targetPath);
178
+ if (existing.isopen) {
179
+ var sameSize = existing.eof === byteCount;
180
+ existing.close();
181
+ if (sameSize && readTextFile(targetPath + ".stamp") === buildStamp()) return;
182
+ }
183
+ } catch (e) {
184
+ /* fall through and (re)write */
185
+ }
186
+ try {
187
+ var out = new File(targetPath, "write");
188
+ if (!out.isopen) out.open();
189
+ if (!out.isopen) {
190
+ post("m4l-jweb: cannot write " + targetPath + "\n");
191
+ return;
192
+ }
193
+ out.eof = 0;
194
+ // File.writebytes silently truncates large calls (observed ~16 KB cap), so
195
+ // write in small slices and verify the byte count afterwards.
196
+ var SLICE = 4096;
197
+ for (var i = 0; i < b64chunks.length; i++) {
198
+ var bytes = b64decode(b64chunks[i]);
199
+ for (var off = 0; off < bytes.length; off += SLICE) {
200
+ out.writebytes(bytes.slice(off, off + SLICE));
201
+ }
202
+ }
203
+ out.close();
204
204
 
205
- var check = new File(targetPath);
206
- var written = check.isopen ? check.eof : -1;
207
- if (check.isopen) check.close();
208
- if (written === byteCount) {
209
- post("m4l-jweb: extracted " + written + " bytes to " + targetPath + "\n");
210
- writeTextFile(targetPath + ".stamp", buildStamp());
211
- } else {
212
- post("m4l-jweb: extract SIZE MISMATCH - wrote " + written + ", expected " + byteCount + "\n");
213
- }
214
- } catch (e2) {
215
- post("m4l-jweb: extract failed - " + (e2 as Error).message + "\n");
216
- }
205
+ var check = new File(targetPath);
206
+ var written = check.isopen ? check.eof : -1;
207
+ if (check.isopen) check.close();
208
+ if (written === byteCount) {
209
+ post("m4l-jweb: extracted " + written + " bytes to " + targetPath + "\n");
210
+ writeTextFile(targetPath + ".stamp", buildStamp());
211
+ } else {
212
+ post("m4l-jweb: extract SIZE MISMATCH - wrote " + written + ", expected " + byteCount + "\n");
213
+ }
214
+ } catch (e2) {
215
+ post("m4l-jweb: extract failed - " + (e2 as Error).message + "\n");
216
+ }
217
217
  }
218
218
 
219
219
  function readTextFile(p: string): string | null {
220
- try {
221
- var f = new File(p);
222
- if (!f.isopen) return null;
223
- var s = f.readstring(Math.min(f.eof, 256));
224
- f.close();
225
- return s;
226
- } catch (e) {
227
- return null;
228
- }
220
+ try {
221
+ var f = new File(p);
222
+ if (!f.isopen) return null;
223
+ var s = f.readstring(Math.min(f.eof, 256));
224
+ f.close();
225
+ return s;
226
+ } catch (e) {
227
+ return null;
228
+ }
229
229
  }
230
230
 
231
231
  function writeTextFile(p: string, s: string): void {
232
- try {
233
- var f = new File(p, "write");
234
- if (!f.isopen) f.open();
235
- if (!f.isopen) return;
236
- f.eof = 0;
237
- f.writestring(s);
238
- f.close();
239
- } catch (e) {
240
- /* non-fatal */
241
- }
232
+ try {
233
+ var f = new File(p, "write");
234
+ if (!f.isopen) f.open();
235
+ if (!f.isopen) return;
236
+ f.eof = 0;
237
+ f.writestring(s);
238
+ f.close();
239
+ } catch (e) {
240
+ /* non-fatal */
241
+ }
242
242
  }
243
243
 
244
244
  var B64CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -246,22 +246,22 @@ var b64lookup: { [c: string]: number } | null = null;
246
246
 
247
247
  /** Base64 -> array of byte values. Max's [js] has no atob. */
248
248
  function b64decode(s: string): number[] {
249
- if (!b64lookup) {
250
- b64lookup = {};
251
- for (var i = 0; i < B64CHARS.length; i++) b64lookup[B64CHARS.charAt(i)] = i;
252
- }
253
- var out: number[] = [];
254
- var buffer = 0;
255
- var bits = 0;
256
- for (var j = 0; j < s.length; j++) {
257
- var c = s.charAt(j);
258
- if (c === "=") break;
259
- buffer = (buffer << 6) | b64lookup[c];
260
- bits += 6;
261
- if (bits >= 8) {
262
- bits -= 8;
263
- out.push((buffer >> bits) & 0xff);
264
- }
265
- }
266
- return out;
249
+ if (!b64lookup) {
250
+ b64lookup = {};
251
+ for (var i = 0; i < B64CHARS.length; i++) b64lookup[B64CHARS.charAt(i)] = i;
252
+ }
253
+ var out: number[] = [];
254
+ var buffer = 0;
255
+ var bits = 0;
256
+ for (var j = 0; j < s.length; j++) {
257
+ var c = s.charAt(j);
258
+ if (c === "=") break;
259
+ buffer = (buffer << 6) | b64lookup[c];
260
+ bits += 6;
261
+ if (bits >= 8) {
262
+ bits -= 8;
263
+ out.push((buffer >> bits) & 0xff);
264
+ }
265
+ }
266
+ return out;
267
267
  }
package/src/liveapi.ts CHANGED
@@ -23,30 +23,30 @@ var tickPoll = new Task(pollTransport, this);
23
23
  var liveSetApi: LiveAPI | null = null;
24
24
 
25
25
  function startTickPoll(): void {
26
- try {
27
- liveSetApi = new LiveAPI("live_set");
28
- } catch (e) {
29
- post("m4l-jweb: tick poll unavailable - " + (e as Error).message + "\n");
30
- return;
31
- }
32
- tickPoll.cancel();
33
- tickPoll.interval = 50;
34
- tickPoll.repeat();
35
- post("m4l-jweb: transport poll on\n");
26
+ try {
27
+ liveSetApi = new LiveAPI("live_set");
28
+ } catch (e) {
29
+ post("m4l-jweb: tick poll unavailable - " + (e as Error).message + "\n");
30
+ return;
31
+ }
32
+ tickPoll.cancel();
33
+ tickPoll.interval = 50;
34
+ tickPoll.repeat();
35
+ post("m4l-jweb: transport poll on\n");
36
36
  }
37
37
 
38
38
  function pollTransport(): void {
39
- if (!liveSetApi) return;
40
- try {
41
- var playing = parseInt(String(liveSetApi.get("is_playing")), 10);
42
- var beats = parseFloat(String(liveSetApi.get("current_song_time")));
43
- outlet(0, "tick", playing, beats);
44
- // A device with a second consumer (another engine on outlet 1) mirrors the
45
- // clock here rather than polling Live twice.
46
- if (typeof onTick === "function") onTick(playing, beats);
47
- } catch (e) {
48
- /* transient - the next poll retries */
49
- }
39
+ if (!liveSetApi) return;
40
+ try {
41
+ var playing = parseInt(String(liveSetApi.get("is_playing")), 10);
42
+ var beats = parseFloat(String(liveSetApi.get("current_song_time")));
43
+ outlet(0, "tick", playing, beats);
44
+ // A device with a second consumer (another engine on outlet 1) mirrors the
45
+ // clock here rather than polling Live twice.
46
+ if (typeof onTick === "function") onTick(playing, beats);
47
+ } catch (e) {
48
+ /* transient - the next poll retries */
49
+ }
50
50
  }
51
51
 
52
52
  /* ------------------------------------------------------------------ *
@@ -59,36 +59,36 @@ function pollTransport(): void {
59
59
  var tempoObs: LiveAPI | null = null;
60
60
 
61
61
  function setupTempoObserver(): void {
62
- // Recreate unconditionally: an object left over from a loading context is
63
- // dead and must not block the real one.
64
- try {
65
- tempoObs = new LiveAPI(onTempo, "live_set");
66
- tempoObs.property = "tempo";
67
- post("m4l-jweb: tempo observer on (current " + tempoObs.get("tempo") + ")\n");
68
- } catch (e) {
69
- post("m4l-jweb: tempo observer unavailable - " + (e as Error).message + "\n");
70
- }
62
+ // Recreate unconditionally: an object left over from a loading context is
63
+ // dead and must not block the real one.
64
+ try {
65
+ tempoObs = new LiveAPI(onTempo, "live_set");
66
+ tempoObs.property = "tempo";
67
+ post("m4l-jweb: tempo observer on (current " + tempoObs.get("tempo") + ")\n");
68
+ } catch (e) {
69
+ post("m4l-jweb: tempo observer unavailable - " + (e as Error).message + "\n");
70
+ }
71
71
  }
72
72
 
73
73
  function onTempo(a: unknown[]): void {
74
- if (a && a[0] == "tempo") {
75
- outlet(0, "tempo", a[1]);
76
- if (typeof onTempoChange === "function") onTempoChange(Number(a[1]));
77
- }
74
+ if (a && a[0] == "tempo") {
75
+ outlet(0, "tempo", a[1]);
76
+ if (typeof onTempoChange === "function") onTempoChange(Number(a[1]));
77
+ }
78
78
  }
79
79
 
80
80
  /** The observer's first callback can beat the page's binding - ui_ready re-reads. */
81
81
  function sendCurrentTempo(): void {
82
- try {
83
- var api = new LiveAPI("live_set");
84
- var t = parseFloat(String(api.get("tempo")));
85
- if (t > 0) {
86
- outlet(0, "tempo", t);
87
- if (typeof onTempoChange === "function") onTempoChange(t);
88
- }
89
- } catch (e) {
90
- post("m4l-jweb: tempo read failed - " + (e as Error).message + "\n");
91
- }
82
+ try {
83
+ var api = new LiveAPI("live_set");
84
+ var t = parseFloat(String(api.get("tempo")));
85
+ if (t > 0) {
86
+ outlet(0, "tempo", t);
87
+ if (typeof onTempoChange === "function") onTempoChange(t);
88
+ }
89
+ } catch (e) {
90
+ post("m4l-jweb: tempo read failed - " + (e as Error).message + "\n");
91
+ }
92
92
  }
93
93
 
94
94
  /* ------------------------------------------------------------------ *
@@ -105,20 +105,20 @@ function sendCurrentTempo(): void {
105
105
  * alive; drop it and the observer dies with it.
106
106
  */
107
107
  function observeProperty(objectPath: string, property: string, selector: string): LiveAPI | null {
108
- try {
109
- var api = new LiveAPI(function (a: unknown[]) {
110
- if (a && a[0] == property) {
111
- var args: unknown[] = [0, selector];
112
- for (var i = 1; i < a.length; i++) args.push(a[i]);
113
- (outlet as Function).apply(this, args);
114
- }
115
- }, objectPath);
116
- api.property = property;
117
- return api;
118
- } catch (e) {
119
- post("m4l-jweb: cannot observe " + objectPath + " " + property + " - " + (e as Error).message + "\n");
120
- return null;
121
- }
108
+ try {
109
+ var api = new LiveAPI(function (a: unknown[]) {
110
+ if (a && a[0] == property) {
111
+ var args: unknown[] = [0, selector];
112
+ for (var i = 1; i < a.length; i++) args.push(a[i]);
113
+ (outlet as Function).apply(this, args);
114
+ }
115
+ }, objectPath);
116
+ api.property = property;
117
+ return api;
118
+ } catch (e) {
119
+ post("m4l-jweb: cannot observe " + objectPath + " " + property + " - " + (e as Error).message + "\n");
120
+ return null;
121
+ }
122
122
  }
123
123
 
124
124
  /* ------------------------------------------------------------------ *
@@ -126,16 +126,16 @@ function observeProperty(objectPath: string, property: string, selector: string)
126
126
  * ------------------------------------------------------------------ */
127
127
 
128
128
  interface LiveNote {
129
- pitch: number;
130
- start_time: number;
131
- duration: number;
132
- velocity: number;
133
- mute?: number;
129
+ pitch: number;
130
+ start_time: number;
131
+ duration: number;
132
+ velocity: number;
133
+ mute?: number;
134
134
  }
135
135
 
136
136
  /** The LiveAPI for the track this device sits on. */
137
137
  function ownTrack(): LiveAPI {
138
- return new LiveAPI("this_device canonical_parent");
138
+ return new LiveAPI("this_device canonical_parent");
139
139
  }
140
140
 
141
141
  /**
@@ -143,52 +143,52 @@ function ownTrack(): LiveAPI {
143
143
  * Creates a clip in the first empty slot on this device's track and fills it.
144
144
  */
145
145
  function write_clip(): void {
146
- var a = arrayfromargs(arguments);
147
- if (a.length < 2) return;
148
- var lengthBeats = a[0];
149
- var n = Number(a[1]);
150
-
151
- var slot = firstEmptySlot();
152
- if (!slot) {
153
- post("m4l-jweb: no empty clip slot on this track\n");
154
- return;
155
- }
156
- slot.call("create_clip", lengthBeats);
157
- var clip = new LiveAPI(slot.unquotedpath + " clip");
158
-
159
- var notes: LiveNote[] = [];
160
- for (var k = 0; k < n; k++) {
161
- var o = 2 + k * 4;
162
- notes.push({
163
- pitch: Number(a[o]),
164
- start_time: Number(a[o + 1]),
165
- duration: Number(a[o + 2]),
166
- velocity: Number(a[o + 3]),
167
- mute: 0,
168
- });
169
- }
170
- try {
171
- clip.call("add_new_notes", { notes: notes });
172
- } catch (e) {
173
- post("m4l-jweb: add_new_notes failed - " + (e as Error).message + "\n");
174
- return;
175
- }
176
- post("m4l-jweb: wrote " + n + " notes over " + lengthBeats + " beats\n");
146
+ var a = arrayfromargs(arguments);
147
+ if (a.length < 2) return;
148
+ var lengthBeats = a[0];
149
+ var n = Number(a[1]);
150
+
151
+ var slot = firstEmptySlot();
152
+ if (!slot) {
153
+ post("m4l-jweb: no empty clip slot on this track\n");
154
+ return;
155
+ }
156
+ slot.call("create_clip", lengthBeats);
157
+ var clip = new LiveAPI(slot.unquotedpath + " clip");
158
+
159
+ var notes: LiveNote[] = [];
160
+ for (var k = 0; k < n; k++) {
161
+ var o = 2 + k * 4;
162
+ notes.push({
163
+ pitch: Number(a[o]),
164
+ start_time: Number(a[o + 1]),
165
+ duration: Number(a[o + 2]),
166
+ velocity: Number(a[o + 3]),
167
+ mute: 0,
168
+ });
169
+ }
170
+ try {
171
+ clip.call("add_new_notes", { notes: notes });
172
+ } catch (e) {
173
+ post("m4l-jweb: add_new_notes failed - " + (e as Error).message + "\n");
174
+ return;
175
+ }
176
+ post("m4l-jweb: wrote " + n + " notes over " + lengthBeats + " beats\n");
177
177
  }
178
178
 
179
179
  function firstEmptySlot(): LiveAPI | null {
180
- try {
181
- var track = ownTrack();
182
- var count = parseInt(String(track.getcount("clip_slots")), 10);
183
- for (var i = 0; i < count; i++) {
184
- var s = new LiveAPI(track.unquotedpath + " clip_slots " + i);
185
- if (parseInt(String(s.get("has_clip")), 10) === 0) return s;
186
- }
187
- return null;
188
- } catch (e) {
189
- post("m4l-jweb: firstEmptySlot error " + (e as Error).message + "\n");
190
- return null;
191
- }
180
+ try {
181
+ var track = ownTrack();
182
+ var count = parseInt(String(track.getcount("clip_slots")), 10);
183
+ for (var i = 0; i < count; i++) {
184
+ var s = new LiveAPI(track.unquotedpath + " clip_slots " + i);
185
+ if (parseInt(String(s.get("has_clip")), 10) === 0) return s;
186
+ }
187
+ return null;
188
+ } catch (e) {
189
+ post("m4l-jweb: firstEmptySlot error " + (e as Error).message + "\n");
190
+ return null;
191
+ }
192
192
  }
193
193
 
194
194
  /**
@@ -197,54 +197,54 @@ function firstEmptySlot(): LiveAPI | null {
197
197
  * "notes <loopEnd> <n> <pitch start duration> ...".
198
198
  */
199
199
  function read_notes(): void {
200
- var clip = pickClip();
201
- if (!clip) {
202
- post("m4l-jweb: no clip found on this track\n");
203
- outlet(0, "read_error", "no_clip");
204
- return;
205
- }
206
- var loopEnd = parseFloat(String(clip.get("loop_end")));
207
- var notes = getNotes(clip, loopEnd);
208
- if (!notes) return;
209
-
210
- var out: unknown[] = ["notes", loopEnd, notes.length];
211
- for (var i = 0; i < notes.length; i++) {
212
- out.push(notes[i].pitch, notes[i].start_time, notes[i].duration);
213
- }
214
- // A note list is variadic, so the message has to be spread with apply().
215
- // outlet()'s typed signature cannot express that; go through Function.
216
- (outlet as Function).apply(this, ([0] as unknown[]).concat(out));
217
- post("m4l-jweb: read " + notes.length + " notes (loop_end " + loopEnd + ")\n");
200
+ var clip = pickClip();
201
+ if (!clip) {
202
+ post("m4l-jweb: no clip found on this track\n");
203
+ outlet(0, "read_error", "no_clip");
204
+ return;
205
+ }
206
+ var loopEnd = parseFloat(String(clip.get("loop_end")));
207
+ var notes = getNotes(clip, loopEnd);
208
+ if (!notes) return;
209
+
210
+ var out: unknown[] = ["notes", loopEnd, notes.length];
211
+ for (var i = 0; i < notes.length; i++) {
212
+ out.push(notes[i].pitch, notes[i].start_time, notes[i].duration);
213
+ }
214
+ // A note list is variadic, so the message has to be spread with apply().
215
+ // outlet()'s typed signature cannot express that; go through Function.
216
+ (outlet as Function).apply(this, ([0] as unknown[]).concat(out));
217
+ post("m4l-jweb: read " + notes.length + " notes (loop_end " + loopEnd + ")\n");
218
218
  }
219
219
 
220
220
  function pickClip(): LiveAPI | null {
221
- try {
222
- var track = ownTrack();
223
- var count = parseInt(String(track.getcount("clip_slots")), 10);
224
- var firstWithClip: LiveAPI | null = null;
225
- for (var i = 0; i < count; i++) {
226
- var s = new LiveAPI(track.unquotedpath + " clip_slots " + i);
227
- if (parseInt(String(s.get("has_clip")), 10) === 1) {
228
- var c = new LiveAPI(s.unquotedpath + " clip");
229
- if (parseInt(String(c.get("is_playing")), 10) === 1) return c;
230
- if (!firstWithClip) firstWithClip = c;
231
- }
232
- }
233
- return firstWithClip;
234
- } catch (e) {
235
- post("m4l-jweb: pickClip error " + (e as Error).message + "\n");
236
- return null;
237
- }
221
+ try {
222
+ var track = ownTrack();
223
+ var count = parseInt(String(track.getcount("clip_slots")), 10);
224
+ var firstWithClip: LiveAPI | null = null;
225
+ for (var i = 0; i < count; i++) {
226
+ var s = new LiveAPI(track.unquotedpath + " clip_slots " + i);
227
+ if (parseInt(String(s.get("has_clip")), 10) === 1) {
228
+ var c = new LiveAPI(s.unquotedpath + " clip");
229
+ if (parseInt(String(c.get("is_playing")), 10) === 1) return c;
230
+ if (!firstWithClip) firstWithClip = c;
231
+ }
232
+ }
233
+ return firstWithClip;
234
+ } catch (e) {
235
+ post("m4l-jweb: pickClip error " + (e as Error).message + "\n");
236
+ return null;
237
+ }
238
238
  }
239
239
 
240
240
  function getNotes(clip: LiveAPI, loopEnd: number): LiveNote[] | null {
241
- // Live 11+: get_notes_extended returns a JSON string.
242
- try {
243
- var d = clip.call("get_notes_extended", 0, 128, 0, loopEnd);
244
- var obj = typeof d === "string" ? JSON.parse(d) : d;
245
- if (obj && obj.notes) return obj.notes;
246
- } catch (e) {
247
- post("m4l-jweb: get_notes_extended failed - " + (e as Error).message + "\n");
248
- }
249
- return null;
241
+ // Live 11+: get_notes_extended returns a JSON string.
242
+ try {
243
+ var d = clip.call("get_notes_extended", 0, 128, 0, loopEnd);
244
+ var obj = typeof d === "string" ? JSON.parse(d) : d;
245
+ if (obj && obj.notes) return obj.notes;
246
+ } catch (e) {
247
+ post("m4l-jweb: get_notes_extended failed - " + (e as Error).message + "\n");
248
+ }
249
+ return null;
250
250
  }
package/src/max.d.ts CHANGED
@@ -32,34 +32,60 @@ declare let outlets: number;
32
32
 
33
33
  /** Max's scheduler. There is no `setTimeout` in [js]. */
34
34
  declare class Task {
35
- constructor(fn: () => void, ctx: unknown);
36
- interval: number;
37
- repeat(count?: number): void;
38
- schedule(delayMs?: number): void;
39
- cancel(): void;
35
+ constructor(fn: () => void, ctx: unknown);
36
+ interval: number;
37
+ repeat(count?: number): void;
38
+ schedule(delayMs?: number): void;
39
+ cancel(): void;
40
40
  }
41
41
 
42
42
  /** Max's file object. Note: `writebytes` truncates silently past ~16 KB. */
43
43
  declare class File {
44
- constructor(path: string, mode?: "read" | "write" | "readwrite");
45
- isopen: boolean;
46
- eof: number;
47
- open(): void;
48
- close(): void;
49
- readstring(count: number): string;
50
- writestring(s: string): void;
51
- writebytes(bytes: number[]): void;
44
+ constructor(path: string, mode?: "read" | "write" | "readwrite");
45
+ isopen: boolean;
46
+ eof: number;
47
+ open(): void;
48
+ close(): void;
49
+ readstring(count: number): string;
50
+ writestring(s: string): void;
51
+ writebytes(bytes: number[]): void;
52
+ }
53
+
54
+ /**
55
+ * A named [buffer~] in the patcher, addressed from [js].
56
+ *
57
+ * This is the seam that makes "disk is the audio transport" work: [js] tells a
58
+ * buffer~ to `replace` a file on disk and MSP plays it from there, so audio
59
+ * never crosses the Max message bridge (a text-parsed protocol) as data.
60
+ *
61
+ * The buffer~ must ALREADY EXIST in the patcher under this name - `new Buffer`
62
+ * binds to one, it does not create one. And `send("replace", path)` is
63
+ * ASYNCHRONOUS: framecount() right after it still reads the old size. Come back
64
+ * on a Task.
65
+ *
66
+ * UNVERIFIED. This declaration is what doc/SPIKES.md spike 1.2 exists to
67
+ * confirm. Do not build on it until that spike is recorded as PASS.
68
+ */
69
+ declare class Buffer {
70
+ constructor(name: string);
71
+ /** Send a message to the buffer~ itself, e.g. `send("replace", "/path/x.wav")`. */
72
+ send(message: string, ...args: unknown[]): void;
73
+ framecount(): number;
74
+ channelcount(): number;
75
+ /** peek(channel, frameIndex, count) - channels and frames are 1-indexed. */
76
+ peek(channel: number, index: number, count?: number): number;
77
+ poke(channel: number, index: number, value: number): void;
52
78
  }
53
79
 
54
80
  /** The Live object model. The whole reason [js] still exists in this stack. */
55
81
  declare class LiveAPI {
56
- constructor(pathOrCallback: string | ((args: unknown[]) => void), path?: string);
57
- property: string;
58
- unquotedpath: string;
59
- get(prop: string): unknown;
60
- set(prop: string, value: unknown): void;
61
- getcount(child: string): number;
62
- call(method: string, ...args: unknown[]): unknown;
82
+ constructor(pathOrCallback: string | ((args: unknown[]) => void), path?: string);
83
+ property: string;
84
+ unquotedpath: string;
85
+ get(prop: string): unknown;
86
+ set(prop: string, value: unknown): void;
87
+ getcount(child: string): number;
88
+ call(method: string, ...args: unknown[]): unknown;
63
89
  }
64
90
 
65
91
  /** Injected by @m4l-jweb/build: "<version> <iso date>". */