@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.
- package/README.md +41 -0
- package/package.json +1 -1
- 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
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 =
|
|
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(
|
|
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:///" +
|
|
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(
|
|
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 });
|