entangled-worlds-orbiters-shared 2.12.0 → 2.14.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 CHANGED
@@ -93,6 +93,12 @@ import * as EntangledWorlds from 'entangled-worlds-orbiters-shared';
93
93
  - `createStatusChannel` - Status-line seam ("Saving…", "Saved")
94
94
  - `isTrustedHostOrigin`, `isEmbedded`, `postToHost` - Host-frame transport
95
95
 
96
+ ### Note head programs (`entangled-worlds-orbiters-shared/faust`)
97
+ - `buildVoiceProgram`, `buildHeadPrograms` - Compose a rack's note head into FAUST source, keyed by shape
98
+ - `exportHead` - Compose, compile, shrink, size and stamp a head's programs; the compiler and the shrink tool are handed in
99
+ - `recipesFingerprint`, `readRecipes`, `readHeadShape`, `programsNamespace` - The recipes file a build publishes, and the checks a release step makes on it
100
+ - `largestFunction`, `shrinkWasm` - The size check and the shrink step
101
+
96
102
  ### Uploads (`entangled-worlds-orbiters-shared/uploads`)
97
103
  - `uploadViaSignedUrl` - The API's signed-URL upload: `/spaces/upload-init` → PUT → `/spaces/upload-complete`, over the app's own authenticated JSON caller
98
104
  - `putToSignedUrl` - The storage PUT alone, with progress and abort
@@ -4,11 +4,87 @@ export declare const Ease: {
4
4
  linear: EaseFn;
5
5
  smooth: EaseFn;
6
6
  };
7
- /** A single curve keyframe: a time key `t` (seconds or beats) and its value. */
7
+ /**
8
+ * A single curve keyframe: a time key `t` (seconds or beats) and its value. `curve` bends the segment
9
+ * to the next keyframe (see {@link bend}); absent or 0 falls back to the track's `ease`.
10
+ */
8
11
  export interface Keyframe {
9
12
  t: number;
10
13
  value: number;
14
+ curve?: number;
15
+ }
16
+ /**
17
+ * How a curve moves between keyframes: `line` ramps (bent by each keyframe's `curve`), `step` holds
18
+ * each value until the next keyframe.
19
+ */
20
+ export type CurveShape = 'line' | 'step';
21
+ /** The range of a keyframe's `curve`: −2 … 2, 0 linear. */
22
+ export declare const CURVE_BEND_LIMIT = 2;
23
+ /**
24
+ * The bend of one segment: maps the fraction `u` in [0, 1] through `curve` c. 0 is linear; c > 0 starts
25
+ * slow and ends fast (exponential), c < 0 starts fast (logarithmic). The automation editor and every
26
+ * player read the same function, so what is drawn is what plays.
27
+ */
28
+ export declare function bend(u: number, curve?: number): number;
29
+ /**
30
+ * The `curve` that puts a segment's midpoint at `mid`, a share of the way from its first value to its
31
+ * last (0 … 1, exclusive): the inverse of {@link bend} at u = ½, for a bend handle dragged up or down.
32
+ */
33
+ export declare function curveForMidpoint(mid: number): number;
34
+ /** What {@link sampleKeyframes} needs besides the keyframes. */
35
+ export interface SampleOptions {
36
+ shape?: CurveShape;
37
+ /** For a segment whose keyframe has no `curve` (default linear). */
38
+ ease?: EaseFn;
39
+ }
40
+ /**
41
+ * The value of sorted keyframes at time key `key`: held before the first and after the last, ramped
42
+ * (or stepped) between. NaN when there are none: an empty lane writes nothing.
43
+ */
44
+ export declare function sampleKeyframes(keyframes: readonly Keyframe[], key: number, { shape, ease }?: SampleOptions): number;
45
+ /**
46
+ * A loop over a curve's time key: the curve plays from 0 up to `start + length`, then repeats
47
+ * `start` … `start + length` (a clip's loop). `start` 0 is a curve that loops on its own length.
48
+ */
49
+ export interface CurveLoop {
50
+ start: number;
51
+ length: number;
52
+ }
53
+ /** The curve's own time key for a running key: before the loop's end as is, then wrapped into the loop. */
54
+ export declare function loopKey(key: number, loop: CurveLoop | null | undefined): number;
55
+ /** One of the shapes {@link insertShape} draws: points as [u, value, curve?] over one cycle, u in [0, 1). */
56
+ export interface CurveShapeDef {
57
+ id: string;
58
+ label: string;
59
+ points: ReadonlyArray<readonly [number, number, number?]>;
60
+ /** A closing point at the range's end, for shapes that come back to where they started. */
61
+ end?: number;
11
62
  }
63
+ /** The ten shapes of the automation editor's Insert shape menu. */
64
+ export declare const CURVE_SHAPES: readonly CurveShapeDef[];
65
+ /** The keyframes with every one inside [from, to] replaced by `insert` (sorted). */
66
+ export declare function replaceRange(keyframes: readonly Keyframe[], from: number, to: number, insert: readonly Keyframe[]): Keyframe[];
67
+ /**
68
+ * Draws a shape over [from, to], `cycles` times, replacing only the keyframes inside the range. Null
69
+ * when the shape is unknown or the range is too short to draw (under 1/16 of a key unit).
70
+ */
71
+ export declare function insertShape(keyframes: readonly Keyframe[], shapeId: string, from: number, to: number, cycles?: number): Keyframe[] | null;
72
+ /** Freehand draw: one keyframe at the start of the cell [cellStart, cellStart + cellLength), replacing the cell. */
73
+ export declare function paintCell(keyframes: readonly Keyframe[], cellStart: number, cellLength: number, value: number): Keyframe[];
74
+ /**
75
+ * Recording: the keyframes after the time key moved from `prevKey` to `key` while `value` was live.
76
+ * What was recorded over in that span is replaced (a wrap from a loop end back to its start clears both
77
+ * ends of the loop). A new keyframe is written only once `spacing` has passed since `lastWritten` (null: write now),
78
+ * so a recording is one point per grid step, not one per frame.
79
+ */
80
+ export declare function recordSpan(keyframes: readonly Keyframe[], prevKey: number, key: number, value: number, { spacing, lastWritten, loop }: {
81
+ spacing: number;
82
+ lastWritten: number | null;
83
+ loop?: CurveLoop | null;
84
+ }): {
85
+ keyframes: Keyframe[];
86
+ written: number | null;
87
+ };
12
88
  export type TimeMode = 'absolute-seconds' | 'beat-locked';
13
89
  /**
14
90
  * Structural shape of a musical projection — typed here so `./clock/curves` never imports
@@ -22,6 +98,10 @@ export interface CurveTrackSpec {
22
98
  timeMode?: TimeMode;
23
99
  projection?: ProjectionLike | null;
24
100
  ease?: EaseFn;
101
+ /** Line (default) or step. */
102
+ shape?: CurveShape;
103
+ /** Loops the curve's time key (a clip loop, or its own length). Absent: plays once, then holds. */
104
+ loop?: CurveLoop | null;
25
105
  outputIntent?: unknown;
26
106
  }
27
107
  export declare class CurveTrack {
@@ -29,11 +109,16 @@ export declare class CurveTrack {
29
109
  timeMode: TimeMode;
30
110
  projection: ProjectionLike | null;
31
111
  ease: EaseFn;
112
+ shape: CurveShape;
113
+ loop: CurveLoop | null;
32
114
  outputIntent: unknown;
33
115
  constructor(spec: CurveTrackSpec);
34
- /** The track's own time key for a given clock position. */
116
+ /** The track's own time key for a given clock position (looped when the track loops). */
35
117
  _key(positionSec: number): number;
118
+ /** The value at a clock position; NaN for a track with no keyframes. */
36
119
  sample(positionSec: number): number;
120
+ /** The value at the track's own time key (beats for a beat-locked track), with no loop applied. */
121
+ sampleKey(key: number): number;
37
122
  }
38
123
  /**
39
124
  * Structural shape of the transport a GestureRecorder/Orchestrator subscribes to — typed here so
@@ -13,40 +13,164 @@ export const Ease = {
13
13
  function lerp(a, b, t) {
14
14
  return a + (b - a) * t;
15
15
  }
16
+ /** The range of a keyframe's `curve`: −2 … 2, 0 linear. */
17
+ export const CURVE_BEND_LIMIT = 2;
18
+ /**
19
+ * The bend of one segment: maps the fraction `u` in [0, 1] through `curve` c. 0 is linear; c > 0 starts
20
+ * slow and ends fast (exponential), c < 0 starts fast (logarithmic). The automation editor and every
21
+ * player read the same function, so what is drawn is what plays.
22
+ */
23
+ export function bend(u, curve = 0) {
24
+ if (!curve)
25
+ return u;
26
+ return curve > 0 ? Math.pow(u, 1 + curve * 3) : 1 - Math.pow(1 - u, 1 - curve * 3);
27
+ }
28
+ /**
29
+ * The `curve` that puts a segment's midpoint at `mid`, a share of the way from its first value to its
30
+ * last (0 … 1, exclusive): the inverse of {@link bend} at u = ½, for a bend handle dragged up or down.
31
+ */
32
+ export function curveForMidpoint(mid) {
33
+ const m = Math.max(0.03, Math.min(0.97, mid));
34
+ const c = m <= 0.5 ? (Math.log(m) / Math.log(0.5) - 1) / 3 : -(Math.log(1 - m) / Math.log(0.5) - 1) / 3;
35
+ return Math.max(-CURVE_BEND_LIMIT, Math.min(CURVE_BEND_LIMIT, c));
36
+ }
37
+ /**
38
+ * The value of sorted keyframes at time key `key`: held before the first and after the last, ramped
39
+ * (or stepped) between. NaN when there are none: an empty lane writes nothing.
40
+ */
41
+ export function sampleKeyframes(keyframes, key, { shape = 'line', ease = Ease.linear } = {}) {
42
+ const kf = keyframes;
43
+ if (!kf.length)
44
+ return NaN;
45
+ if (key <= kf[0].t)
46
+ return kf[0].value;
47
+ const last = kf[kf.length - 1];
48
+ if (key >= last.t)
49
+ return last.value;
50
+ // Binary search: the last keyframe at or before key.
51
+ let lo = 0;
52
+ let hi = kf.length - 1;
53
+ while (hi - lo > 1) {
54
+ const mid = (lo + hi) >> 1;
55
+ if (kf[mid].t <= key)
56
+ lo = mid;
57
+ else
58
+ hi = mid;
59
+ }
60
+ const a = kf[lo];
61
+ const b = kf[lo + 1];
62
+ if (shape === 'step')
63
+ return a.value;
64
+ const frac = b.t > a.t ? (key - a.t) / (b.t - a.t) : 1;
65
+ return lerp(a.value, b.value, a.curve ? bend(frac, a.curve) : ease(frac));
66
+ }
67
+ /** The curve's own time key for a running key: before the loop's end as is, then wrapped into the loop. */
68
+ export function loopKey(key, loop) {
69
+ if (!loop || !(loop.length > 0))
70
+ return key;
71
+ const end = loop.start + loop.length;
72
+ if (key < end)
73
+ return key;
74
+ return loop.start + ((key - end) % loop.length);
75
+ }
76
+ /** The ten shapes of the automation editor's Insert shape menu. */
77
+ export const CURVE_SHAPES = [
78
+ { id: 'sine', label: 'Sine', points: [[0, 0.5, -0.2], [0.25, 1, 0.2], [0.5, 0.5, -0.2], [0.75, 0, 0.2]], end: 0.5 },
79
+ { id: 'triangle', label: 'Triangle', points: [[0, 0], [0.5, 1]], end: 0 },
80
+ { id: 'sawDown', label: 'Saw down', points: [[0, 1], [0.999, 0]] },
81
+ { id: 'sawUp', label: 'Saw up', points: [[0, 0], [0.999, 1]] },
82
+ { id: 'square', label: 'Square', points: [[0, 1], [0.5 - 1e-3, 1], [0.5, 0], [0.999, 0]] },
83
+ { id: 'curveUp', label: 'Curve up', points: [[0, 0, 1.2], [0.999, 1]] },
84
+ { id: 'curveDown', label: 'Curve down', points: [[0, 1, -1.2], [0.999, 0]] },
85
+ { id: 'sCurveUp', label: 'S-curve up', points: [[0, 0, 0.8], [0.5, 0.5, -0.8], [0.999, 1]] },
86
+ { id: 'sCurveDown', label: 'S-curve down', points: [[0, 1, 0.8], [0.5, 0.5, -0.8], [0.999, 0]] },
87
+ { id: 'attackDecay', label: 'Attack / decay', points: [[0, 0, -0.6], [0.15, 1, -1], [0.999, 0]] },
88
+ ];
89
+ const byKey = (a, b) => a.t - b.t;
90
+ /** The keyframes with every one inside [from, to] replaced by `insert` (sorted). */
91
+ export function replaceRange(keyframes, from, to, insert) {
92
+ const eps = 1e-6;
93
+ return keyframes.filter((k) => k.t < from - eps || k.t > to + eps).concat(insert).sort(byKey);
94
+ }
95
+ /**
96
+ * Draws a shape over [from, to], `cycles` times, replacing only the keyframes inside the range. Null
97
+ * when the shape is unknown or the range is too short to draw (under 1/16 of a key unit).
98
+ */
99
+ export function insertShape(keyframes, shapeId, from, to, cycles = 1) {
100
+ const def = CURVE_SHAPES.find((s) => s.id === shapeId);
101
+ if (!def || to - from < 0.0625)
102
+ return null;
103
+ const n = Math.max(1, Math.floor(cycles));
104
+ const len = (to - from) / n;
105
+ const eps = Math.min(1 / 64, len / 8);
106
+ const made = [];
107
+ for (let k = 0; k < n; k++) {
108
+ for (const [u, value, curve] of def.points) {
109
+ const t = u >= 0.999 ? from + (k + 1) * len - eps : from + (k + u) * len;
110
+ made.push(curve ? { t, value, curve } : { t, value });
111
+ }
112
+ }
113
+ if (def.end != null)
114
+ made.push({ t: to, value: def.end });
115
+ return replaceRange(keyframes, from, to, made);
116
+ }
117
+ /** Freehand draw: one keyframe at the start of the cell [cellStart, cellStart + cellLength), replacing the cell. */
118
+ export function paintCell(keyframes, cellStart, cellLength, value) {
119
+ const eps = 1e-6;
120
+ return keyframes
121
+ .filter((k) => k.t < cellStart - eps || k.t >= cellStart + cellLength - eps)
122
+ .concat({ t: cellStart, value })
123
+ .sort(byKey);
124
+ }
125
+ /**
126
+ * Recording: the keyframes after the time key moved from `prevKey` to `key` while `value` was live.
127
+ * What was recorded over in that span is replaced (a wrap from a loop end back to its start clears both
128
+ * ends of the loop). A new keyframe is written only once `spacing` has passed since `lastWritten` (null: write now),
129
+ * so a recording is one point per grid step, not one per frame.
130
+ */
131
+ export function recordSpan(keyframes, prevKey, key, value, { spacing, lastWritten, loop }) {
132
+ const wrapped = key < prevKey;
133
+ const start = loop ? loop.start : 0;
134
+ const end = loop ? loop.start + loop.length : Infinity;
135
+ const over = (t) => (wrapped ? (t > prevKey && t < end) || (t >= start && t <= key) : t > prevKey && t <= key);
136
+ const kept = keyframes.filter((k) => !over(k.t));
137
+ const last = lastWritten == null || wrapped ? -Infinity : lastWritten;
138
+ if (key - last < spacing)
139
+ return { keyframes: kept, written: lastWritten };
140
+ return { keyframes: kept.concat({ t: key, value }).sort(byKey), written: key };
141
+ }
16
142
  export class CurveTrack {
17
143
  keyframes;
18
144
  timeMode;
19
145
  projection;
20
146
  ease;
147
+ shape;
148
+ loop;
21
149
  outputIntent;
22
150
  constructor(spec) {
23
- this.keyframes = spec.keyframes.slice().sort((a, b) => a.t - b.t);
151
+ this.keyframes = spec.keyframes.slice().sort(byKey);
24
152
  this.timeMode = spec.timeMode ?? 'absolute-seconds';
25
153
  this.projection = spec.projection ?? null;
26
154
  this.ease = spec.ease ?? Ease.linear;
155
+ this.shape = spec.shape ?? 'line';
156
+ this.loop = spec.loop ?? null;
27
157
  this.outputIntent = spec.outputIntent ?? null;
28
158
  if (this.timeMode === 'beat-locked' && !this.projection) {
29
159
  throw new Error('beat-locked CurveTrack needs a MusicalProjection');
30
160
  }
31
161
  }
32
- /** The track's own time key for a given clock position. */
162
+ /** The track's own time key for a given clock position (looped when the track loops). */
33
163
  _key(positionSec) {
34
- return this.timeMode === 'beat-locked' ? this.projection.secondsToBeats(positionSec) : positionSec;
164
+ const key = this.timeMode === 'beat-locked' ? this.projection.secondsToBeats(positionSec) : positionSec;
165
+ return loopKey(key, this.loop);
35
166
  }
167
+ /** The value at a clock position; NaN for a track with no keyframes. */
36
168
  sample(positionSec) {
37
- const k = this._key(positionSec);
38
- const kf = this.keyframes;
39
- if (k <= kf[0].t)
40
- return kf[0].value;
41
- if (k >= kf[kf.length - 1].t)
42
- return kf[kf.length - 1].value;
43
- let i = 0;
44
- while (i < kf.length - 1 && kf[i + 1].t <= k)
45
- i++;
46
- const a = kf[i];
47
- const b = kf[i + 1];
48
- const frac = (k - a.t) / (b.t - a.t);
49
- return lerp(a.value, b.value, this.ease(frac));
169
+ return this.sampleKey(this._key(positionSec));
170
+ }
171
+ /** The value at the track's own time key (beats for a beat-locked track), with no loop applied. */
172
+ sampleKey(key) {
173
+ return sampleKeyframes(this.keyframes, key, { shape: this.shape, ease: this.ease });
50
174
  }
51
175
  }
52
176
  /**
@@ -0,0 +1,109 @@
1
+ /**
2
+ * A head made ready for play mode: its programs composed, compiled, shrunk, sized and stamped.
3
+ *
4
+ * Play mode downloads finished programs: no compiler in the page, no first-build wait on a phone.
5
+ * This is the step that makes them, once, when an Orbiter is released. It runs where a compiler and
6
+ * the shrink step exist (a server, a build, a test), never in a listener's browser, so both arrive
7
+ * as tools rather than imports: this file stays pure.
8
+ *
9
+ * For one head (`voiceProgram.ts`, `buildHeadPrograms`):
10
+ *
11
+ * 1. **Compose** its programs: the note modules as one, the instrument as another; the instrument
12
+ * alone when nothing sits in front of it.
13
+ * 2. **Compile** each with the app's own options.
14
+ * 3. **Shrink** each (the same step the module build uses: the same program, a quarter smaller).
15
+ * 4. **Size** each: past the heavy line its largest function is one Safari may never optimise. The
16
+ * export still goes through; the author is told.
17
+ * 5. **Stamp** the export with what it was built from and the runtime it expects.
18
+ *
19
+ * A released Orbiter keeps its sound: an export stays valid when a module's source changes later
20
+ * (the stamp says what it was built from). Play mode refuses it only when the runtime it expects is
21
+ * no longer the app's, and then plays as it would have without one.
22
+ */
23
+ import { type VoiceSlot } from './voiceProgram.js';
24
+ /** One item of Faust's UI tree: a group (with `items`) or a control. */
25
+ export interface FaustUiItem {
26
+ type: string;
27
+ label: string;
28
+ address?: string;
29
+ init?: number;
30
+ min?: number;
31
+ max?: number;
32
+ step?: number;
33
+ items?: FaustUiItem[];
34
+ [field: string]: unknown;
35
+ }
36
+ /** Faust's metadata for a compiled DSP (its JSON): the UI tree, inputs, outputs, declares. */
37
+ export interface FaustDspMeta {
38
+ name: string;
39
+ inputs: number;
40
+ outputs: number;
41
+ compile_options: string;
42
+ ui: FaustUiItem[];
43
+ meta?: Array<Record<string, string>>;
44
+ [field: string]: unknown;
45
+ }
46
+ /** A compiled program as it is stored and downloaded: its wasm and the description of its controls. */
47
+ export interface CompiledProgram {
48
+ /** Faust's sha key; the `registerProcessor` name. */
49
+ processorName: string;
50
+ dspName: string;
51
+ dspMeta: FaustDspMeta;
52
+ /** Fully-qualified control addresses, in Faust's order. */
53
+ paramAddresses: string[];
54
+ wasmBase64: string;
55
+ /** 4 for a single-precision build, 8 for -double. */
56
+ sampleSize: 4 | 8;
57
+ }
58
+ /** Every control address in the DSP's UI tree, in Faust's own order. */
59
+ export declare function collectParamAddresses(ui: FaustUiItem[] | null | undefined): string[];
60
+ /** What an export was built from, and what it needs to play. */
61
+ export interface HeadExportStamp {
62
+ /** The runtime contract the programs were built for: the app's, as its recipes file names it. */
63
+ runtime: number;
64
+ /** The FAUST compiler's version, and its options. */
65
+ faust: string;
66
+ options: string;
67
+ /** The shrink step's version. */
68
+ shrink: string;
69
+ /** The fingerprint of the recipes file: every `.dsp` source and library the compiler could read. */
70
+ sources: string;
71
+ }
72
+ /** The compiler and the shrink step, handed in by whoever runs the export. */
73
+ export interface HeadExportTools {
74
+ /** Compile `source` under `name` with the sources and libraries mounted: FAUST's factory. */
75
+ compile(name: string, source: string): Promise<{
76
+ code: Uint8Array;
77
+ json: string;
78
+ shaKey: string;
79
+ }>;
80
+ /** The same program, smaller. */
81
+ shrink(wasm: Uint8Array): Uint8Array;
82
+ /**
83
+ * A program already filed under this key, if there is one: it is taken as it is, never compiled
84
+ * again (an instrument's program is the same file for every Orbiter that plays it).
85
+ */
86
+ find?(key: string): Promise<CompiledProgram | null>;
87
+ stamp: HeadExportStamp;
88
+ }
89
+ /** One finished program of a head. */
90
+ export interface ExportedProgram {
91
+ /** What it is filed under: its shape. */
92
+ key: string;
93
+ artifact: CompiledProgram;
94
+ /** Its largest function, in bytes, and whether that is past the heavy line. */
95
+ largestFunction: number;
96
+ heavy: boolean;
97
+ }
98
+ export interface HeadExport {
99
+ /** Null when nothing sits in front of the instrument. */
100
+ notes: ExportedProgram | null;
101
+ instrument: ExportedProgram;
102
+ stamp: HeadExportStamp;
103
+ }
104
+ /**
105
+ * `slots`: the head's note modules in rack order, then its instrument, each with what it does with
106
+ * notes. Throws when a program does not compile: the caller saves the release without programs, and
107
+ * play mode plays as it does without them.
108
+ */
109
+ export declare function exportHead(slots: readonly VoiceSlot[], tools: HeadExportTools): Promise<HeadExport>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * A head made ready for play mode: its programs composed, compiled, shrunk, sized and stamped.
3
+ *
4
+ * Play mode downloads finished programs: no compiler in the page, no first-build wait on a phone.
5
+ * This is the step that makes them, once, when an Orbiter is released. It runs where a compiler and
6
+ * the shrink step exist (a server, a build, a test), never in a listener's browser, so both arrive
7
+ * as tools rather than imports: this file stays pure.
8
+ *
9
+ * For one head (`voiceProgram.ts`, `buildHeadPrograms`):
10
+ *
11
+ * 1. **Compose** its programs: the note modules as one, the instrument as another; the instrument
12
+ * alone when nothing sits in front of it.
13
+ * 2. **Compile** each with the app's own options.
14
+ * 3. **Shrink** each (the same step the module build uses: the same program, a quarter smaller).
15
+ * 4. **Size** each: past the heavy line its largest function is one Safari may never optimise. The
16
+ * export still goes through; the author is told.
17
+ * 5. **Stamp** the export with what it was built from and the runtime it expects.
18
+ *
19
+ * A released Orbiter keeps its sound: an export stays valid when a module's source changes later
20
+ * (the stamp says what it was built from). Play mode refuses it only when the runtime it expects is
21
+ * no longer the app's, and then plays as it would have without one.
22
+ */
23
+ import { buildHeadPrograms, HEAD_PROGRAM_NAME } from './voiceProgram.js';
24
+ import { HEAVY_FUNCTION_BYTES, largestFunction } from './wasmSize.js';
25
+ /** Every control address in the DSP's UI tree, in Faust's own order. */
26
+ export function collectParamAddresses(ui) {
27
+ const out = [];
28
+ const walk = (items) => items.forEach((item) => {
29
+ if (item.items)
30
+ walk(item.items);
31
+ else if (item.address)
32
+ out.push(item.address);
33
+ });
34
+ walk(ui ?? []);
35
+ return out;
36
+ }
37
+ function base64(bytes) {
38
+ let binary = '';
39
+ // In pieces: one call with every byte as an argument overflows the stack on a large program.
40
+ for (let at = 0; at < bytes.length; at += 0x8000)
41
+ binary += String.fromCharCode(...bytes.subarray(at, at + 0x8000));
42
+ return btoa(binary);
43
+ }
44
+ function bytes(text) {
45
+ const binary = atob(text);
46
+ const out = new Uint8Array(binary.length);
47
+ for (let i = 0; i < binary.length; i += 1)
48
+ out[i] = binary.charCodeAt(i);
49
+ return out;
50
+ }
51
+ async function exportProgram(key, source, tools) {
52
+ const filed = await tools.find?.(key);
53
+ if (filed) {
54
+ const size = largestFunction(bytes(filed.wasmBase64));
55
+ return { key, artifact: filed, largestFunction: size, heavy: size > HEAVY_FUNCTION_BYTES };
56
+ }
57
+ const factory = await tools.compile(HEAD_PROGRAM_NAME, source);
58
+ const wasm = tools.shrink(factory.code);
59
+ const dspMeta = JSON.parse(factory.json);
60
+ const size = largestFunction(wasm);
61
+ return {
62
+ key,
63
+ artifact: {
64
+ processorName: factory.shaKey,
65
+ dspName: HEAD_PROGRAM_NAME,
66
+ dspMeta,
67
+ paramAddresses: collectParamAddresses(dspMeta.ui),
68
+ wasmBase64: base64(wasm),
69
+ sampleSize: dspMeta.compile_options.includes('-double') ? 8 : 4,
70
+ },
71
+ largestFunction: size,
72
+ heavy: size > HEAVY_FUNCTION_BYTES,
73
+ };
74
+ }
75
+ /**
76
+ * `slots`: the head's note modules in rack order, then its instrument, each with what it does with
77
+ * notes. Throws when a program does not compile: the caller saves the release without programs, and
78
+ * play mode plays as it does without them.
79
+ */
80
+ export async function exportHead(slots, tools) {
81
+ const programs = buildHeadPrograms(slots);
82
+ // One after the other: one libfaust compiles one program at a time.
83
+ const notes = programs.notes ? await exportProgram(programs.notes.key, programs.notes.source, tools) : null;
84
+ const instrument = await exportProgram(programs.instrument.key, programs.instrument.source, tools);
85
+ if (notes && notes.artifact.dspMeta.outputs !== instrument.artifact.dspMeta.inputs) {
86
+ throw new Error(`[headExport] the note modules put out ${notes.artifact.dspMeta.outputs} signals; the instrument takes ${instrument.artifact.dspMeta.inputs}`);
87
+ }
88
+ return { notes, instrument, stamp: tools.stamp };
89
+ }
@@ -0,0 +1,4 @@
1
+ export * from './voiceProgram.js';
2
+ export * from './wasmSize.js';
3
+ export * from './headExport.js';
4
+ export * from './recipes.js';
@@ -0,0 +1,7 @@
1
+ // Composing a rack's note head into FAUST programs, and making them ready for play mode: shared by
2
+ // the app that plays them and the server step that compiles them at release, so the source, its key
3
+ // and its file name are the same on both sides. Pure: the compiler and the shrink tool are handed in.
4
+ export * from './voiceProgram.js';
5
+ export * from './wasmSize.js';
6
+ export * from './headExport.js';
7
+ export * from './recipes.js';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The recipes file: everything one build of the app compiles its programs from, in one file named by
3
+ * a fingerprint of its content.
4
+ *
5
+ * The app publishes it beside its compiler; the release step fetches it by the fingerprint the author's
6
+ * page sends, checks the content against the name, and compiles from exactly that. The sources stay
7
+ * where they are authored: nothing else has to hold a copy of them in step.
8
+ *
9
+ * The fingerprint covers all of it (the sources, the libraries, the compiler's version and options,
10
+ * the runtime contract), so two builds that would compile a different program never share a name.
11
+ */
12
+ import { type VoiceSlot } from './voiceProgram.js';
13
+ export interface Recipes {
14
+ /** `.dsp` file name → FAUST source. */
15
+ sources: Readonly<Record<string, string>>;
16
+ /** Library file name → FAUST source, mounted beside the standard ones. */
17
+ libraries: Readonly<Record<string, string>>;
18
+ /** The compiler: the version of the package that carries libfaust. */
19
+ faust: string;
20
+ /** The compiler's options. */
21
+ options: string;
22
+ /** The runtime contract a program compiled from these plays under. */
23
+ runtime: number;
24
+ }
25
+ export declare const isRecipesFingerprint: (value: unknown) => value is string;
26
+ /** The file a fingerprint names, as the app publishes it. */
27
+ export declare const recipesFileName: (fingerprint: string) => string;
28
+ /** A hash of everything a compile reads and is told (SHA-256, its first 16 hex digits). */
29
+ export declare function recipesFingerprint(recipes: Recipes): Promise<string>;
30
+ /** A downloaded recipes file, if it reads as one; anything else is null. */
31
+ export declare function readRecipes(value: unknown): Recipes | null;
32
+ /**
33
+ * How this package composes a program from a shape. Raise it when the same shape would compose to
34
+ * different text: a test keeps a fingerprint of the composed text beside it and fails until then.
35
+ */
36
+ export declare const COMPOSER_VERSION = 1;
37
+ /**
38
+ * Where a build's compiled programs are filed: the recipes, the shrink step and the composer
39
+ * together. A file under it is never replaced, so a release keeps the programs it was given.
40
+ */
41
+ export declare function programsNamespace(fingerprint: string, shrink: string): Promise<string>;
42
+ /** The most World dimensions an Orbiter has: a head each. */
43
+ export declare const MAX_HEADS = 3;
44
+ /** The most note modules in front of an instrument: a generator and the MIDI effects. */
45
+ export declare const MAX_HEAD_NOTE_MODULES = 12;
46
+ /**
47
+ * A head's shape as a client sent it, checked before anything is composed from it: the note modules
48
+ * in rack order, then the instrument. Every file must be one of the recipes', every count a whole
49
+ * number inside the rack's limits. Returns the shape with nothing else on it; throws with the reason.
50
+ */
51
+ export declare function readHeadShape(value: unknown, sources: Readonly<Record<string, string>>): VoiceSlot[];
52
+ /** One head's programs on a release: their keys, and whether the author was told it is heavy. */
53
+ export interface ReleasedHead {
54
+ /** Null when nothing sits in front of the instrument. */
55
+ notes: string | null;
56
+ instrument: string;
57
+ heavy?: boolean;
58
+ }
59
+ /** What a release carries of its programs: written by the server at release, never by a client. */
60
+ export interface ReleasedPrograms {
61
+ /** The runtime contract the programs were built for. */
62
+ runtime: number;
63
+ /** Where they are filed (`programsNamespace`): what they were built from and with. */
64
+ sources: string;
65
+ /** By World dimension id. */
66
+ heads: Record<string, ReleasedHead>;
67
+ }
68
+ /** A release's programs, if what it carries reads as them; anything else is no programs. */
69
+ export declare function readReleasedPrograms(value: unknown): ReleasedPrograms | null;