@driftengine/audio 4.8.2 → 4.8.4
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 +1 -1
- package/dist/audioHarness.d.ts +3 -1
- package/dist/audioHarness.js +4 -1
- package/dist/graph.d.ts +9 -3
- package/dist/graph.js +11 -5
- package/dist/registry.d.ts +11 -3
- package/dist/registry.js +11 -0
- package/package.json +1 -1
- package/src/audioHarness.ts +4 -1
- package/src/graph.ts +11 -5
- package/src/registry.ts +24 -3
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.
|
|
7
|
+
**Cost: 6.3 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)
|
package/dist/audioHarness.d.ts
CHANGED
|
@@ -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;
|
package/dist/audioHarness.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
270
|
+
this.started = true;
|
|
271
|
+
this.launch(Number.isFinite(fromSec) && fromSec > 0 ? fromSec : 0);
|
|
266
272
|
return this.startsInSec;
|
|
267
273
|
}
|
|
268
274
|
/**
|
package/dist/registry.d.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
|
|
@@ -22,8 +22,16 @@ export interface SoundSource {
|
|
|
22
22
|
* the browser decodes all of these natively.
|
|
23
23
|
*/
|
|
24
24
|
urls: readonly string[];
|
|
25
|
-
/**
|
|
26
|
-
|
|
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
package/src/audioHarness.ts
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
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
|
-
/**
|
|
27
|
-
|
|
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. */
|