entangled-worlds-orbiters-shared 2.13.0 → 2.15.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
@@ -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 = 2;
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;
@@ -0,0 +1,102 @@
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 { MAX_NOTE_LANES } from './voiceProgram.js';
13
+ const FINGERPRINT = /^[0-9a-f]{16}$/;
14
+ export const isRecipesFingerprint = (value) => typeof value === 'string' && FINGERPRINT.test(value);
15
+ /** The file a fingerprint names, as the app publishes it. */
16
+ export const recipesFileName = (fingerprint) => `sources-${fingerprint}.json`;
17
+ async function shortHash(parts) {
18
+ const text = new TextEncoder().encode(JSON.stringify(parts));
19
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', text));
20
+ return Array.from(digest.subarray(0, 8), (byte) => byte.toString(16).padStart(2, '0')).join('');
21
+ }
22
+ /** A hash of everything a compile reads and is told (SHA-256, its first 16 hex digits). */
23
+ export function recipesFingerprint(recipes) {
24
+ const sorted = (files) => Object.keys(files).sort().map((file) => [file, files[file]]);
25
+ return shortHash([sorted(recipes.sources), sorted(recipes.libraries), recipes.faust, recipes.options, recipes.runtime]);
26
+ }
27
+ const isTextMap = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
28
+ && Object.values(value).every((text) => typeof text === 'string');
29
+ /** A downloaded recipes file, if it reads as one; anything else is null. */
30
+ export function readRecipes(value) {
31
+ const recipes = value;
32
+ if (!recipes || typeof recipes !== 'object')
33
+ return null;
34
+ if (!isTextMap(recipes.sources) || !isTextMap(recipes.libraries) || !Object.keys(recipes.sources).length)
35
+ return null;
36
+ if (typeof recipes.faust !== 'string' || typeof recipes.options !== 'string' || !Number.isInteger(recipes.runtime))
37
+ return null;
38
+ return { sources: recipes.sources, libraries: recipes.libraries, faust: recipes.faust, options: recipes.options, runtime: recipes.runtime };
39
+ }
40
+ /**
41
+ * How this package composes a program from a shape. Raise it when the same shape would compose to
42
+ * different text: a test keeps a fingerprint of the composed text beside it and fails until then.
43
+ */
44
+ export const COMPOSER_VERSION = 2;
45
+ /**
46
+ * Where a build's compiled programs are filed: the recipes, the shrink step and the composer
47
+ * together. A file under it is never replaced, so a release keeps the programs it was given.
48
+ */
49
+ export function programsNamespace(fingerprint, shrink) {
50
+ return shortHash([fingerprint, shrink, COMPOSER_VERSION]);
51
+ }
52
+ /** The most World dimensions an Orbiter has: a head each. */
53
+ export const MAX_HEADS = 3;
54
+ /** The most note modules in front of an instrument: a generator and the MIDI effects. */
55
+ export const MAX_HEAD_NOTE_MODULES = 12;
56
+ /** The largest program constant an instrument takes. */
57
+ const MAX_CONSTANT = 64;
58
+ const whole = (value, most) => Number.isInteger(value) && value >= 1 && value <= most;
59
+ /**
60
+ * A head's shape as a client sent it, checked before anything is composed from it: the note modules
61
+ * in rack order, then the instrument. Every file must be one of the recipes', every count a whole
62
+ * number inside the rack's limits. Returns the shape with nothing else on it; throws with the reason.
63
+ */
64
+ export function readHeadShape(value, sources) {
65
+ if (!Array.isArray(value) || value.length < 1 || value.length > MAX_HEAD_NOTE_MODULES + 1)
66
+ throw new Error('a head is 1 to 13 modules');
67
+ return value.map((slot, index) => {
68
+ const file = slot?.dspFileName;
69
+ const notes = slot?.notes;
70
+ if (typeof file !== 'string' || !Object.hasOwn(sources, file))
71
+ throw new Error(`module ${index}: no such source`);
72
+ const last = index === value.length - 1;
73
+ if (last) {
74
+ if (notes?.kind !== 'instrument' || !whole(notes.polyphony, MAX_NOTE_LANES))
75
+ throw new Error(`module ${index}: a head ends with an instrument of 1 to ${MAX_NOTE_LANES} voices`);
76
+ const constants = notes.constants ?? {};
77
+ if (typeof constants !== 'object' || Array.isArray(constants))
78
+ throw new Error(`module ${index}: constants`);
79
+ const entries = Object.entries(constants);
80
+ if (entries.length > 8 || entries.some(([name, n]) => !/^[A-Z][A-Z0-9_]{0,23}$/.test(name) || !whole(n, MAX_CONSTANT)))
81
+ throw new Error(`module ${index}: constants`);
82
+ return { dspFileName: file, notes: { kind: 'instrument', polyphony: notes.polyphony, ...(entries.length ? { constants: Object.fromEntries(entries) } : {}) } };
83
+ }
84
+ if ((notes?.kind !== 'generator' && notes?.kind !== 'midi') || !whole(notes.lanesPerLane, MAX_NOTE_LANES))
85
+ throw new Error(`module ${index}: a note module with 1 to ${MAX_NOTE_LANES} lanes per lane`);
86
+ return { dspFileName: file, notes: { kind: notes.kind, lanesPerLane: notes.lanesPerLane } };
87
+ });
88
+ }
89
+ const isHead = (head) => {
90
+ const h = head;
91
+ return Boolean(h && typeof h === 'object' && typeof h.instrument === 'string' && (h.notes === null || typeof h.notes === 'string'));
92
+ };
93
+ /** A release's programs, if what it carries reads as them; anything else is no programs. */
94
+ export function readReleasedPrograms(value) {
95
+ const programs = value;
96
+ if (!programs || typeof programs !== 'object' || !Number.isInteger(programs.runtime) || typeof programs.sources !== 'string')
97
+ return null;
98
+ if (!/^[a-z0-9]+$/i.test(programs.sources) || !programs.heads || typeof programs.heads !== 'object')
99
+ return null;
100
+ const heads = Object.fromEntries(Object.entries(programs.heads).filter(([, head]) => isHead(head)));
101
+ return Object.keys(heads).length ? { runtime: programs.runtime, sources: programs.sources, heads } : null;
102
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Composing a rack description into one FAUST program.
3
+ *
4
+ * This is the play runtime's source: the same per-module `.dsp` files the edit
5
+ * runtime compiles standalone, stitched together with `component()` so both
6
+ * runtimes are built from one authored source. Parity is structural, and
7
+ * measured — a two-module chain renders bit-identically either way.
8
+ *
9
+ * ## The slot group is not optional
10
+ *
11
+ * Two instances of the same module in one program **silently collapse into a
12
+ * single control**:
13
+ *
14
+ * process = component("psGain.dsp") : component("psGain.dsp");
15
+ * -> one address: /psVoice/gain
16
+ *
17
+ * Both multiplies still happen, so the audio is right, but the two slots share
18
+ * one parameter and move together — and Faust does not warn. Wrapping each slot
19
+ * in its own group fixes it and costs nothing:
20
+ *
21
+ * process = hgroup("s0", component(...)) : hgroup("s1", component(...));
22
+ * -> /psVoice/s0/gain, /psVoice/s1/gain
23
+ *
24
+ * That group name is also the **per-slot identity** a single compiled program
25
+ * was said to lack. It has one — as long as the composer never forgets, which
26
+ * is why it is emitted here rather than left to each `.dsp`, and why
27
+ * `slotGroupId` is derived from the slot index and cannot be omitted.
28
+ *
29
+ * ## A note chain
30
+ *
31
+ * A head rack that plays notes composes as lanes (`lib/ps_notes.lib`):
32
+ *
33
+ * process = nt.chainEntry(8) // the lanes the allocator writes
34
+ * : hgroup("s0", nt.bypass(8, 1, library("pitch.dsp")[SLOT=0;].notes(8)))
35
+ * : hgroup("tap0", nt.tap(8)) // the notes leaving s0
36
+ * : nt.toPlay(8) // the beats stay behind
37
+ * : hgroup("s1", library("synth.dsp")[SLOT=1;].play(8)) // a note voice per lane
38
+ * : hgroup("s2", component("filter.dsp"));
39
+ *
40
+ * One entry at the head, sized by the instrument's polyphony; each generator or MIDI effect takes the
41
+ * lanes so far and multiplies them by its `lanesPerLane`, inside its note-domain bypass; the
42
+ * instrument plays every lane that reaches it. `SLOT` is the module's place, its part of the seed;
43
+ * a module that adds lanes (a structural `lanesOut`) also gets `LANES_OUT`, its lanes out per lane in,
44
+ * since `notes(n)` is told only n.
45
+ *
46
+ * Between the note modules a lane carries a fourth signal, the beat its note started on (what Random
47
+ * and Velocity draw from): `nt.chainEntry` writes it, `nt.toPlay` drops it, so an instrument takes
48
+ * three signals a lane whether or not anything sits in front of it. An instrument alone keeps the
49
+ * plain `nt.entry`.
50
+ */
51
+ /** The most note lanes one head plays: what reaches the instrument after every note module has multiplied them. */
52
+ export declare const MAX_NOTE_LANES = 16;
53
+ /**
54
+ * What a slot does with notes: a notes instrument's polyphony (and its program constants, when it has
55
+ * any), or a generator's or MIDI effect's lanes out per lane in.
56
+ */
57
+ export type SlotNotes = {
58
+ kind: 'instrument';
59
+ polyphony: number;
60
+ constants?: Readonly<Record<string, number>>;
61
+ copies?: number;
62
+ } | {
63
+ kind: 'generator' | 'midi';
64
+ lanesPerLane: number;
65
+ };
66
+ /** One slot of a composed voice: the module's `.dsp`, by file name. */
67
+ export interface VoiceSlot {
68
+ dspFileName: string;
69
+ /**
70
+ * A note module, with what it does with notes at its stored values (the registry's `resolveNotes`):
71
+ * a generator or MIDI effect's lanes out per lane in, or a notes instrument's polyphony. Both are
72
+ * structural: they shape the program.
73
+ */
74
+ notes?: SlotNotes;
75
+ }
76
+ /**
77
+ * The group name for a slot. Index-based, so it is stable across renames and
78
+ * unique by construction — the two properties the collision above needs.
79
+ */
80
+ export declare function slotGroupId(index: number): string;
81
+ /** The address a slot's control has once composed: `/<voice>/<group>/<control>`. */
82
+ export declare function composedParamAddress(voiceName: string, index: number, controlName: string): string;
83
+ /**
84
+ * Build the `.dsp` source for one voice. `voiceName` is the program name and the address root;
85
+ * `slots` the ordered rack chain.
86
+ */
87
+ export declare function buildVoiceProgram({ voiceName, slots }?: {
88
+ voiceName?: string;
89
+ slots?: readonly VoiceSlot[];
90
+ }): string;
91
+ /** The group of the tap after a note module (its slot index): the notes leaving it. */
92
+ export declare function tapGroupId(index: number): string;
93
+ /**
94
+ * A stable key for a composed voice.
95
+ *
96
+ * The compiled program is a pure function of the ordered slot list, so this is
97
+ * what makes "compile once, then every listener fetches a finished artifact"
98
+ * possible. It deliberately covers *only* what changes the program: the modules
99
+ * and their order. Parameter **values** are written into the running unit and
100
+ * must not invalidate the cache — a knob turn is not a recompile
101
+ * (`reactivity: 'live'` versus `'structural'`).
102
+ *
103
+ */
104
+ export declare function voiceCacheKey(slots: readonly VoiceSlot[] | null | undefined): string;
105
+ /**
106
+ * The name every program of a released head declares. A control's address starts with it, so with one
107
+ * name the two programs of a head read as one (`/psHead/s0/rate`, `/psHead/s1/cutoff`), and an
108
+ * instrument's program is the same file whichever Orbiter plays it.
109
+ */
110
+ export declare const HEAD_PROGRAM_NAME = "psHead";
111
+ /** One program of a released head: what it is filed under, and its source. */
112
+ export interface HeadProgram {
113
+ key: string;
114
+ source: string;
115
+ }
116
+ /**
117
+ * A released head, as the programs play mode downloads: the note modules as one (the entry, each
118
+ * module in its bypass, a tap after each; its outputs are the lanes), and the instrument as another
119
+ * (its inputs are those lanes), played one after the other in one audio processor.
120
+ */
121
+ export interface HeadPrograms {
122
+ /** Null when nothing sits in front of the instrument: its program then carries the entry itself. */
123
+ notes: HeadProgram | null;
124
+ instrument: HeadProgram;
125
+ /** The lanes between the two (three signals a lane). */
126
+ lanes: number;
127
+ }
128
+ /**
129
+ * A head composed for release: always two programs when a note module sits in front of the
130
+ * instrument, so neither grows past the size a phone still optimises, and the instrument's (the
131
+ * large one) depends only on the instrument, its lanes, its constants and its place: every Orbiter
132
+ * with that instrument there shares one file.
133
+ *
134
+ * `slots`: the note modules in rack order, then the instrument (the audio effects after it stay
135
+ * units of their own). The addresses are the ones `buildVoiceProgram` gives the same chain.
136
+ */
137
+ export declare function buildHeadPrograms(slots: readonly VoiceSlot[]): HeadPrograms;