entangled-worlds-orbiters-shared 2.16.0 → 2.17.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/dist/faust/recipes.js +10 -6
- package/dist/faust/voiceProgram.d.ts +2 -0
- package/dist/faust/voiceProgram.js +16 -9
- package/package.json +1 -1
- package/src/faust/recipes.ts +9 -5
- package/src/faust/voiceProgram.ts +18 -10
package/dist/faust/recipes.js
CHANGED
|
@@ -70,20 +70,24 @@ export function readHeadShape(value, sources) {
|
|
|
70
70
|
if (typeof file !== 'string' || !Object.hasOwn(sources, file))
|
|
71
71
|
throw new Error(`module ${index}: no such source`);
|
|
72
72
|
const last = index === value.length - 1;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const constants = notes.constants ?? {};
|
|
73
|
+
// Program constants, an instrument's or a note module's: a few safe names, each a small whole number.
|
|
74
|
+
const constantsOf = (given) => {
|
|
75
|
+
const constants = given ?? {};
|
|
77
76
|
if (typeof constants !== 'object' || Array.isArray(constants))
|
|
78
77
|
throw new Error(`module ${index}: constants`);
|
|
79
78
|
const entries = Object.entries(constants);
|
|
80
79
|
if (entries.length > 8 || entries.some(([name, n]) => !/^[A-Z][A-Z0-9_]{0,23}$/.test(name) || !whole(n, MAX_CONSTANT)))
|
|
81
80
|
throw new Error(`module ${index}: constants`);
|
|
82
|
-
return
|
|
81
|
+
return entries.length ? { constants: Object.fromEntries(entries) } : {};
|
|
82
|
+
};
|
|
83
|
+
if (last) {
|
|
84
|
+
if (notes?.kind !== 'instrument' || !whole(notes.polyphony, MAX_NOTE_LANES))
|
|
85
|
+
throw new Error(`module ${index}: a head ends with an instrument of 1 to ${MAX_NOTE_LANES} voices`);
|
|
86
|
+
return { dspFileName: file, notes: { kind: 'instrument', polyphony: notes.polyphony, ...constantsOf(notes.constants) } };
|
|
83
87
|
}
|
|
84
88
|
if ((notes?.kind !== 'generator' && notes?.kind !== 'midi') || !whole(notes.lanesPerLane, MAX_NOTE_LANES))
|
|
85
89
|
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 } };
|
|
90
|
+
return { dspFileName: file, notes: { kind: notes.kind, lanesPerLane: notes.lanesPerLane, ...constantsOf(notes.constants) } };
|
|
87
91
|
});
|
|
88
92
|
}
|
|
89
93
|
const isHead = (head) => {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* lanes so far and multiplies them by its `lanesPerLane`, inside its note-domain bypass; the
|
|
42
42
|
* instrument plays every lane that reaches it. `SLOT` is the module's place, its part of the seed;
|
|
43
43
|
* a module that adds lanes (a structural `lanesOut`) also gets `LANES_OUT`, its lanes out per lane in,
|
|
44
|
+
* and a note module with program constants (an optional section compiled in or out) gets them too,
|
|
44
45
|
* since `notes(n)` is told only n.
|
|
45
46
|
*
|
|
46
47
|
* Between the note modules a lane carries a fourth signal, the beat its note started on (what Random
|
|
@@ -62,6 +63,7 @@ export type SlotNotes = {
|
|
|
62
63
|
} | {
|
|
63
64
|
kind: 'generator' | 'midi';
|
|
64
65
|
lanesPerLane: number;
|
|
66
|
+
constants?: Readonly<Record<string, number>>;
|
|
65
67
|
};
|
|
66
68
|
/** One slot of a composed voice: the module's `.dsp`, by file name. */
|
|
67
69
|
export interface VoiceSlot {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* lanes so far and multiplies them by its `lanesPerLane`, inside its note-domain bypass; the
|
|
42
42
|
* instrument plays every lane that reaches it. `SLOT` is the module's place, its part of the seed;
|
|
43
43
|
* a module that adds lanes (a structural `lanesOut`) also gets `LANES_OUT`, its lanes out per lane in,
|
|
44
|
+
* and a note module with program constants (an optional section compiled in or out) gets them too,
|
|
44
45
|
* since `notes(n)` is told only n.
|
|
45
46
|
*
|
|
46
47
|
* Between the note modules a lane carries a fourth signal, the beat its note started on (what Random
|
|
@@ -117,7 +118,7 @@ function noteModules(slots, head) {
|
|
|
117
118
|
}
|
|
118
119
|
const k = wholeAtLeastOne(notes.lanesPerLane, `slot ${index}: lanesPerLane`);
|
|
119
120
|
// A module that adds lanes is told how many it puts out per lane in (an echo's repeats + 1).
|
|
120
|
-
const env = k === 1 ? `SLOT = ${index};` : `SLOT = ${index}; LANES_OUT = ${k}
|
|
121
|
+
const env = (k === 1 ? `SLOT = ${index};` : `SLOT = ${index}; LANES_OUT = ${k};`) + constantsEnv(notes.constants, index);
|
|
121
122
|
const fx = `library("${slot.dspFileName}")[${env}].notes(${lanes})`;
|
|
122
123
|
parts.push(`hgroup("${slotGroupId(index)}", nt.bypass(${lanes}, ${k}, ${fx}))`);
|
|
123
124
|
lanes *= k;
|
|
@@ -134,13 +135,16 @@ function noteModules(slots, head) {
|
|
|
134
135
|
function instrumentPart(slots, head, lanes) {
|
|
135
136
|
// The instrument's place salts its random draws too (an LFO's Random), as every note module's does;
|
|
136
137
|
// its program constants (unison's `U`) shape it, as its polyphony does.
|
|
137
|
-
const constants =
|
|
138
|
-
|
|
138
|
+
const constants = constantsEnv(slots[head].notes.constants, head);
|
|
139
|
+
return `hgroup("${slotGroupId(head)}", library("${slots[head].dspFileName}")[SLOT = ${head};${constants}].play(${lanes}))`;
|
|
140
|
+
}
|
|
141
|
+
/** A slot's program constants as library environment entries (` U = 2;`), each a safe name and a whole number. */
|
|
142
|
+
function constantsEnv(constants, index) {
|
|
143
|
+
return Object.entries(constants ?? {}).map(([name, value]) => {
|
|
139
144
|
if (!/^[A-Z][A-Z0-9_]*$/.test(name))
|
|
140
|
-
throw new Error(`[voiceProgram] slot ${
|
|
141
|
-
return ` ${name} = ${wholeAtLeastOne(value, `slot ${
|
|
145
|
+
throw new Error(`[voiceProgram] slot ${index}: unsafe constant ${name}`);
|
|
146
|
+
return ` ${name} = ${wholeAtLeastOne(value, `slot ${index}: ${name}`)};`;
|
|
142
147
|
}).join('');
|
|
143
|
-
return `hgroup("${slotGroupId(head)}", library("${slots[head].dspFileName}")[SLOT = ${head};${constants}].play(${lanes}))`;
|
|
144
148
|
}
|
|
145
149
|
/** The entry, the note modules and the instrument, up to `head` (the instrument): lanes all the way. */
|
|
146
150
|
function noteChain(slots, head) {
|
|
@@ -167,8 +171,8 @@ export function voiceCacheKey(slots) {
|
|
|
167
171
|
// A note module's lane count is structural too: it shapes the program.
|
|
168
172
|
const shape = (slots ?? []).map((slot) => {
|
|
169
173
|
const notes = slot?.notes;
|
|
170
|
-
const constants = notes
|
|
171
|
-
const lanes = !notes ? '' : notes.kind === 'instrument' ? `#p${notes.polyphony}${constants}` : `#${notes.kind}${notes.lanesPerLane}`;
|
|
174
|
+
const constants = notes ? constantsKey(notes.constants) : '';
|
|
175
|
+
const lanes = !notes ? '' : notes.kind === 'instrument' ? `#p${notes.polyphony}${constants}` : `#${notes.kind}${notes.lanesPerLane}${constants}`;
|
|
172
176
|
return String(slot?.dspFileName ?? '') + lanes;
|
|
173
177
|
}).join('>');
|
|
174
178
|
return `v${fnv(shape)}`;
|
|
@@ -238,7 +242,10 @@ export function buildHeadPrograms(slots) {
|
|
|
238
242
|
// FAUST roots a program's controls in a group named after it only when it has several groups at the
|
|
239
243
|
// top: named here, both programs' addresses start as the one program's do, whatever each holds.
|
|
240
244
|
const root = (chain) => `vgroup("${HEAD_PROGRAM_NAME}", ${chain})`;
|
|
241
|
-
const shape = slots.slice(0, head).map((slot) =>
|
|
245
|
+
const shape = slots.slice(0, head).map((slot) => {
|
|
246
|
+
const n = slot.notes;
|
|
247
|
+
return `${slot.dspFileName}#${n.kind}${n.lanesPerLane}${constantsKey(n.constants)}`;
|
|
248
|
+
}).join('>');
|
|
242
249
|
return {
|
|
243
250
|
notes: {
|
|
244
251
|
key: `n${fileKey(`${shape}>#p${notes.polyphony}`)}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "entangled-worlds-orbiters-shared",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "Shared library for entangled worlds orbiters - contains core world rendering, moons system, and UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/faust/recipes.ts
CHANGED
|
@@ -90,16 +90,20 @@ export function readHeadShape(value: unknown, sources: Readonly<Record<string, s
|
|
|
90
90
|
const notes = (slot as VoiceSlot | null)?.notes as SlotNotes | undefined;
|
|
91
91
|
if (typeof file !== 'string' || !Object.hasOwn(sources, file)) throw new Error(`module ${index}: no such source`);
|
|
92
92
|
const last = index === value.length - 1;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const constants =
|
|
93
|
+
// Program constants, an instrument's or a note module's: a few safe names, each a small whole number.
|
|
94
|
+
const constantsOf = (given: unknown): { constants?: Record<string, number> } => {
|
|
95
|
+
const constants = given ?? {};
|
|
96
96
|
if (typeof constants !== 'object' || Array.isArray(constants)) throw new Error(`module ${index}: constants`);
|
|
97
97
|
const entries = Object.entries(constants);
|
|
98
98
|
if (entries.length > 8 || entries.some(([name, n]) => !/^[A-Z][A-Z0-9_]{0,23}$/.test(name) || !whole(n, MAX_CONSTANT))) throw new Error(`module ${index}: constants`);
|
|
99
|
-
return
|
|
99
|
+
return entries.length ? { constants: Object.fromEntries(entries) } : {};
|
|
100
|
+
};
|
|
101
|
+
if (last) {
|
|
102
|
+
if (notes?.kind !== 'instrument' || !whole(notes.polyphony, MAX_NOTE_LANES)) throw new Error(`module ${index}: a head ends with an instrument of 1 to ${MAX_NOTE_LANES} voices`);
|
|
103
|
+
return { dspFileName: file, notes: { kind: 'instrument', polyphony: notes.polyphony, ...constantsOf(notes.constants) } };
|
|
100
104
|
}
|
|
101
105
|
if ((notes?.kind !== 'generator' && notes?.kind !== 'midi') || !whole(notes.lanesPerLane, MAX_NOTE_LANES)) throw new Error(`module ${index}: a note module with 1 to ${MAX_NOTE_LANES} lanes per lane`);
|
|
102
|
-
return { dspFileName: file, notes: { kind: notes.kind, lanesPerLane: notes.lanesPerLane } };
|
|
106
|
+
return { dspFileName: file, notes: { kind: notes.kind, lanesPerLane: notes.lanesPerLane, ...constantsOf(notes.constants) } };
|
|
103
107
|
});
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* lanes so far and multiplies them by its `lanesPerLane`, inside its note-domain bypass; the
|
|
42
42
|
* instrument plays every lane that reaches it. `SLOT` is the module's place, its part of the seed;
|
|
43
43
|
* a module that adds lanes (a structural `lanesOut`) also gets `LANES_OUT`, its lanes out per lane in,
|
|
44
|
+
* and a note module with program constants (an optional section compiled in or out) gets them too,
|
|
44
45
|
* since `notes(n)` is told only n.
|
|
45
46
|
*
|
|
46
47
|
* Between the note modules a lane carries a fourth signal, the beat its note started on (what Random
|
|
@@ -55,7 +56,7 @@ export const MAX_NOTE_LANES = 16;
|
|
|
55
56
|
* What a slot does with notes: a notes instrument's polyphony (and its program constants, when it has
|
|
56
57
|
* any), or a generator's or MIDI effect's lanes out per lane in.
|
|
57
58
|
*/
|
|
58
|
-
export type SlotNotes = { kind: 'instrument'; polyphony: number; constants?: Readonly<Record<string, number>>; copies?: number } | { kind: 'generator' | 'midi'; lanesPerLane: number };
|
|
59
|
+
export type SlotNotes = { kind: 'instrument'; polyphony: number; constants?: Readonly<Record<string, number>>; copies?: number } | { kind: 'generator' | 'midi'; lanesPerLane: number; constants?: Readonly<Record<string, number>> };
|
|
59
60
|
|
|
60
61
|
/** Faust identifiers: the group name goes into generated source, so keep it tame. */
|
|
61
62
|
const SAFE_SEGMENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -143,7 +144,7 @@ function noteModules(slots: readonly VoiceSlot[], head: number): { parts: string
|
|
|
143
144
|
}
|
|
144
145
|
const k = wholeAtLeastOne(notes.lanesPerLane, `slot ${index}: lanesPerLane`);
|
|
145
146
|
// A module that adds lanes is told how many it puts out per lane in (an echo's repeats + 1).
|
|
146
|
-
const env = k === 1 ? `SLOT = ${index};` : `SLOT = ${index}; LANES_OUT = ${k}
|
|
147
|
+
const env = (k === 1 ? `SLOT = ${index};` : `SLOT = ${index}; LANES_OUT = ${k};`) + constantsEnv(notes.constants, index);
|
|
147
148
|
const fx = `library("${slot.dspFileName}")[${env}].notes(${lanes})`;
|
|
148
149
|
parts.push(`hgroup("${slotGroupId(index)}", nt.bypass(${lanes}, ${k}, ${fx}))`);
|
|
149
150
|
lanes *= k;
|
|
@@ -160,14 +161,18 @@ function noteModules(slots: readonly VoiceSlot[], head: number): { parts: string
|
|
|
160
161
|
function instrumentPart(slots: readonly VoiceSlot[], head: number, lanes: number): string {
|
|
161
162
|
// The instrument's place salts its random draws too (an LFO's Random), as every note module's does;
|
|
162
163
|
// its program constants (unison's `U`) shape it, as its polyphony does.
|
|
163
|
-
const constants =
|
|
164
|
-
.map(([name, value]) => {
|
|
165
|
-
if (!/^[A-Z][A-Z0-9_]*$/.test(name)) throw new Error(`[voiceProgram] slot ${head}: unsafe constant ${name}`);
|
|
166
|
-
return ` ${name} = ${wholeAtLeastOne(value, `slot ${head}: ${name}`)};`;
|
|
167
|
-
}).join('');
|
|
164
|
+
const constants = constantsEnv((slots[head].notes as { constants?: Readonly<Record<string, number>> }).constants, head);
|
|
168
165
|
return `hgroup("${slotGroupId(head)}", library("${slots[head].dspFileName}")[SLOT = ${head};${constants}].play(${lanes}))`;
|
|
169
166
|
}
|
|
170
167
|
|
|
168
|
+
/** A slot's program constants as library environment entries (` U = 2;`), each a safe name and a whole number. */
|
|
169
|
+
function constantsEnv(constants: Readonly<Record<string, number>> | undefined, index: number): string {
|
|
170
|
+
return Object.entries(constants ?? {}).map(([name, value]) => {
|
|
171
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(name)) throw new Error(`[voiceProgram] slot ${index}: unsafe constant ${name}`);
|
|
172
|
+
return ` ${name} = ${wholeAtLeastOne(value, `slot ${index}: ${name}`)};`;
|
|
173
|
+
}).join('');
|
|
174
|
+
}
|
|
175
|
+
|
|
171
176
|
/** The entry, the note modules and the instrument, up to `head` (the instrument): lanes all the way. */
|
|
172
177
|
function noteChain(slots: readonly VoiceSlot[], head: number): string[] {
|
|
173
178
|
const { parts, lanes } = noteModules(slots, head);
|
|
@@ -194,8 +199,8 @@ export function voiceCacheKey(slots: readonly VoiceSlot[] | null | undefined): s
|
|
|
194
199
|
// A note module's lane count is structural too: it shapes the program.
|
|
195
200
|
const shape = (slots ?? []).map((slot) => {
|
|
196
201
|
const notes = slot?.notes;
|
|
197
|
-
const constants = notes
|
|
198
|
-
const lanes = !notes ? '' : notes.kind === 'instrument' ? `#p${notes.polyphony}${constants}` : `#${notes.kind}${notes.lanesPerLane}`;
|
|
202
|
+
const constants = notes ? constantsKey(notes.constants) : '';
|
|
203
|
+
const lanes = !notes ? '' : notes.kind === 'instrument' ? `#p${notes.polyphony}${constants}` : `#${notes.kind}${notes.lanesPerLane}${constants}`;
|
|
199
204
|
return String(slot?.dspFileName ?? '') + lanes;
|
|
200
205
|
}).join('>');
|
|
201
206
|
return `v${fnv(shape)}`;
|
|
@@ -287,7 +292,10 @@ export function buildHeadPrograms(slots: readonly VoiceSlot[]): HeadPrograms {
|
|
|
287
292
|
// FAUST roots a program's controls in a group named after it only when it has several groups at the
|
|
288
293
|
// top: named here, both programs' addresses start as the one program's do, whatever each holds.
|
|
289
294
|
const root = (chain: string) => `vgroup("${HEAD_PROGRAM_NAME}", ${chain})`;
|
|
290
|
-
const shape = slots.slice(0, head).map((slot) =>
|
|
295
|
+
const shape = slots.slice(0, head).map((slot) => {
|
|
296
|
+
const n = slot.notes as Extract<SlotNotes, { kind: 'generator' | 'midi' }>;
|
|
297
|
+
return `${slot.dspFileName}#${n.kind}${n.lanesPerLane}${constantsKey(n.constants)}`;
|
|
298
|
+
}).join('>');
|
|
291
299
|
return {
|
|
292
300
|
notes: {
|
|
293
301
|
key: `n${fileKey(`${shape}>#p${notes.polyphony}`)}`,
|