@m4l-jweb/wrapper 0.9.1 → 0.9.5

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/core.ts +141 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m4l-jweb/wrapper",
3
- "version": "0.9.1",
3
+ "version": "0.9.5",
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
@@ -753,6 +753,110 @@ function placeFetch(fetched: ActiveFetch): void {
753
753
  outlet(1, "maxurl", "dictionary", reqDict.name);
754
754
  }
755
755
 
756
+ /* ------------------------------------------------------------------ *
757
+ * Save-to-disk
758
+ *
759
+ * The inverse of fetch-to-disk: the APP has the bytes (a rendered WAV), and hands
760
+ * them over base64 in slices (`saveToFile()` in the bridge). We writebytes them into
761
+ * a `<dest>.part` file exactly as extractPayload does - same 16 KB truncation cap, same
762
+ * byte-count verification - then reuse the fetch machinery's PHASE 3 to atomically place
763
+ * `.part` over the destination through [maxurl]. The destination is never touched until
764
+ * the .part has been written AND verified. Requires the `download` chain (for [maxurl]).
765
+ * ------------------------------------------------------------------ */
766
+
767
+ var SAVE_PLACE_RESPONSE_DICT = "m4ljweb_save_place_response";
768
+
769
+ interface ActiveSave {
770
+ requestId: string;
771
+ /** Absolute, resolved destination. */
772
+ destPath: string;
773
+ /** Bytes the app promised in save_begin - the size the .part must match. */
774
+ expect: number;
775
+ /** Bytes actually written so far, for the verify. */
776
+ written: number;
777
+ /** The open .part file, held across the chunk messages. */
778
+ file: File | null;
779
+ }
780
+
781
+ var activeSave: ActiveSave | null = null;
782
+
783
+ /** The app: `save_begin <requestId> <destPath> <byteCount>` - open the .part file. */
784
+ function save_begin(requestId: string, destPath: string, byteCount: number): void {
785
+ // A save already in flight is abandoned - its .part is left for the next place/overwrite.
786
+ if (activeSave && activeSave.file && activeSave.file.isopen) activeSave.file.close();
787
+
788
+ var resolved = resolveFetchPath(destPath);
789
+ var target = partPath(resolved);
790
+ var f: File | null = null;
791
+ try {
792
+ f = new File(target, "write");
793
+ if (!f.isopen) f.open();
794
+ } catch (e) {
795
+ f = null;
796
+ }
797
+ if (!f || !f.isopen) {
798
+ outlet(0, "save_error", requestId, "cannot open " + target);
799
+ activeSave = null;
800
+ return;
801
+ }
802
+ f.eof = 0;
803
+ activeSave = { requestId: requestId, destPath: resolved, expect: Number(byteCount), written: 0, file: f };
804
+ }
805
+
806
+ /** The app: `save_chunk <requestId> <base64>` - write one slice. */
807
+ function save_chunk(requestId: string, b64: string): void {
808
+ if (!activeSave || activeSave.requestId !== requestId || !activeSave.file) return;
809
+ var bytes = b64decode(b64);
810
+ var SLICE = 4096; // File.writebytes truncates large calls (~16 KB cap) - same as extractPayload
811
+ for (var off = 0; off < bytes.length; off += SLICE) {
812
+ activeSave.file.writebytes(bytes.slice(off, off + SLICE));
813
+ }
814
+ activeSave.written += bytes.length;
815
+ }
816
+
817
+ /** The app: `save_end <requestId>` - close, verify size, place atomically. */
818
+ function save_end(requestId: string): void {
819
+ if (!activeSave || activeSave.requestId !== requestId) return;
820
+ var save = activeSave;
821
+ if (save.file && save.file.isopen) save.file.close();
822
+
823
+ var onDisk = fileSize(partPath(save.destPath));
824
+ if (onDisk !== save.expect) {
825
+ outlet(0, "save_error", requestId, "size mismatch: wrote " + onDisk + " bytes, expected " + save.expect);
826
+ activeSave = null;
827
+ return;
828
+ }
829
+ // PHASE 3: place the verified .part over the destination via [maxurl]. NOTE the
830
+ // destPath must be a FLAT filename in the device folder - maxurl (libcurl) and Max's
831
+ // [js] File resolve a subdirectory differently, so `sub/x.wav` writes the .part where
832
+ // File agrees but maxurl cannot reach it, and the place returns -1. Keep saves flat.
833
+ var placeUrl = encodeURI("file:///" + partPath(save.destPath));
834
+ post("m4l-jweb: save place " + placeUrl + " -> " + save.destPath + "\n");
835
+ var reqDict = new Dict();
836
+ reqDict.set("url", placeUrl);
837
+ reqDict.set("http_method", "get");
838
+ reqDict.set("filename_out", save.destPath);
839
+ reqDict.set("overwrite_output_file", 1);
840
+ reqDict.set("response_dict", SAVE_PLACE_RESPONSE_DICT);
841
+ outlet(1, "maxurl", "dictionary", reqDict.name);
842
+ }
843
+
844
+ /** maxurl finished the save's place copy. Validate on bytes, like finishPlace. */
845
+ function finishSavePlace(): void {
846
+ if (!activeSave) return;
847
+ var save = activeSave;
848
+ var placed = fileSize(save.destPath);
849
+ if (placed !== save.expect) {
850
+ outlet(0, "save_error", save.requestId, "could not place save: " + placed + " bytes at destination, expected " + save.expect);
851
+ activeSave = null;
852
+ return;
853
+ }
854
+ truncate(partPath(save.destPath)); // [js] cannot delete; zero it
855
+ post("m4l-jweb: saved " + placed + " bytes to " + save.destPath + "\n");
856
+ outlet(0, "save_done", save.requestId, placed);
857
+ activeSave = null;
858
+ }
859
+
756
860
  /* ------------------------------------------------------------------ *
757
861
  * Samples - the `samples` chain
758
862
  * ------------------------------------------------------------------ */
@@ -806,6 +910,41 @@ function buffer_load(slot: string, path: string): void {
806
910
  outlet(1, "buffer_replace", slot, resolved);
807
911
  }
808
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
+
809
948
  /** The app: `fetch_to_file <requestId> <url> <destPath>`. */
810
949
  function fetch_to_file(requestId: string, url: string, destPath: string): void {
811
950
  fetchQueue.push({ requestId: requestId, url: url, destPath: destPath, partBytes: 0 });
@@ -824,6 +963,8 @@ function maxurl_done(msgType: string, dictName: string): void {
824
963
  // hook, a request the device made lands here, finds no `currentFetch`, and is
825
964
  // dropped in silence - which looks exactly like maxurl never answering.
826
965
  if (typeof onMaxurlReply === "function" && onMaxurlReply(dictName)) return;
966
+ // A save's place copy comes back on its own dict, and has no `currentFetch` behind it.
967
+ if (dictName === SAVE_PLACE_RESPONSE_DICT) return finishSavePlace();
827
968
  if (!currentFetch) return;
828
969
  var fetched = currentFetch;
829
970