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.
@@ -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
- 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 ?? {};
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 { dspFileName: file, notes: { kind: 'instrument', polyphony: notes.polyphony, ...(entries.length ? { constants: Object.fromEntries(entries) } : {}) } };
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 = Object.entries(slots[head].notes.constants ?? {})
138
- .map(([name, value]) => {
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 ${head}: unsafe constant ${name}`);
141
- return ` ${name} = ${wholeAtLeastOne(value, `slot ${head}: ${name}`)};`;
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?.kind === 'instrument' ? constantsKey(notes.constants) : '';
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) => `${slot.dspFileName}#${slot.notes.kind}${slot.notes.lanesPerLane}`).join('>');
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.16.0",
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",
@@ -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
- if (last) {
94
- 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`);
95
- const constants = notes.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 { dspFileName: file, notes: { kind: 'instrument', polyphony: notes.polyphony, ...(entries.length ? { constants: Object.fromEntries(entries) } : {}) } };
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 = Object.entries((slots[head].notes as { constants?: Readonly<Record<string, number>> }).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?.kind === 'instrument' ? constantsKey(notes.constants) : '';
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) => `${slot.dspFileName}#${slot.notes!.kind}${(slot.notes as { lanesPerLane: number }).lanesPerLane}`).join('>');
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}`)}`,