@m4l-jweb/wrapper 0.9.5 → 1.0.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 +41 -0
  2. package/package.json +1 -1
  3. package/src/core.ts +26 -92
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @m4l-jweb/wrapper
2
+
3
+ The Max-side glue: the ES5 `[js]` script that owns a device's lifecycle, its LiveAPI work, transport polling, clip I/O and file writes. You do not usually import this - `@m4l-jweb/build` compiles it into every device it produces.
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/wrapper
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ // Only a device repo extending the wrapper needs this: wrapper/device.ts is
17
+ // compiled together with the packaged sources into one ES5 script.
18
+ //
19
+ // Everything here must be ES5 - Max's [js] is not a modern JavaScript engine, and the
20
+ // build proves it with acorn before packaging.
21
+ ```
22
+
23
+ ## Notes
24
+
25
+ - `[js]` runs even inside a frozen device, and it is the only place LiveAPI exists - which is why the lifecycle lives here rather than in the browser.
26
+ - This package exists mainly so the build can find the wrapper sources (`@m4l-jweb/wrapper/sources`). It has no browser-facing API of its own.
27
+ - **`[node.script]` is never used.** It proved unstable in the field - silent non-start, then a full Live crash.
28
+
29
+ ## Requirements
30
+
31
+ Ableton Live 12 with Max 9. Devices are built on `[jweb~]`, the browser view with signal outlets; older hosts are unverified.
32
+
33
+ ## Links
34
+
35
+ - [Repository and full README](https://github.com/alienmind/m4l-jweb)
36
+ - [Architecture](https://github.com/alienmind/m4l-jweb/blob/main/doc/ARCHITECTURE.md)
37
+ - [What Max actually does: the measured facts](https://github.com/alienmind/m4l-jweb/blob/main/doc/MAX-FACTS.md)
38
+
39
+ ## License
40
+
41
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m4l-jweb/wrapper",
3
- "version": "0.9.5",
3
+ "version": "1.0.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",
package/src/core.ts CHANGED
@@ -690,6 +690,28 @@ function partPath(destPath: string): string {
690
690
  return destPath + ".part";
691
691
  }
692
692
 
693
+ /**
694
+ * Where a SAVE lands before it has earned its destination - and deliberately NOT
695
+ * `<dest>.part`.
696
+ *
697
+ * `[js]` cannot delete a file, so a spent `.part` is truncated to zero bytes and left.
698
+ * For a download that is harmless: the destination is derived from the URL, so the same
699
+ * sample re-fetched reuses (and overwrites) the same scratch file. A SAVE has no such
700
+ * guarantee - an audio export names its file after the moment it was rendered, so every
701
+ * bounce would strand another 0-byte `superdough-export-<n>.wav.part` next to the real
702
+ * one, forever.
703
+ *
704
+ * One fixed scratch name per device folder instead: reused, overwritten, and never more
705
+ * than a single stray zero-byte file however many exports are made. Safe because saves
706
+ * are strictly one at a time (`activeSave`), and separate from the fetch scratch so a
707
+ * download in flight cannot collide with a save.
708
+ */
709
+ function savePartPath(destPath: string): string {
710
+ var cut = destPath.lastIndexOf("/");
711
+ var dir = cut < 0 ? "" : destPath.slice(0, cut + 1);
712
+ return dir + "m4l-jweb-save.part";
713
+ }
714
+
693
715
  interface ActiveFetch {
694
716
  requestId: string;
695
717
  url: string;
@@ -786,7 +808,7 @@ function save_begin(requestId: string, destPath: string, byteCount: number): voi
786
808
  if (activeSave && activeSave.file && activeSave.file.isopen) activeSave.file.close();
787
809
 
788
810
  var resolved = resolveFetchPath(destPath);
789
- var target = partPath(resolved);
811
+ var target = savePartPath(resolved);
790
812
  var f: File | null = null;
791
813
  try {
792
814
  f = new File(target, "write");
@@ -820,7 +842,7 @@ function save_end(requestId: string): void {
820
842
  var save = activeSave;
821
843
  if (save.file && save.file.isopen) save.file.close();
822
844
 
823
- var onDisk = fileSize(partPath(save.destPath));
845
+ var onDisk = fileSize(savePartPath(save.destPath));
824
846
  if (onDisk !== save.expect) {
825
847
  outlet(0, "save_error", requestId, "size mismatch: wrote " + onDisk + " bytes, expected " + save.expect);
826
848
  activeSave = null;
@@ -830,7 +852,7 @@ function save_end(requestId: string): void {
830
852
  // destPath must be a FLAT filename in the device folder - maxurl (libcurl) and Max's
831
853
  // [js] File resolve a subdirectory differently, so `sub/x.wav` writes the .part where
832
854
  // File agrees but maxurl cannot reach it, and the place returns -1. Keep saves flat.
833
- var placeUrl = encodeURI("file:///" + partPath(save.destPath));
855
+ var placeUrl = encodeURI("file:///" + savePartPath(save.destPath));
834
856
  post("m4l-jweb: save place " + placeUrl + " -> " + save.destPath + "\n");
835
857
  var reqDict = new Dict();
836
858
  reqDict.set("url", placeUrl);
@@ -851,100 +873,12 @@ function finishSavePlace(): void {
851
873
  activeSave = null;
852
874
  return;
853
875
  }
854
- truncate(partPath(save.destPath)); // [js] cannot delete; zero it
876
+ truncate(savePartPath(save.destPath)); // [js] cannot delete; zero it, and reuse it
855
877
  post("m4l-jweb: saved " + placed + " bytes to " + save.destPath + "\n");
856
878
  outlet(0, "save_done", save.requestId, placed);
857
879
  activeSave = null;
858
880
  }
859
881
 
860
- /* ------------------------------------------------------------------ *
861
- * Samples - the `samples` chain
862
- * ------------------------------------------------------------------ */
863
-
864
- /**
865
- * The app: `buffer_load <slot> <path>` - read a file into that slot's [buffer~].
866
- *
867
- * WHY THIS GOES THROUGH [js] AT ALL, when the chain could route it straight to the
868
- * buffer: because the path the app wrote is not a path [buffer~] can open, and it
869
- * fails in the two ways this file exists to prevent.
870
- *
871
- * A RELATIVE path is resolved against the device's folder - the same resolution
872
- * `fetch_to_file` does, and it has to be the same one or the app downloads a file to
873
- * one place and loads it from another. [buffer~] does not resolve it that way: a bare
874
- * name is looked up in MAX's SEARCH PATH, which does not contain the device's folder,
875
- * so `preview.wav` - freshly downloaded, right there next to the .amxd - reports
876
- * "can't open" and the promise times out.
877
- *
878
- * ...and the resolved path CONTAINS SPACES on a normal Live install ("Ableton
879
- * Library", "Max For Live"). A message travelling through the patcher as text would
880
- * split there into three atoms and [buffer~] would open the first one. Handed out of
881
- * [js] as a string, it stays ONE symbol all the way to `replace`.
882
- *
883
- * The file is checked before the buffer is asked for it, because a missing file makes
884
- * [buffer~] print to the Max console and stay silent - there is no failure bang to
885
- * bind to, and the app would learn nothing until the timeout. `buffer_error` says so
886
- * at once.
887
- */
888
- function buffer_load(slot: string, path: string): void {
889
- var resolved = resolveFetchPath(path); // the same folder the download wrote to
890
- var f: File | null = null;
891
- try {
892
- f = new File(resolved, "read");
893
- } catch (e) {
894
- f = null;
895
- }
896
- if (!f || !f.isopen) {
897
- outlet(0, "buffer_error", slot, "no file at " + resolved);
898
- return;
899
- }
900
- var bytes = f.eof;
901
- f.close();
902
- if (!bytes) {
903
- outlet(0, "buffer_error", slot, "empty file at " + resolved);
904
- return;
905
- }
906
-
907
- // Outlet 1 is the aux outlet; the `samples` chain routes `buffer_replace` off it,
908
- // exactly as the `download` chain routes `maxurl`. Outlet 0 belongs to [jweb].
909
- post("m4l-jweb: buffer_load " + slot + " -> " + resolved + "\n");
910
- outlet(1, "buffer_replace", slot, resolved);
911
- }
912
-
913
- /**
914
- * The app: `render_load <slot> <path> <lengthBeats>` - read a rendered WAV into a
915
- * renderplay slot's [buffer~].
916
- *
917
- * Same path problem, same fix as buffer_load: a relative path is resolved against the
918
- * device folder (where saveToFile wrote it), and the resolved path - which contains
919
- * spaces on a normal Live install - is handed out of [js] as ONE symbol via the aux
920
- * outlet, so `replace` sees a single file rather than three atoms. The loop length rides
921
- * alongside as `render_len` for the chain's transport-lock detector.
922
- */
923
- function render_load(slot: string, path: string, lengthBeats: number): void {
924
- var resolved = resolveFetchPath(path);
925
- var f: File | null = null;
926
- try {
927
- f = new File(resolved, "read");
928
- } catch (e) {
929
- f = null;
930
- }
931
- if (!f || !f.isopen) {
932
- outlet(0, "render_error", slot, "no file at " + resolved);
933
- return;
934
- }
935
- var bytes = f.eof;
936
- f.close();
937
- if (!bytes) {
938
- outlet(0, "render_error", slot, "empty file at " + resolved);
939
- return;
940
- }
941
- post("m4l-jweb: render_load " + slot + " -> " + resolved + " (" + lengthBeats + " beats)\n");
942
- // Aux outlet (1): the renderplay chain routes render_replace and render_len off it,
943
- // the same way the download chain routes maxurl. Outlet 0 belongs to [jweb].
944
- outlet(1, "render_len", slot, lengthBeats);
945
- outlet(1, "render_replace", slot, resolved);
946
- }
947
-
948
882
  /** The app: `fetch_to_file <requestId> <url> <destPath>`. */
949
883
  function fetch_to_file(requestId: string, url: string, destPath: string): void {
950
884
  fetchQueue.push({ requestId: requestId, url: url, destPath: destPath, partBytes: 0 });