@driftengine/audio 4.8.2 → 4.8.3

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
@@ -4,7 +4,7 @@ Layered stems, synthesis, rhythm and kick detection, distance and stereo placeme
4
4
  sources, reverb zones a **source** can be inside as well as the listener, and first-order
5
5
  ambisonic soundfields decoded through six virtual speakers.
6
6
 
7
- **Cost: 6.1 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
7
+ **Cost: 6.4 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
8
8
  3% — the number is a fact about the build rather than a claim in a document.
9
9
 
10
10
  Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
@@ -42,6 +42,8 @@ export declare class StubNode {
42
42
  readonly stops: number[];
43
43
  /** Instants `start` was asked for, so a test can see an offline launch has no lead. */
44
44
  readonly starts: number[];
45
+ /** Offsets into the buffer `start` was asked for, beside `starts`. */
46
+ readonly offsets: number[];
45
47
  type: string;
46
48
  buffer: unknown;
47
49
  loop: boolean;
@@ -83,7 +85,7 @@ export declare class StubNode {
83
85
  */
84
86
  connect(target: StubNode, output?: number): StubNode;
85
87
  disconnect(): void;
86
- start(at?: number): void;
88
+ start(at?: number, offset?: number): void;
87
89
  stop(at?: number): void;
88
90
  getByteFrequencyData(): void;
89
91
  getFloatFrequencyData(): void;
@@ -40,6 +40,8 @@ export class StubNode {
40
40
  stops = [];
41
41
  /** Instants `start` was asked for, so a test can see an offline launch has no lead. */
42
42
  starts = [];
43
+ /** Offsets into the buffer `start` was asked for, beside `starts`. */
44
+ offsets = [];
43
45
  type = '';
44
46
  buffer = null;
45
47
  loop = false;
@@ -86,8 +88,9 @@ export class StubNode {
86
88
  return target;
87
89
  }
88
90
  disconnect() { }
89
- start(at = 0) {
91
+ start(at = 0, offset = 0) {
90
92
  this.starts.push(at);
93
+ this.offsets.push(offset);
91
94
  }
92
95
  stop(at = 0) {
93
96
  this.stops.push(at);
package/dist/graph.d.ts CHANGED
@@ -245,7 +245,7 @@ export declare class AudioGraph {
245
245
  */
246
246
  release(): void;
247
247
  /**
248
- * Stop the stems and start them again from the top.
248
+ * Stop the stems and start them again: from the top, or from `fromSec` into the track.
249
249
  *
250
250
  * A `BufferSource` cannot be rewound — the spec makes it one-shot — so starting
251
251
  * over means discarding the sources and creating new ones. That is cheap: a
@@ -256,12 +256,18 @@ export declare class AudioGraph {
256
256
  * in its own world. Left running instead, a loop's downbeats land somewhere
257
257
  * different on every attempt.
258
258
  *
259
- * **Returns how long until beat zero is actually heard**, in seconds, because
259
+ * **`fromSec` is a seek**, for music held to a clock of its own: a demo's frame counter or a
260
+ * replay's tick has to resume the score where that clock is after every skip, and every stem
261
+ * starts there at one instant, wrapped by its own length because the stems loop. A caller that
262
+ * ran its own source into the music bus instead had a track outside every stem's lock. Anything
263
+ * not a positive number is the top.
264
+ *
265
+ * **Returns how long until that place is actually heard**, in seconds, because
260
266
  * `launch` schedules a little ahead of now and a caller lining a picture up
261
267
  * against the music needs that number rather than an assumption. Zero when
262
268
  * nothing started.
263
269
  */
264
- restart(): number;
270
+ restart(fromSec?: number): number;
265
271
  /**
266
272
  * Seconds until the scheduled start of whatever is playing, or 0 if it is
267
273
  * already sounding.
package/dist/graph.js CHANGED
@@ -242,7 +242,7 @@ export class AudioGraph {
242
242
  this.launch(this.positionSec);
243
243
  }
244
244
  /**
245
- * Stop the stems and start them again from the top.
245
+ * Stop the stems and start them again: from the top, or from `fromSec` into the track.
246
246
  *
247
247
  * A `BufferSource` cannot be rewound — the spec makes it one-shot — so starting
248
248
  * over means discarding the sources and creating new ones. That is cheap: a
@@ -253,16 +253,22 @@ export class AudioGraph {
253
253
  * in its own world. Left running instead, a loop's downbeats land somewhere
254
254
  * different on every attempt.
255
255
  *
256
- * **Returns how long until beat zero is actually heard**, in seconds, because
256
+ * **`fromSec` is a seek**, for music held to a clock of its own: a demo's frame counter or a
257
+ * replay's tick has to resume the score where that clock is after every skip, and every stem
258
+ * starts there at one instant, wrapped by its own length because the stems loop. A caller that
259
+ * ran its own source into the music bus instead had a track outside every stem's lock. Anything
260
+ * not a positive number is the top.
261
+ *
262
+ * **Returns how long until that place is actually heard**, in seconds, because
257
263
  * `launch` schedules a little ahead of now and a caller lining a picture up
258
264
  * against the music needs that number rather than an assumption. Zero when
259
265
  * nothing started.
260
266
  */
261
- restart() {
267
+ restart(fromSec = 0) {
262
268
  this.stopSources();
263
- this.started = false;
264
269
  this.paused = false;
265
- this.start();
270
+ this.started = true;
271
+ this.launch(Number.isFinite(fromSec) && fromSec > 0 ? fromSec : 0);
266
272
  return this.startsInSec;
267
273
  }
268
274
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Named sound slots, resolved once at load: real file if present, synthesised
3
- * buffer if not.
3
+ * buffer if not — or, for a slot registered with no stand-in, a loud failure.
4
4
  *
5
5
  * Synthesis is a placeholder, never the destination. Addressing every sound by
6
6
  * slot means dropping a file into the assets folder replaces it — no code
@@ -22,8 +22,16 @@ export interface SoundSource {
22
22
  * the browser decodes all of these natively.
23
23
  */
24
24
  urls: readonly string[];
25
- /** Built only when no candidate is present or usable. */
26
- synth: (ctx: BaseAudioContext) => AudioBuffer;
25
+ /**
26
+ * Built only when no candidate is present or usable.
27
+ *
28
+ * **Omitted, the slot is required.** A stand-in is right for a footstep and wrong for a score
29
+ * that has to be the file or nothing a game would ship, which until now could not be registered
30
+ * at all and was decoded outside the registry. A required slot none of whose candidates loads is
31
+ * recorded in `unbuilt`, and `load` rejects — after every other slot has settled, so one absent
32
+ * file still silences nothing else — naming each such slot and what it tried.
33
+ */
34
+ synth?: (ctx: BaseAudioContext) => AudioBuffer;
27
35
  }
28
36
  /** How a slot ended up being filled. Useful in dev to see what is still synth. */
29
37
  export type SoundOrigin = 'file' | 'synth';
package/dist/registry.js CHANGED
@@ -79,9 +79,16 @@ export class SoundRegistry {
79
79
  async load(ctx) {
80
80
  const entries = [...this.sources.entries()];
81
81
  await Promise.allSettled(entries.map(([slot, source]) => this.loadFile(ctx, slot, source)));
82
+ const missing = [];
82
83
  for (const [slot, source] of entries) {
83
84
  if (this.buffers.has(slot))
84
85
  continue;
86
+ if (source.synth === undefined) {
87
+ const tried = source.urls.length === 0 ? 'no candidates' : source.urls.join(', ');
88
+ this.failed.set(slot, `required, and none of its files loaded: ${tried}`);
89
+ missing.push(`\`${slot}\` (${tried})`);
90
+ continue;
91
+ }
85
92
  await new Promise((resolve) => {
86
93
  setTimeout(resolve, 0);
87
94
  });
@@ -106,6 +113,10 @@ export class SoundRegistry {
106
113
  this.failed.set(slot, error instanceof Error ? error.message : String(error));
107
114
  }
108
115
  }
116
+ if (missing.length > 0) {
117
+ throw new Error(`SoundRegistry: ${missing.length === 1 ? 'a required slot' : `${missing.length} required slots`} ` +
118
+ `did not load, and a required slot has no stand-in: ${missing.join('; ')}.`);
119
+ }
109
120
  }
110
121
  /** The first candidate that loads and decodes wins; none of them is normal. */
111
122
  async loadFile(ctx, slot, source) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/audio",
3
- "version": "4.8.2",
3
+ "version": "4.8.3",
4
4
  "description": "Layered stems, synthesis, rhythm analysis and positional placement",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -52,6 +52,8 @@ export class StubNode {
52
52
  readonly stops: number[] = [];
53
53
  /** Instants `start` was asked for, so a test can see an offline launch has no lead. */
54
54
  readonly starts: number[] = [];
55
+ /** Offsets into the buffer `start` was asked for, beside `starts`. */
56
+ readonly offsets: number[] = [];
55
57
  type = '';
56
58
  buffer: unknown = null;
57
59
  loop = false;
@@ -102,8 +104,9 @@ export class StubNode {
102
104
  return target;
103
105
  }
104
106
  disconnect(): void {}
105
- start(at = 0): void {
107
+ start(at = 0, offset = 0): void {
106
108
  this.starts.push(at);
109
+ this.offsets.push(offset);
107
110
  }
108
111
  stop(at = 0): void {
109
112
  this.stops.push(at);
package/src/graph.ts CHANGED
@@ -357,7 +357,7 @@ export class AudioGraph {
357
357
  }
358
358
 
359
359
  /**
360
- * Stop the stems and start them again from the top.
360
+ * Stop the stems and start them again: from the top, or from `fromSec` into the track.
361
361
  *
362
362
  * A `BufferSource` cannot be rewound — the spec makes it one-shot — so starting
363
363
  * over means discarding the sources and creating new ones. That is cheap: a
@@ -368,16 +368,22 @@ export class AudioGraph {
368
368
  * in its own world. Left running instead, a loop's downbeats land somewhere
369
369
  * different on every attempt.
370
370
  *
371
- * **Returns how long until beat zero is actually heard**, in seconds, because
371
+ * **`fromSec` is a seek**, for music held to a clock of its own: a demo's frame counter or a
372
+ * replay's tick has to resume the score where that clock is after every skip, and every stem
373
+ * starts there at one instant, wrapped by its own length because the stems loop. A caller that
374
+ * ran its own source into the music bus instead had a track outside every stem's lock. Anything
375
+ * not a positive number is the top.
376
+ *
377
+ * **Returns how long until that place is actually heard**, in seconds, because
372
378
  * `launch` schedules a little ahead of now and a caller lining a picture up
373
379
  * against the music needs that number rather than an assumption. Zero when
374
380
  * nothing started.
375
381
  */
376
- restart(): number {
382
+ restart(fromSec = 0): number {
377
383
  this.stopSources();
378
- this.started = false;
379
384
  this.paused = false;
380
- this.start();
385
+ this.started = true;
386
+ this.launch(Number.isFinite(fromSec) && fromSec > 0 ? fromSec : 0);
381
387
  return this.startsInSec;
382
388
  }
383
389
 
package/src/registry.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Named sound slots, resolved once at load: real file if present, synthesised
3
- * buffer if not.
3
+ * buffer if not — or, for a slot registered with no stand-in, a loud failure.
4
4
  *
5
5
  * Synthesis is a placeholder, never the destination. Addressing every sound by
6
6
  * slot means dropping a file into the assets folder replaces it — no code
@@ -23,8 +23,16 @@ export interface SoundSource {
23
23
  * the browser decodes all of these natively.
24
24
  */
25
25
  urls: readonly string[];
26
- /** Built only when no candidate is present or usable. */
27
- synth: (ctx: BaseAudioContext) => AudioBuffer;
26
+ /**
27
+ * Built only when no candidate is present or usable.
28
+ *
29
+ * **Omitted, the slot is required.** A stand-in is right for a footstep and wrong for a score
30
+ * that has to be the file or nothing a game would ship, which until now could not be registered
31
+ * at all and was decoded outside the registry. A required slot none of whose candidates loads is
32
+ * recorded in `unbuilt`, and `load` rejects — after every other slot has settled, so one absent
33
+ * file still silences nothing else — naming each such slot and what it tried.
34
+ */
35
+ synth?: (ctx: BaseAudioContext) => AudioBuffer;
28
36
  }
29
37
 
30
38
  /** How a slot ended up being filled. Useful in dev to see what is still synth. */
@@ -117,8 +125,15 @@ export class SoundRegistry {
117
125
  const entries = [...this.sources.entries()];
118
126
  await Promise.allSettled(entries.map(([slot, source]) => this.loadFile(ctx, slot, source)));
119
127
 
128
+ const missing: string[] = [];
120
129
  for (const [slot, source] of entries) {
121
130
  if (this.buffers.has(slot)) continue;
131
+ if (source.synth === undefined) {
132
+ const tried = source.urls.length === 0 ? 'no candidates' : source.urls.join(', ');
133
+ this.failed.set(slot, `required, and none of its files loaded: ${tried}`);
134
+ missing.push(`\`${slot}\` (${tried})`);
135
+ continue;
136
+ }
122
137
  await new Promise<void>((resolve) => {
123
138
  setTimeout(resolve, 0);
124
139
  });
@@ -142,6 +157,12 @@ export class SoundRegistry {
142
157
  this.failed.set(slot, error instanceof Error ? error.message : String(error));
143
158
  }
144
159
  }
160
+ if (missing.length > 0) {
161
+ throw new Error(
162
+ `SoundRegistry: ${missing.length === 1 ? 'a required slot' : `${missing.length} required slots`} ` +
163
+ `did not load, and a required slot has no stand-in: ${missing.join('; ')}.`,
164
+ );
165
+ }
145
166
  }
146
167
 
147
168
  /** The first candidate that loads and decodes wins; none of them is normal. */