@m4l-jweb/build 0.9.1 → 0.9.9
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/package.json +2 -2
- package/src/chains.mjs +16 -426
- package/templates/base.json +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m4l-jweb/build",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"description": "m4l-jweb: the CLI that builds and packages a device repo into installable Max for Live devices.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"archiver": "^7.0.1",
|
|
35
35
|
"esbuild": "^0.25.0",
|
|
36
36
|
"typescript": "^5.7.0",
|
|
37
|
-
"@m4l-jweb/wrapper": "0.9.
|
|
37
|
+
"@m4l-jweb/wrapper": "0.9.9"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/chains.mjs
CHANGED
|
@@ -82,7 +82,7 @@ export function removeLine(lines, srcId, dstId) {
|
|
|
82
82
|
* and fatal in Live - it is the bang every LiveAPI observer is created from.
|
|
83
83
|
*/
|
|
84
84
|
export function claimAppMessages(ctx, routeId, unmatchedOutlet) {
|
|
85
|
-
const [srcId, srcOutlet] = ctx.appOut ?? [ctx.jwebId,
|
|
85
|
+
const [srcId, srcOutlet] = ctx.appOut ?? [ctx.jwebId, 2];
|
|
86
86
|
|
|
87
87
|
// Nobody has claimed the stream yet, and yet [jweb] no longer reaches the
|
|
88
88
|
// wrapper: a chain cut that cord by hand (the old `removeLine(jwebId,
|
|
@@ -748,205 +748,7 @@ function downloadChain(ctx) {
|
|
|
748
748
|
lines.push(line("obj-prepend-maxurl-progress", 0, unmatchedId, 0));
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
-
|
|
752
|
-
* "samples" - the first chain that ORIGINATES a sound: a named [buffer~] per slot,
|
|
753
|
-
* loaded from a file on disk, played back through [groove~] into the signal path.
|
|
754
|
-
*
|
|
755
|
-
* app -> buffer_load <slot> <path> -> [js] (resolves the path)
|
|
756
|
-
* -> buffer_replace -> [buffer~ <name>]
|
|
757
|
-
* buffer_play <slot> -> [groove~] set + play
|
|
758
|
-
* buffer_stop -> [groove~] stop
|
|
759
|
-
* app <- buffer_ready <slot> <sr> <ms> <chans> when the read actually completed
|
|
760
|
-
* buffer_error <slot> <msg> when there was no file to read
|
|
761
|
-
*
|
|
762
|
-
* THE BYTES NEVER CROSS THE BRIDGE. [buffer~] reads the file itself; what travels in
|
|
763
|
-
* Max messages is a path and, coming back, a description of what landed - the same
|
|
764
|
-
* rule the `download` chain follows, which is how you get the file there in the
|
|
765
|
-
* first place.
|
|
766
|
-
*
|
|
767
|
-
* WAV/AIFF/Next-Sun ONLY. [buffer~]'s `read`/`replace` does not take MP3 - that list
|
|
768
|
-
* (MP3, OGG, FLAC, M4A) is [sfplay~]'s, which streams from disk rather than filling a
|
|
769
|
-
* buffer, and is therefore a different chain. A format it cannot read is an error in
|
|
770
|
-
* the Max console and NO bang, so the app's promise times out rather than lying.
|
|
771
|
-
*
|
|
772
|
-
* WHAT LOADED IS NOT WHAT YOU ASKED FOR, so the chain reports what it GOT. `replace`
|
|
773
|
-
* resizes the buffer and adopts the FILE's channel count and sample rate, so a slot
|
|
774
|
-
* is not mono because you wanted it to be. [info~] is banged from the buffer's own
|
|
775
|
-
* "read completed" outlet (outlet 1 - outlet 0 is a mouse position in the editing
|
|
776
|
-
* window) and reports sample rate, duration and channels; the app derives frames from
|
|
777
|
-
* those. A frame count on its own is not proof of a read: a failed `replace` leaves
|
|
778
|
-
* the PREVIOUS contents in place, so "there are samples in there" says nothing. The
|
|
779
|
-
* bang does - it only fires when a read completed - and until it does the app's
|
|
780
|
-
* promise is still open (see loadSample() in @m4l-jweb/bridge, which is where the
|
|
781
|
-
* timeout lives).
|
|
782
|
-
*
|
|
783
|
-
* info~'s outlets fire right-to-left, so the LAST one to arrive is outlet 0, the
|
|
784
|
-
* sample rate - which is therefore the one wired to [pack]'s hot inlet. Wire it the
|
|
785
|
-
* other way round and the message goes out carrying the PREVIOUS load's numbers.
|
|
786
|
-
*
|
|
787
|
-
* ONE VOICE, DELIBERATELY. `groove~` takes `set <buffer-name>` to switch buffers, so
|
|
788
|
-
* one stereo player covers N slots: this is a PREVIEW - the sample browser's "let me
|
|
789
|
-
* hear it, through the track" - not a sampler. Polyphony is the `instrument` chain,
|
|
790
|
-
* and it is still open (doc/TODO.md item 2).
|
|
791
|
-
*
|
|
792
|
-
* It SUMS into the signal path rather than claiming it ([+~]), because it makes sound
|
|
793
|
-
* of its own: on an audio effect the preview plays over the track's audio, and on an
|
|
794
|
-
* instrument there is nothing at the input to add.
|
|
795
|
-
*
|
|
796
|
-
* The buffer names are INSTANCE-SCOPED (`deviceBufName`, `---buf-<device>-<slot>`), so
|
|
797
|
-
* two copies of this device on two tracks own separate buffers. They used to be global
|
|
798
|
-
* to Max and generated from the device name alone, which meant the second copy loaded
|
|
799
|
-
* silently stole the first's samples.
|
|
800
|
-
*
|
|
801
|
-
* Slots default to one, named "preview". `slots: ["kick", "snare"]` in the manifest
|
|
802
|
-
* gives you more.
|
|
803
|
-
*/
|
|
804
|
-
function samplesChain(ctx) {
|
|
805
|
-
const { boxes, lines, device, jwebId, unmatchedId } = ctx;
|
|
806
|
-
const slots = device?.slots ?? ["preview"];
|
|
807
|
-
// Instance-scoped, for the same reason the instrument's are: two copies of a preview
|
|
808
|
-
// device on two tracks are two devices, and a global name would hand both the same
|
|
809
|
-
// buffer. `---` expands per device instance in Live - see deviceBufName.
|
|
810
|
-
const bufName = (slot) => deviceBufName(device, slot);
|
|
811
|
-
|
|
812
|
-
// `buffer_load` is NOT claimed from [jweb]. It goes on to the wrapper, which
|
|
813
|
-
// resolves the path and hands it back on its AUX OUTLET as `buffer_replace <slot>
|
|
814
|
-
// <abs path>` - the same shape the `download` chain takes `maxurl` in.
|
|
815
|
-
//
|
|
816
|
-
// The detour is the whole fix. The app writes a path relative to the device's
|
|
817
|
-
// folder (that is where fetchToFile puts the file), and [buffer~] does not resolve
|
|
818
|
-
// it that way: a bare name is looked up in MAX'S SEARCH PATH, which the device's
|
|
819
|
-
// folder is not in, so a file that was downloaded correctly reports "can't open".
|
|
820
|
-
// The resolved path then contains SPACES on a normal Live install ("Ableton
|
|
821
|
-
// Library"), and a path travelling through the patcher as message text would split
|
|
822
|
-
// there into atoms. Out of [js] it stays one symbol.
|
|
823
|
-
boxes.push(box("obj-samples-route", "route buffer_play buffer_stop", { numoutlets: 3, outlettype: ["", "", ""] }));
|
|
824
|
-
claimAppMessages(ctx, "obj-samples-route", 2);
|
|
825
|
-
|
|
826
|
-
boxes.push(box("obj-samples-replaceroute", "route buffer_replace", { numoutlets: 2, outlettype: ["", ""] }));
|
|
827
|
-
lines.push(line(unmatchedId, 1, "obj-samples-replaceroute", 0));
|
|
828
|
-
|
|
829
|
-
// `route` strips the selector, so the slot name is now the first word of both
|
|
830
|
-
// remaining messages: a second route per stream dispatches on it.
|
|
831
|
-
const slotList = slots.join(" ");
|
|
832
|
-
const slotOutlets = { numoutlets: slots.length + 1, outlettype: slots.map(() => "").concat("") };
|
|
833
|
-
boxes.push(box("obj-samples-loadslot", `route ${slotList}`, slotOutlets));
|
|
834
|
-
boxes.push(box("obj-samples-playslot", `route ${slotList}`, slotOutlets));
|
|
835
|
-
lines.push(line("obj-samples-replaceroute", 0, "obj-samples-loadslot", 0));
|
|
836
|
-
lines.push(line("obj-samples-route", 0, "obj-samples-playslot", 0));
|
|
837
|
-
|
|
838
|
-
// The player. `groove~ <buffer> 2` = two signal outlets (plus a loop-sync outlet),
|
|
839
|
-
// and it MIXES a buffer with more channels down rather than dropping them.
|
|
840
|
-
// @loop 0 makes it a one-shot: a preview that loops forever is a preview you have
|
|
841
|
-
// to fight. [sig~ 1.] is the playback rate, in the left inlet, which is a SIGNAL
|
|
842
|
-
// inlet - a float there means something else entirely (a position, in ms).
|
|
843
|
-
boxes.push(box("obj-samples-rate", "sig~ 1.", { numinlets: 1, numoutlets: 1, outlettype: ["signal"] }));
|
|
844
|
-
boxes.push(
|
|
845
|
-
box("obj-samples-groove", `groove~ ${bufName(slots[0])} 2 @loop 0`, {
|
|
846
|
-
numinlets: 3,
|
|
847
|
-
numoutlets: 3,
|
|
848
|
-
outlettype: ["signal", "signal", "signal"],
|
|
849
|
-
}),
|
|
850
|
-
);
|
|
851
|
-
lines.push(line("obj-samples-rate", 0, "obj-samples-groove", 0));
|
|
852
|
-
|
|
853
|
-
// MONO FOLD. `groove~ <buf> 2` hard-wires two signal outlets to L and R, and a
|
|
854
|
-
// MONO buffer drives outlet 0 ONLY - so a mono file (most of tidal-drum-machines
|
|
855
|
-
// is mono) plays in one ear. The channel count is not a build-time fact: [info~]
|
|
856
|
-
// MEASURES it when the buffer loads. So gate the R channel at RUNTIME - a
|
|
857
|
-
// [selector~ 2] whose control says which groove~ outlet is the real right channel:
|
|
858
|
-
// input 1 = groove~ outlet 1 (a stereo file's true R)
|
|
859
|
-
// input 2 = groove~ outlet 0 (fold the mono signal to R as well)
|
|
860
|
-
// The control is the currently-loaded slot's channel count run through
|
|
861
|
-
// [expr ($i1==1)+1]: mono(1) -> 2 (fold), stereo(2) -> 1 (real R). The L channel
|
|
862
|
-
// always takes groove~ outlet 0, so it needs no gate.
|
|
863
|
-
boxes.push(box("obj-samples-rsel", "selector~ 2", { numinlets: 3, numoutlets: 1, outlettype: ["signal"] }));
|
|
864
|
-
boxes.push(box("obj-samples-rgate", "expr ($i1==1)+1", { numinlets: 1, numoutlets: 1, outlettype: ["int"] }));
|
|
865
|
-
lines.push(line("obj-samples-groove", 1, "obj-samples-rsel", 1)); // stereo R -> input 1
|
|
866
|
-
lines.push(line("obj-samples-groove", 0, "obj-samples-rsel", 2)); // mono fold -> input 2
|
|
867
|
-
lines.push(line("obj-samples-rgate", 0, "obj-samples-rsel", 0)); // which one, per loaded slot
|
|
868
|
-
|
|
869
|
-
// Stop: a bare `buffer_stop` arrives from [route] as a BANG - the word is gone -
|
|
870
|
-
// so re-materialize it in a message box, or groove~ hears nothing it knows.
|
|
871
|
-
boxes.push(box("obj-samples-stopmsg", "stop", { maxclass: "message", numinlets: 2, numoutlets: 1 }));
|
|
872
|
-
lines.push(line("obj-samples-route", 1, "obj-samples-stopmsg", 0));
|
|
873
|
-
lines.push(line("obj-samples-stopmsg", 0, "obj-samples-groove", 0));
|
|
874
|
-
|
|
875
|
-
slots.forEach((slot, i) => {
|
|
876
|
-
const buf = `obj-samples-buf-${slot}`;
|
|
877
|
-
const info = `obj-samples-info-${slot}`;
|
|
878
|
-
|
|
879
|
-
// buffer~: outlet 0 is a mouse position, outlet 1 is the bang on a completed
|
|
880
|
-
// read. Only the second one means anything here.
|
|
881
|
-
boxes.push(box(buf, `buffer~ ${bufName(slot)}`, { numinlets: 1, numoutlets: 2, outlettype: ["float", "bang"] }));
|
|
882
|
-
boxes.push(box(`obj-samples-replace-${slot}`, "prepend replace"));
|
|
883
|
-
lines.push(line("obj-samples-loadslot", i, `obj-samples-replace-${slot}`, 0));
|
|
884
|
-
lines.push(line(`obj-samples-replace-${slot}`, 0, buf, 0));
|
|
885
|
-
|
|
886
|
-
// What actually landed. info~'s outlets: 0 = sample rate, 6 = duration in ms,
|
|
887
|
-
// 8 = number of channels (per its reference page - do not count them from
|
|
888
|
-
// memory). They fire right-to-left, so 0 arrives last and is the hot one.
|
|
889
|
-
boxes.push(
|
|
890
|
-
box(info, `info~ ${bufName(slot)}`, {
|
|
891
|
-
numinlets: 1,
|
|
892
|
-
numoutlets: 10,
|
|
893
|
-
outlettype: ["float", "list", "float", "float", "float", "float", "float", "", "int", ""],
|
|
894
|
-
}),
|
|
895
|
-
);
|
|
896
|
-
boxes.push(box(`obj-samples-pack-${slot}`, "pack 0. 0. 0", { numinlets: 3, numoutlets: 1, outlettype: [""] }));
|
|
897
|
-
boxes.push(box(`obj-samples-ready-${slot}`, `prepend buffer_ready ${slot}`));
|
|
898
|
-
|
|
899
|
-
lines.push(line(buf, 1, info, 0)); // the read completed - ask what it was
|
|
900
|
-
lines.push(line(info, 8, `obj-samples-pack-${slot}`, 2)); // channels (fires first)
|
|
901
|
-
lines.push(line(info, 6, `obj-samples-pack-${slot}`, 1)); // duration, ms
|
|
902
|
-
lines.push(line(info, 0, `obj-samples-pack-${slot}`, 0)); // sample rate - hot, last
|
|
903
|
-
lines.push(line(`obj-samples-pack-${slot}`, 0, `obj-samples-ready-${slot}`, 0));
|
|
904
|
-
lines.push(line(`obj-samples-ready-${slot}`, 0, jwebId, 0));
|
|
905
|
-
|
|
906
|
-
// Retain THIS slot's measured channel count, so play can re-assert the mono
|
|
907
|
-
// fold for whichever buffer is loaded into the one shared groove~. [info~]
|
|
908
|
-
// outlet 8 stores it in [f]'s cold inlet (no output); the play trigger bangs it
|
|
909
|
-
// out to the shared gate below. Every slot's [f] feeds that one gate.
|
|
910
|
-
boxes.push(box(`obj-samples-chans-${slot}`, "f", { numinlets: 2, numoutlets: 1, outlettype: [""] }));
|
|
911
|
-
lines.push(line(info, 8, `obj-samples-chans-${slot}`, 1)); // measured channels, cold-stored
|
|
912
|
-
lines.push(line(`obj-samples-chans-${slot}`, 0, "obj-samples-rgate", 0));
|
|
913
|
-
|
|
914
|
-
// Play: pick the buffer, THEN start it. Two cords out of one outlet fire in an
|
|
915
|
-
// order Max chooses, so the sequence goes through a [t b b] - right outlet
|
|
916
|
-
// first - and never through a fan-out. Starting the OLD buffer and then
|
|
917
|
-
// switching is exactly the bug that looks like "the wrong sample previewed".
|
|
918
|
-
boxes.push(box(`obj-samples-trig-${slot}`, "t b b b", { numinlets: 1, numoutlets: 3, outlettype: ["bang", "bang", "bang"] }));
|
|
919
|
-
boxes.push(box(`obj-samples-set-${slot}`, `set ${bufName(slot)}`, { maxclass: "message", numinlets: 2, numoutlets: 1 }));
|
|
920
|
-
// A float in groove~'s left inlet is a playback POSITION in ms, and 0 is "from
|
|
921
|
-
// the beginning" - which is also what starts it after a `stop`.
|
|
922
|
-
boxes.push(box(`obj-samples-start-${slot}`, "0", { maxclass: "message", numinlets: 2, numoutlets: 1 }));
|
|
923
|
-
|
|
924
|
-
// [t] fires right-to-left, so: set the fold gate, THEN the buffer, THEN start -
|
|
925
|
-
// the gate and the buffer are both in place before groove~ makes a sound.
|
|
926
|
-
lines.push(line("obj-samples-playslot", i, `obj-samples-trig-${slot}`, 0));
|
|
927
|
-
lines.push(line(`obj-samples-trig-${slot}`, 2, `obj-samples-chans-${slot}`, 0)); // first: assert the fold
|
|
928
|
-
lines.push(line(`obj-samples-trig-${slot}`, 1, `obj-samples-set-${slot}`, 0)); // then: set the buffer
|
|
929
|
-
lines.push(line(`obj-samples-trig-${slot}`, 0, `obj-samples-start-${slot}`, 0)); // ...then play it
|
|
930
|
-
lines.push(line(`obj-samples-set-${slot}`, 0, "obj-samples-groove", 0));
|
|
931
|
-
lines.push(line(`obj-samples-start-${slot}`, 0, "obj-samples-groove", 0));
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
// Sum into the signal path: this chain MAKES sound, it does not process what came
|
|
935
|
-
// before. Claiming the stage instead would silence the track the preview plays over.
|
|
936
|
-
for (const [ch, id] of [
|
|
937
|
-
[0, "obj-samples-mix-l"],
|
|
938
|
-
[1, "obj-samples-mix-r"],
|
|
939
|
-
]) {
|
|
940
|
-
const [srcId, srcOut] = ctx.audioIn(ch);
|
|
941
|
-
boxes.push(box(id, "+~", { numinlets: 2, numoutlets: 1, outlettype: ["signal"] }));
|
|
942
|
-
lines.push(line(srcId, srcOut, id, 0));
|
|
943
|
-
// L takes groove~ outlet 0 directly; R takes the mono-fold [selector~] instead
|
|
944
|
-
// of groove~ outlet 1, so a mono buffer reaches both ears.
|
|
945
|
-
if (ch === 0) lines.push(line("obj-samples-groove", 0, id, 1));
|
|
946
|
-
else lines.push(line("obj-samples-rsel", 0, id, 1));
|
|
947
|
-
ctx.setAudioOut(ch, id, 0);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
751
|
+
|
|
950
752
|
|
|
951
753
|
/**
|
|
952
754
|
* "delay" - a feedback delay, sent from a dry/wet knob: `.delay()`, `.delaytime()`
|
|
@@ -1078,244 +880,32 @@ function reverbChain(ctx) {
|
|
|
1078
880
|
}
|
|
1079
881
|
}
|
|
1080
882
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
*
|
|
1084
|
-
* [poly~] loads N copies of this and hands each `note` message to the first voice
|
|
1085
|
-
* whose [thispoly~] is not busy - so polyphony and voice-stealing are Max's job, not
|
|
1086
|
-
* the app's, which is the whole reason to spend a [poly~] rather than run N groove~s
|
|
1087
|
-
* and a scheduler by hand. Max cannot embed this inline (no factory device does; they
|
|
1088
|
-
* all ship it as a named .maxpat), so the build freezes it into the .amxd as a
|
|
1089
|
-
* dependency and [poly~] resolves it by name from the device's own bundle - the same
|
|
1090
|
-
* way `Analogue Drums.amxd` carries `analog.Kick~.maxpat` (checked on disk).
|
|
1091
|
-
*
|
|
1092
|
-
* MULTI-SAMPLE. A voice can play ANY of the device's slots - one named [buffer~] per
|
|
1093
|
-
* slot - so the instrument is a keymap, not one repitched sample. The voice request at
|
|
1094
|
-
* [in 1] is the list `slot rate velocity durMs channels`:
|
|
1095
|
-
* - slot -> which buffer: [sel 0 1 ...] picks the matching `set <bufName>`
|
|
1096
|
-
* message, so groove~ switches to that slot's buffer before it starts.
|
|
1097
|
-
* - rate -> playback RATE straight into [sig~] -> groove~'s left (signal) inlet.
|
|
1098
|
-
* EXPLICIT, not derived: the app decides whether a note plays a
|
|
1099
|
-
* dedicated sample at rate 1 or a repitched one at rate 2, which is
|
|
1100
|
-
* what makes this a multi-sample keymap rather than one stretched
|
|
1101
|
-
* buffer. No pitch->rate arithmetic here.
|
|
1102
|
-
* - velocity -> amplitude, /127, on both channels' [*~].
|
|
1103
|
-
* - durMs -> when to FREE the voice: [delay durMs] -> 0 to [thispoly~] and
|
|
1104
|
-
* `stop` to groove~. The app times the note; Max holds the voice
|
|
1105
|
-
* exactly that long. A one-shot past the sample's end is silent
|
|
1106
|
-
* anyway (@loop 0), so this only bounds the allocation.
|
|
1107
|
-
* - channels -> the mono fold, the SAME runtime gate the samples chain uses: a
|
|
1108
|
-
* mono buffer drives groove~ outlet 0 only, so a [selector~ 2] keyed
|
|
1109
|
-
* on the measured channel count folds it into R (see samplesChain).
|
|
1110
|
-
*
|
|
1111
|
-
* The order is sequenced with [t]: the whole list reaches [unpack] (buffer selected,
|
|
1112
|
-
* rate, gate and duration all set) BEFORE the voice marks itself busy and starts
|
|
1113
|
-
* groove~ from 0. Buffer-select is separate from start - `set <buf>` switches the
|
|
1114
|
-
* buffer WITHOUT playing, and only the seq's `0` starts it - so a note never begins on
|
|
1115
|
-
* the previous note's buffer or rate.
|
|
1116
|
-
*/
|
|
1117
|
-
function instrumentVoicePatch(bufNames) {
|
|
1118
|
-
let vy = 40;
|
|
1119
|
-
const vbox = (id, text, extra = {}) => ({
|
|
1120
|
-
box: { id, maxclass: "newobj", text, numinlets: 1, numoutlets: 1, outlettype: [""], patching_rect: [24, (vy += 30), 150, 22], ...extra },
|
|
1121
|
-
});
|
|
1122
|
-
const vmsg = (id, text) => ({
|
|
1123
|
-
box: { id, maxclass: "message", text, numinlets: 2, numoutlets: 1, outlettype: [""], patching_rect: [200, (vy += 30), 90, 22] },
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
const L = (s, so, d, di) => ({ patchline: { source: [s, so], destination: [d, di] } });
|
|
1127
|
-
|
|
1128
|
-
const boxes = [
|
|
1129
|
-
vbox("v-in", "in 1", { numinlets: 0, numoutlets: 1 }),
|
|
1130
|
-
vbox("v-trig", "t b l", { numoutlets: 2, outlettype: ["bang", ""] }),
|
|
1131
|
-
vbox("v-unpack", "unpack 0 0. 0 0 0", { numoutlets: 5, outlettype: ["int", "float", "int", "int", "int"] }),
|
|
1132
|
-
vbox("v-seq", "t b b", { numoutlets: 2, outlettype: ["bang", "bang"] }),
|
|
1133
|
-
vmsg("v-busy", "1"),
|
|
1134
|
-
vmsg("v-start", "0"),
|
|
1135
|
-
vbox("v-thispoly", "thispoly~", { numoutlets: 2, outlettype: ["int", "int"] }),
|
|
1136
|
-
// slot -> which buffer. [sel] fires the matching outlet; the last (rightmost) is
|
|
1137
|
-
// the no-match passthrough, left unwired.
|
|
1138
|
-
vbox("v-sel", `sel ${bufNames.map((_, i) => i).join(" ")}`, { numoutlets: bufNames.length + 1, outlettype: bufNames.map(() => "bang").concat("") }),
|
|
1139
|
-
vbox("v-sig", "sig~", { numoutlets: 1, outlettype: ["signal"] }),
|
|
1140
|
-
vbox("v-vel", "/ 127.", { outlettype: ["float"] }),
|
|
1141
|
-
// groove~ starts on the FIRST slot's buffer; `set` switches it per note.
|
|
1142
|
-
vbox("v-groove", `groove~ ${bufNames[0]} 2 @loop 0`, { numinlets: 3, numoutlets: 3, outlettype: ["signal", "signal", "signal"] }),
|
|
1143
|
-
vbox("v-gate", "expr ($i1==1)+1", { outlettype: ["int"] }),
|
|
1144
|
-
vbox("v-rsel", "selector~ 2", { numinlets: 3, numoutlets: 1, outlettype: ["signal"] }),
|
|
1145
|
-
vbox("v-ampL", "*~", { numinlets: 2, numoutlets: 1, outlettype: ["signal"] }),
|
|
1146
|
-
vbox("v-ampR", "*~", { numinlets: 2, numoutlets: 1, outlettype: ["signal"] }),
|
|
1147
|
-
vbox("v-del", "delay", { numinlets: 2, numoutlets: 1, outlettype: ["bang"] }),
|
|
1148
|
-
vbox("v-free", "t b b", { numoutlets: 2, outlettype: ["bang", "bang"] }),
|
|
1149
|
-
vmsg("v-freebusy", "0"),
|
|
1150
|
-
vmsg("v-stop", "stop"),
|
|
1151
|
-
vbox("v-out1", "out~ 1", { numinlets: 1, numoutlets: 0 }),
|
|
1152
|
-
vbox("v-out2", "out~ 2", { numinlets: 1, numoutlets: 0 }),
|
|
1153
|
-
];
|
|
1154
|
-
|
|
1155
|
-
const lines = [
|
|
1156
|
-
L("v-in", 0, "v-trig", 0),
|
|
1157
|
-
// list first (right outlet), then the start sequence (left) - values before play.
|
|
1158
|
-
L("v-trig", 1, "v-unpack", 0),
|
|
1159
|
-
L("v-trig", 0, "v-seq", 0),
|
|
1160
|
-
L("v-unpack", 0, "v-sel", 0), // slot -> pick a buffer
|
|
1161
|
-
L("v-unpack", 1, "v-sig", 0), // rate (explicit) -> signal rate
|
|
1162
|
-
L("v-sig", 0, "v-groove", 0),
|
|
1163
|
-
L("v-unpack", 2, "v-vel", 0), // velocity -> amp
|
|
1164
|
-
L("v-vel", 0, "v-ampL", 1),
|
|
1165
|
-
L("v-vel", 0, "v-ampR", 1),
|
|
1166
|
-
L("v-unpack", 3, "v-del", 1), // duration -> delay time (cold)
|
|
1167
|
-
L("v-unpack", 4, "v-gate", 0), // channels -> mono fold gate
|
|
1168
|
-
L("v-gate", 0, "v-rsel", 0),
|
|
1169
|
-
// the start sequence: mark busy, then start groove~ from 0 AND arm the free timer.
|
|
1170
|
-
L("v-seq", 1, "v-busy", 0),
|
|
1171
|
-
L("v-busy", 0, "v-thispoly", 0),
|
|
1172
|
-
L("v-seq", 0, "v-start", 0),
|
|
1173
|
-
L("v-start", 0, "v-groove", 0),
|
|
1174
|
-
L("v-seq", 0, "v-del", 0), // bang the delay: it fires after durMs
|
|
1175
|
-
// groove~ out: L direct, R via the mono-fold selector (stereo R vs. folded mono).
|
|
1176
|
-
L("v-groove", 0, "v-ampL", 0),
|
|
1177
|
-
L("v-groove", 1, "v-rsel", 1), // stereo R
|
|
1178
|
-
L("v-groove", 0, "v-rsel", 2), // mono fold
|
|
1179
|
-
L("v-rsel", 0, "v-ampR", 0),
|
|
1180
|
-
L("v-ampL", 0, "v-out1", 0),
|
|
1181
|
-
L("v-ampR", 0, "v-out2", 0),
|
|
1182
|
-
// free the voice when the note's duration elapses.
|
|
1183
|
-
L("v-del", 0, "v-free", 0),
|
|
1184
|
-
L("v-free", 1, "v-freebusy", 0),
|
|
1185
|
-
L("v-freebusy", 0, "v-thispoly", 0),
|
|
1186
|
-
L("v-free", 0, "v-stop", 0),
|
|
1187
|
-
L("v-stop", 0, "v-groove", 0),
|
|
1188
|
-
];
|
|
1189
|
-
|
|
1190
|
-
// One `set <buffer>, ` message per slot: [sel] outlet i switches groove~ to that
|
|
1191
|
-
// slot's buffer WITHOUT starting it (the seq's `0` does that). A trailing comma
|
|
1192
|
-
// would start it; there is none, deliberately.
|
|
1193
|
-
bufNames.forEach((buf, i) => {
|
|
1194
|
-
const setId = `v-set-${i}`;
|
|
1195
|
-
boxes.push(vmsg(setId, `set ${buf}`));
|
|
1196
|
-
lines.push(L("v-sel", i, setId, 0));
|
|
1197
|
-
lines.push(L(setId, 0, "v-groove", 0));
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
return {
|
|
1201
|
-
patcher: {
|
|
1202
|
-
fileversion: 1,
|
|
1203
|
-
appversion: { major: 8, minor: 0, revision: 0, architecture: "x64", modernui: 1 },
|
|
1204
|
-
rect: [100, 100, 320, 600],
|
|
1205
|
-
boxes,
|
|
1206
|
-
lines,
|
|
1207
|
-
},
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
883
|
+
|
|
884
|
+
|
|
1210
885
|
|
|
1211
886
|
/**
|
|
1212
|
-
* "
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
* the allocation and stealing.
|
|
1216
|
-
*
|
|
1217
|
-
* app -> voice_play <slot> <rate> <vel> <durMs> <chans> -> [route] -> [prepend note] -> [poly~]
|
|
1218
|
-
* buffer_load <slot> <path> -> [js] resolves -> [buffer~ <slot>]
|
|
1219
|
-
* app <- buffer_ready <slot> <sr> <ms> <chans> when that slot's read completed
|
|
1220
|
-
*
|
|
1221
|
-
* The load path is the samples chain's, deliberately: the bytes never cross the bridge
|
|
1222
|
-
* ([buffer~] reads the file, the wrapper resolves the path on its aux outlet), and the
|
|
1223
|
-
* reply reports what [info~] MEASURED, not what was asked for. Every slot is its own
|
|
1224
|
-
* named [buffer~]; the voice picks one per note by index and plays it at the rate the
|
|
1225
|
-
* app chose - so a dedicated sample plays at rate 1 and a repitched one at rate 2,
|
|
1226
|
-
* which is the app's decision, not the chain's.
|
|
1227
|
-
*
|
|
1228
|
-
* `slots: ["c", "e", "g"]` in the manifest is three buffers; the default is one
|
|
1229
|
-
* ("voice"). The buffer names are INSTANCE-SCOPED (`deviceBufName`): each copy of the
|
|
1230
|
-
* device owns its own, which is what lets a drum rack exist on two tracks at once.
|
|
1231
|
-
* `---` scopes per DEVICE, so the voice spells the same name the device does - see
|
|
1232
|
-
* deviceBufName.
|
|
1233
|
-
*
|
|
1234
|
-
* It SUMS into the signal path ([+~]): an instrument makes sound where there was none
|
|
1235
|
-
* at its input, so there is nothing to claim a stage over.
|
|
887
|
+
* "webaudio" - the primary audio source for modern m4l-jweb devices.
|
|
888
|
+
* It takes the L and R signal outlets from [jweb~] (outlets 0 and 1)
|
|
889
|
+
* and pipes them directly into the device's audio path.
|
|
1236
890
|
*/
|
|
1237
|
-
function
|
|
1238
|
-
const { boxes, lines,
|
|
1239
|
-
const slots = device?.slots ?? ["voice"];
|
|
1240
|
-
const bufName = (slot) => deviceBufName(device, slot);
|
|
1241
|
-
const voices = device?.voices ?? 8;
|
|
1242
|
-
const voiceFile = `${device?.name}-voice.maxpat`;
|
|
1243
|
-
|
|
1244
|
-
// The frozen voice patch (a keymap of every slot's buffer), and the [poly~] that
|
|
1245
|
-
// loads N copies of it. The voice spells the buffers exactly as the device does:
|
|
1246
|
-
// `---` is device-scoped, so no id travels through poly~'s arguments.
|
|
1247
|
-
ctx.extras.push({ name: voiceFile, data: instrumentVoicePatch(slots.map((s) => voiceBufName(device, s))) });
|
|
1248
|
-
// [poly~]'s name is the file WITHOUT its extension, per Max's abstraction lookup.
|
|
1249
|
-
boxes.push(
|
|
1250
|
-
box(`obj-instr-poly`, `poly~ ${voiceFile.replace(/\.maxpat$/, "")} ${voices}`, {
|
|
1251
|
-
numinlets: 1,
|
|
1252
|
-
numoutlets: 2,
|
|
1253
|
-
outlettype: ["signal", "signal"],
|
|
1254
|
-
}),
|
|
1255
|
-
);
|
|
891
|
+
function webaudioChain(ctx) {
|
|
892
|
+
const { boxes, lines, jwebId } = ctx;
|
|
1256
893
|
|
|
1257
|
-
|
|
1258
|
-
// the selector, leaving the bare args; `prepend note` is the word poly~ dispatches on
|
|
1259
|
-
// to pick a free voice. Claimed in series so ui_ready still reaches the wrapper.
|
|
1260
|
-
boxes.push(box("obj-instr-playroute", "route voice_play", { numoutlets: 2, outlettype: ["", ""] }));
|
|
1261
|
-
claimAppMessages(ctx, "obj-instr-playroute", 1);
|
|
1262
|
-
boxes.push(box("obj-instr-note", "prepend note"));
|
|
1263
|
-
lines.push(line("obj-instr-playroute", 0, "obj-instr-note", 0));
|
|
1264
|
-
lines.push(line("obj-instr-note", 0, "obj-instr-poly", 0));
|
|
1265
|
-
|
|
1266
|
-
// The load path, from the wrapper's aux outlet - identical in shape to samples: a
|
|
1267
|
-
// bare buffer name resolves against MAX'S SEARCH PATH, not the device folder, so the
|
|
1268
|
-
// wrapper resolves it and hands back `buffer_replace <slot> <abs path>`. One buffer
|
|
1269
|
-
// per slot, dispatched by slot name (route matches a whole word).
|
|
1270
|
-
boxes.push(box("obj-instr-replaceroute", "route buffer_replace", { numoutlets: 2, outlettype: ["", ""] }));
|
|
1271
|
-
lines.push(line(unmatchedId, 1, "obj-instr-replaceroute", 0));
|
|
1272
|
-
boxes.push(box("obj-instr-loadslot", `route ${slots.join(" ")}`, { numoutlets: slots.length + 1, outlettype: slots.map(() => "").concat("") }));
|
|
1273
|
-
lines.push(line("obj-instr-replaceroute", 0, "obj-instr-loadslot", 0));
|
|
1274
|
-
|
|
1275
|
-
slots.forEach((slot, i) => {
|
|
1276
|
-
const buf = `obj-instr-buf-${slot}`;
|
|
1277
|
-
const info = `obj-instr-info-${slot}`;
|
|
1278
|
-
boxes.push(box(buf, `buffer~ ${bufName(slot)}`, { numinlets: 1, numoutlets: 2, outlettype: ["float", "bang"] }));
|
|
1279
|
-
boxes.push(box(`obj-instr-replace-${slot}`, "prepend replace"));
|
|
1280
|
-
lines.push(line("obj-instr-loadslot", i, `obj-instr-replace-${slot}`, 0));
|
|
1281
|
-
lines.push(line(`obj-instr-replace-${slot}`, 0, buf, 0));
|
|
1282
|
-
|
|
1283
|
-
// Report what LOADED, from the read-completed outlet (1). info~ fires right-to-left,
|
|
1284
|
-
// so the sample rate (outlet 0) arrives last and drives [pack]'s hot inlet.
|
|
1285
|
-
boxes.push(
|
|
1286
|
-
box(info, `info~ ${bufName(slot)}`, {
|
|
1287
|
-
numinlets: 1,
|
|
1288
|
-
numoutlets: 10,
|
|
1289
|
-
outlettype: ["float", "list", "float", "float", "float", "float", "float", "", "int", ""],
|
|
1290
|
-
}),
|
|
1291
|
-
);
|
|
1292
|
-
boxes.push(box(`obj-instr-pack-${slot}`, "pack 0. 0. 0", { numinlets: 3, numoutlets: 1, outlettype: [""] }));
|
|
1293
|
-
boxes.push(box(`obj-instr-ready-${slot}`, `prepend buffer_ready ${slot}`));
|
|
1294
|
-
lines.push(line(buf, 1, info, 0));
|
|
1295
|
-
lines.push(line(info, 8, `obj-instr-pack-${slot}`, 2)); // channels
|
|
1296
|
-
lines.push(line(info, 6, `obj-instr-pack-${slot}`, 1)); // duration ms
|
|
1297
|
-
lines.push(line(info, 0, `obj-instr-pack-${slot}`, 0)); // sample rate, hot
|
|
1298
|
-
lines.push(line(`obj-instr-pack-${slot}`, 0, `obj-instr-ready-${slot}`, 0));
|
|
1299
|
-
lines.push(line(`obj-instr-ready-${slot}`, 0, jwebId, 0));
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
// Sum the voices into the signal path - an instrument originates sound.
|
|
1303
|
-
for (const [ch, id] of [
|
|
1304
|
-
[0, "obj-instr-mix-l"],
|
|
1305
|
-
[1, "obj-instr-mix-r"],
|
|
1306
|
-
]) {
|
|
894
|
+
for (const ch of [0, 1]) {
|
|
1307
895
|
const [srcId, srcOut] = ctx.audioIn(ch);
|
|
896
|
+
const id = `obj-webaudio-mix-${ch === 0 ? "l" : "r"}`;
|
|
897
|
+
|
|
1308
898
|
boxes.push(box(id, "+~", { numinlets: 2, numoutlets: 1, outlettype: ["signal"] }));
|
|
1309
|
-
lines.push(line(srcId, srcOut, id, 0));
|
|
1310
|
-
lines.push(line(
|
|
899
|
+
lines.push(line(srcId, srcOut, id, 0)); // The device input
|
|
900
|
+
lines.push(line(jwebId, ch, id, 1)); // jweb~ output
|
|
901
|
+
|
|
1311
902
|
ctx.setAudioOut(ch, id, 0);
|
|
1312
903
|
}
|
|
1313
904
|
}
|
|
1314
905
|
|
|
1315
906
|
export const CHAINS = {
|
|
907
|
+
webaudio: webaudioChain,
|
|
1316
908
|
midiin: midiInChain,
|
|
1317
|
-
samples: samplesChain,
|
|
1318
|
-
instrument: instrumentChain,
|
|
1319
909
|
midiout: midiOutChain,
|
|
1320
910
|
passthrough: passthroughChain,
|
|
1321
911
|
gain: gainChain,
|
package/templates/base.json
CHANGED
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
"box": {
|
|
70
70
|
"disablefind": 0,
|
|
71
71
|
"id": "obj-jweb",
|
|
72
|
-
"maxclass": "jweb",
|
|
72
|
+
"maxclass": "jweb~",
|
|
73
73
|
"numinlets": 1,
|
|
74
|
-
"numoutlets":
|
|
75
|
-
"outlettype": [""],
|
|
74
|
+
"numoutlets": 3,
|
|
75
|
+
"outlettype": ["signal", "signal", ""],
|
|
76
76
|
"patching_rect": [16.0, 104.0, 400.0, 169.0],
|
|
77
77
|
"presentation": 1,
|
|
78
78
|
"presentation_rect": [0.0, 0.0, 420.0, 169.0],
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
{ "patchline": { "destination": ["obj-midiout", 0], "source": ["obj-midiin", 0] } },
|
|
85
85
|
{ "patchline": { "destination": ["obj-js", 0], "source": ["obj-thisdevice", 0] } },
|
|
86
86
|
{ "patchline": { "destination": ["obj-jweb", 0], "source": ["obj-js", 0] } },
|
|
87
|
-
{ "patchline": { "destination": ["obj-js", 0], "source": ["obj-jweb",
|
|
87
|
+
{ "patchline": { "destination": ["obj-js", 0], "source": ["obj-jweb", 2] } }
|
|
88
88
|
],
|
|
89
89
|
"latency": 0,
|
|
90
90
|
"is_mpe": 0,
|