@weasel-js/audio 1.1.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/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/index.d.ts +265 -0
- package/dist/index.js +622 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @weasel-js/audio
|
|
2
|
+
|
|
3
|
+
Web Audio engine for 2D scenes. No weasel dependencies — positional audio takes
|
|
4
|
+
plain `{ x, y }`.
|
|
5
|
+
|
|
6
|
+
Playback is lookahead-scheduled on the engine's own timer rather than triggered
|
|
7
|
+
from an animation frame: `AudioContext.currentTime` is driven by the audio
|
|
8
|
+
hardware, ticks independently of `requestAnimationFrame`, and cannot be paused
|
|
9
|
+
or time-scaled. Triggering a sound *on* a frame inherits frame jitter, which is
|
|
10
|
+
audible.
|
|
11
|
+
|
|
12
|
+
A hidden tab is the limit of that. Browsers clamp `setTimeout` to at least a
|
|
13
|
+
second there, so the pass runs far too late for a 100 ms lookahead and anything
|
|
14
|
+
booked while the tab is away arrives late. The clock keeps running, so ordering
|
|
15
|
+
survives; on return the engine drops what came due meanwhile rather than firing
|
|
16
|
+
it all at once. Moving the tick to a Worker would fix it and is not built.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
const engine = createAudioEngine();
|
|
20
|
+
const jump = await engine.load('/sfx/jump.wav');
|
|
21
|
+
engine.play(jump, { bus: 'sfx', position: { x: 40, y: 0 } });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Browsers start an `AudioContext` suspended until a user gesture. The engine
|
|
25
|
+
resumes on the first gesture automatically; `play()` before that drops the voice
|
|
26
|
+
with a dev warning rather than queueing it.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
interface AnalyserTapOptions {
|
|
2
|
+
/** Power of two, 32..32768. Default 2048. */
|
|
3
|
+
fftSize?: number;
|
|
4
|
+
}
|
|
5
|
+
interface AnalyserTap {
|
|
6
|
+
/** The underlying node, exposed for disposal assertions and advanced wiring. */
|
|
7
|
+
node: AnalyserNode;
|
|
8
|
+
/** Frequency data, sized `node.frequencyBinCount` when no array is given. */
|
|
9
|
+
frequencies(out?: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
|
|
10
|
+
/** Time-domain data, sized `node.fftSize` when no array is given — twice the
|
|
11
|
+
* frequency bin count, which is the whole window rather than half of it. */
|
|
12
|
+
waveform(out?: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
|
|
13
|
+
/** RMS amplitude over the whole time-domain window, 0..1. */
|
|
14
|
+
level(): number;
|
|
15
|
+
/** `n` averaged frequency bands normalized to 0..1 — the ergonomic form for
|
|
16
|
+
* driving a shader uniform, a vertex color, or a pose. `out`, when given,
|
|
17
|
+
* must be exactly `n` long. */
|
|
18
|
+
bands(n: number, out?: Float32Array<ArrayBuffer>): Float32Array<ArrayBuffer>;
|
|
19
|
+
/** Detach from the tapped node. Idempotent. */
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
22
|
+
declare function createAnalyserTap(ctx: AudioContext, source: AudioNode, opts?: AnalyserTapOptions): AnalyserTap;
|
|
23
|
+
|
|
24
|
+
interface BusHandle {
|
|
25
|
+
setGain(value: number, rampMs?: number): void;
|
|
26
|
+
mute(on: boolean): void;
|
|
27
|
+
solo(on: boolean): void;
|
|
28
|
+
/** The bus's own gain, as last set — not what a mute or solo is imposing. */
|
|
29
|
+
gain(): number;
|
|
30
|
+
/** The mute flag as set, independent of any solo. */
|
|
31
|
+
muted(): boolean;
|
|
32
|
+
/** The solo flag as set; whether it silences anything is a graph-wide answer. */
|
|
33
|
+
soloed(): boolean;
|
|
34
|
+
/** Whether the bus is being heard: unmuted, and soloed if any bus is soloed.
|
|
35
|
+
* Not derivable from `muted()` and `soloed()` alone. */
|
|
36
|
+
audible(): boolean;
|
|
37
|
+
}
|
|
38
|
+
interface BusGraph {
|
|
39
|
+
master: GainNode;
|
|
40
|
+
node(name: string): GainNode;
|
|
41
|
+
bus(name: string): BusHandle;
|
|
42
|
+
names(): string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The mix graph: one `GainNode` per named bus, all routed to a master that
|
|
46
|
+
* routes to the destination.
|
|
47
|
+
*
|
|
48
|
+
* Gain, mute and solo are three inputs to one output value rather than three
|
|
49
|
+
* things that write the node directly — otherwise unmuting restores the wrong
|
|
50
|
+
* value whenever a solo changed it in between. All three write through
|
|
51
|
+
* `writeParam`, so `rampMs` survives instead of being overwritten by the
|
|
52
|
+
* recomputation that follows it.
|
|
53
|
+
*/
|
|
54
|
+
declare function createBusGraph(ctx: AudioContext, names: string[]): BusGraph;
|
|
55
|
+
|
|
56
|
+
/** Opaque reference to a decoded sound. Mirrors `TextureHandle` in core. */
|
|
57
|
+
interface SoundHandle {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
}
|
|
60
|
+
interface SoundCache {
|
|
61
|
+
load(url: string): Promise<SoundHandle>;
|
|
62
|
+
loadAll(urls: Record<string, string>): Promise<Record<string, SoundHandle>>;
|
|
63
|
+
decode(bytes: ArrayBuffer): Promise<SoundHandle>;
|
|
64
|
+
/** Take ownership of a buffer the caller already has — a procedural synth,
|
|
65
|
+
* an OfflineAudioContext render, a recording. Synchronous; nothing to decode. */
|
|
66
|
+
register(buffer: AudioBuffer): SoundHandle;
|
|
67
|
+
buffer(handle: SoundHandle): AudioBuffer | null;
|
|
68
|
+
}
|
|
69
|
+
declare function createSoundCache(ctx: AudioContext, fetchFn?: typeof fetch): SoundCache;
|
|
70
|
+
|
|
71
|
+
interface Vec2 {
|
|
72
|
+
x: number;
|
|
73
|
+
y: number;
|
|
74
|
+
}
|
|
75
|
+
interface SpatialOptions {
|
|
76
|
+
/** Distance within which gain stays at 1. Default 1. The inverse model needs
|
|
77
|
+
* this above 0: at 0 it cliffs gain from 1 to 0 at any nonzero distance. */
|
|
78
|
+
refDistance?: number;
|
|
79
|
+
/** Distance at which linear rolloff reaches 0. Ignored by inverse. Default
|
|
80
|
+
* 10000. At or below `refDistance` it gives the same 1-to-0 cliff, there. */
|
|
81
|
+
maxDistance?: number;
|
|
82
|
+
/** Default 'inverse' — the natural-sounding one. */
|
|
83
|
+
rolloff?: 'inverse' | 'linear';
|
|
84
|
+
/** How sharply gain falls. Default 1. */
|
|
85
|
+
rolloffFactor?: number;
|
|
86
|
+
/** Horizontal distance mapping to full left/right. Default 500. */
|
|
87
|
+
panWidth?: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Map a source position to a gain and a stereo pan, relative to the listener.
|
|
91
|
+
*
|
|
92
|
+
* Pure — no Web Audio, no state. This is the whole spatial model.
|
|
93
|
+
*/
|
|
94
|
+
declare function spatialize(source: Vec2, listener: Vec2, opts?: SpatialOptions): {
|
|
95
|
+
gain: number;
|
|
96
|
+
pan: number;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type StealPolicy = 'oldest' | 'quietest';
|
|
100
|
+
interface VoicePoolOptions {
|
|
101
|
+
/** Maximum concurrent voices, at least 1. Beyond this, `acquire` steals. */
|
|
102
|
+
limit: number;
|
|
103
|
+
/** Default 'oldest'. */
|
|
104
|
+
steal?: StealPolicy;
|
|
105
|
+
}
|
|
106
|
+
interface VoiceRecord {
|
|
107
|
+
/** Engine time the voice started, in ms. */
|
|
108
|
+
startedAt: number;
|
|
109
|
+
/** Current gain, consulted by the 'quietest' steal policy. */
|
|
110
|
+
gain: number;
|
|
111
|
+
}
|
|
112
|
+
interface Acquisition {
|
|
113
|
+
slot: number;
|
|
114
|
+
/** Identifies this voice, not its slot: a stolen slot is reissued at once,
|
|
115
|
+
* so `release` and `setGain` take the token and ignore a stale one. */
|
|
116
|
+
token: number;
|
|
117
|
+
/** Token of the voice evicted to make room, or null. The caller is
|
|
118
|
+
* responsible for actually stopping that voice's nodes. */
|
|
119
|
+
stolen: number | null;
|
|
120
|
+
}
|
|
121
|
+
interface VoicePool {
|
|
122
|
+
acquire(record: VoiceRecord): Acquisition;
|
|
123
|
+
release(slot: number, token: number): void;
|
|
124
|
+
setGain(slot: number, token: number, gain: number): void;
|
|
125
|
+
active(): number;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Slot bookkeeping for concurrent voices, with a steal policy for when the
|
|
129
|
+
* limit is reached.
|
|
130
|
+
*
|
|
131
|
+
* This is pure accounting — it owns no audio nodes. The caller builds the
|
|
132
|
+
* `GainNode`/`StereoPannerNode` chain and mints a fresh `AudioBufferSourceNode`
|
|
133
|
+
* per play, because a source node is single-use by specification and cannot be
|
|
134
|
+
* restarted once stopped. Nothing is pooled but the slot numbers.
|
|
135
|
+
*/
|
|
136
|
+
declare function createVoicePool(opts: VoicePoolOptions): VoicePool;
|
|
137
|
+
|
|
138
|
+
interface PlayOptions {
|
|
139
|
+
/** Default: the first configured bus. */
|
|
140
|
+
bus?: string;
|
|
141
|
+
/** 0..1, default 1. Multiplied by any spatialized gain. */
|
|
142
|
+
gain?: number;
|
|
143
|
+
/** Playback rate, default 1. */
|
|
144
|
+
rate?: number;
|
|
145
|
+
/** Detune in cents, default 0. */
|
|
146
|
+
detune?: number;
|
|
147
|
+
loop?: boolean;
|
|
148
|
+
/** Explicit stereo pan, -1..1. Ignored when `position` is given. */
|
|
149
|
+
pan?: number;
|
|
150
|
+
/** World position; spatialized against the engine listener. */
|
|
151
|
+
position?: Vec2;
|
|
152
|
+
/** Engine time in ms (see `engine.now()`). Default: as soon as possible. */
|
|
153
|
+
when?: number;
|
|
154
|
+
/** `stopKey(key)` stops every voice sharing this key. */
|
|
155
|
+
cancelKey?: string;
|
|
156
|
+
onDone?: () => void;
|
|
157
|
+
}
|
|
158
|
+
interface VoiceHandle {
|
|
159
|
+
id: number;
|
|
160
|
+
/** `fadeMs` ramps the voice out and stops it at the end of the ramp. */
|
|
161
|
+
stop(fadeMs?: number): void;
|
|
162
|
+
setGain(value: number, rampMs?: number): void;
|
|
163
|
+
/** Playback rate. Applies to a voice booked for a future `when` too. */
|
|
164
|
+
setRate(value: number): void;
|
|
165
|
+
/** Detune in cents. Applies to a voice booked for a future `when` too. */
|
|
166
|
+
setDetune(cents: number): void;
|
|
167
|
+
/** Explicit stereo pan. Stops the voice tracking a `position`. */
|
|
168
|
+
setPan(value: number): void;
|
|
169
|
+
setPosition(p: Vec2): void;
|
|
170
|
+
isPlaying(): boolean;
|
|
171
|
+
}
|
|
172
|
+
interface AudioEngineOptions {
|
|
173
|
+
/** Injectable for tests and for consumers that own the context. */
|
|
174
|
+
context?: AudioContext;
|
|
175
|
+
/** Scheduling window in ms. Default 100. */
|
|
176
|
+
lookahead?: number;
|
|
177
|
+
/** Scheduler pass interval in ms. Default 25. */
|
|
178
|
+
tickInterval?: number;
|
|
179
|
+
/** Default ['sfx', 'music', 'ui']. */
|
|
180
|
+
buses?: string[];
|
|
181
|
+
/** Max concurrent voices PER BUS before stealing. Default 32. */
|
|
182
|
+
voiceLimit?: number;
|
|
183
|
+
/** Default 'oldest'. */
|
|
184
|
+
steal?: StealPolicy;
|
|
185
|
+
fetchFn?: typeof fetch;
|
|
186
|
+
setTimer?: (cb: () => void, ms: number) => unknown;
|
|
187
|
+
clearTimer?: (handle: unknown) => void;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface AudioEngine {
|
|
191
|
+
/** The engine's `AudioContext` — the one passed as `options.context`, or the
|
|
192
|
+
* one the engine created. Use it for `createBuffer`, for analysis, or for a
|
|
193
|
+
* node graph of your own alongside the engine's.
|
|
194
|
+
*
|
|
195
|
+
* `dispose()` closes a context the engine created, and every call on a
|
|
196
|
+
* closed context throws; a context you passed in is left open, because it is
|
|
197
|
+
* yours to close. The reference stays valid either way — read `state()`
|
|
198
|
+
* rather than assuming. */
|
|
199
|
+
readonly context: AudioContext;
|
|
200
|
+
state(): AudioContextState;
|
|
201
|
+
unlock(): Promise<void>;
|
|
202
|
+
/** Engine time in ms. */
|
|
203
|
+
now(): number;
|
|
204
|
+
load(url: string): Promise<SoundHandle>;
|
|
205
|
+
loadAll(urls: Record<string, string>): Promise<Record<string, SoundHandle>>;
|
|
206
|
+
decode(bytes: ArrayBuffer): Promise<SoundHandle>;
|
|
207
|
+
/** Play a buffer the consumer already holds — a synth, an offline render, a
|
|
208
|
+
* recording. `load` and `decode` both assume encoded bytes. */
|
|
209
|
+
register(buffer: AudioBuffer): SoundHandle;
|
|
210
|
+
play(sound: SoundHandle, opts?: PlayOptions): VoiceHandle;
|
|
211
|
+
stopKey(key: string): void;
|
|
212
|
+
stopAll(): void;
|
|
213
|
+
bus(name: string): BusHandle;
|
|
214
|
+
/** The configured bus names, in order. The first is `play()`'s default. */
|
|
215
|
+
busNames(): string[];
|
|
216
|
+
/** Voices currently sounding, on one bus or on all of them. A voice booked
|
|
217
|
+
* for a future `when` is not counted until it starts. */
|
|
218
|
+
activeVoices(bus?: string): number;
|
|
219
|
+
analyser(busName?: string, opts?: AnalyserTapOptions): AnalyserTap;
|
|
220
|
+
setListener(p: Vec2, opts?: SpatialOptions): void;
|
|
221
|
+
dispose(): void;
|
|
222
|
+
}
|
|
223
|
+
declare function createAudioEngine(opts?: AudioEngineOptions): AudioEngine;
|
|
224
|
+
|
|
225
|
+
interface SchedulerOptions {
|
|
226
|
+
/** Engine time in ms. Backed by `AudioContext.currentTime * 1000` in production. */
|
|
227
|
+
now: () => number;
|
|
228
|
+
/** One-shot: it fires once and the pass re-arms it. `setInterval` here would
|
|
229
|
+
* leave every previous timer running. */
|
|
230
|
+
setTimer: (cb: () => void, ms: number) => unknown;
|
|
231
|
+
clearTimer: (handle: unknown) => void;
|
|
232
|
+
/** How far ahead to book events, in ms. Default 100. */
|
|
233
|
+
lookahead?: number;
|
|
234
|
+
/** Time between passes, in ms. Default 25. */
|
|
235
|
+
interval?: number;
|
|
236
|
+
}
|
|
237
|
+
interface Scheduler {
|
|
238
|
+
start(): void;
|
|
239
|
+
/** Stop the timer. The queue survives; `clear()` empties it. */
|
|
240
|
+
stop(): void;
|
|
241
|
+
/** Book `fire` for engine time `when`. It runs on the first pass whose
|
|
242
|
+
* lookahead window reaches it, receiving `when` so it can hand the true
|
|
243
|
+
* time to `source.start()` rather than "now". */
|
|
244
|
+
schedule(when: number, fire: (when: number) => void, key?: string): void;
|
|
245
|
+
/** Drop queued events with this key. A pass takes its whole batch out of the
|
|
246
|
+
* queue before firing any of it, so a callback cannot cancel a sibling that
|
|
247
|
+
* came due alongside it. */
|
|
248
|
+
cancelKey(key: string): void;
|
|
249
|
+
/** Drop every queued event. Without this, a stop/start cycle fires
|
|
250
|
+
* everything that came due meanwhile in one burst on the first pass. */
|
|
251
|
+
clear(): void;
|
|
252
|
+
pending(): number;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Lookahead scheduler. Each pass fires everything due within `lookahead` ms,
|
|
256
|
+
* in time order, handing each callback its own scheduled time.
|
|
257
|
+
*
|
|
258
|
+
* It runs on its own timer rather than on an animation frame, which stops
|
|
259
|
+
* entirely when nothing is animating. The timer is one-shot: the pass re-arms
|
|
260
|
+
* it at the end, so passes cannot overlap. A hidden tab clamps it to at least a
|
|
261
|
+
* second, which the lookahead cannot cover — see the README.
|
|
262
|
+
*/
|
|
263
|
+
declare function createScheduler(opts: SchedulerOptions): Scheduler;
|
|
264
|
+
|
|
265
|
+
export { type Acquisition, type AnalyserTap, type AnalyserTapOptions, type AudioEngine, type AudioEngineOptions, type BusGraph, type BusHandle, type PlayOptions, type Scheduler, type SchedulerOptions, type SoundCache, type SoundHandle, type SpatialOptions, type StealPolicy, type Vec2, type VoiceHandle, type VoicePool, type VoicePoolOptions, type VoiceRecord, createAnalyserTap, createAudioEngine, createBusGraph, createScheduler, createSoundCache, createVoicePool, spatialize };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
// src/analyser.ts
|
|
2
|
+
function createAnalyserTap(ctx, source, opts = {}) {
|
|
3
|
+
const node = ctx.createAnalyser();
|
|
4
|
+
node.fftSize = opts.fftSize ?? 2048;
|
|
5
|
+
source.connect(node);
|
|
6
|
+
let attached = true;
|
|
7
|
+
let freqScratch = new Uint8Array(0);
|
|
8
|
+
let timeScratch = new Uint8Array(0);
|
|
9
|
+
const freqBuf = () => {
|
|
10
|
+
if (freqScratch.length !== node.frequencyBinCount) {
|
|
11
|
+
freqScratch = new Uint8Array(node.frequencyBinCount);
|
|
12
|
+
}
|
|
13
|
+
return freqScratch;
|
|
14
|
+
};
|
|
15
|
+
const timeBuf = () => {
|
|
16
|
+
if (timeScratch.length !== node.fftSize) timeScratch = new Uint8Array(node.fftSize);
|
|
17
|
+
return timeScratch;
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
node,
|
|
21
|
+
frequencies(out) {
|
|
22
|
+
const target = out ?? new Uint8Array(node.frequencyBinCount);
|
|
23
|
+
node.getByteFrequencyData(target);
|
|
24
|
+
return target;
|
|
25
|
+
},
|
|
26
|
+
waveform(out) {
|
|
27
|
+
const target = out ?? new Uint8Array(node.fftSize);
|
|
28
|
+
node.getByteTimeDomainData(target);
|
|
29
|
+
return target;
|
|
30
|
+
},
|
|
31
|
+
level() {
|
|
32
|
+
const buf = timeBuf();
|
|
33
|
+
node.getByteTimeDomainData(buf);
|
|
34
|
+
let sum = 0;
|
|
35
|
+
for (let i = 0; i < buf.length; i += 1) {
|
|
36
|
+
const v = (buf[i] - 128) / 128;
|
|
37
|
+
sum += v * v;
|
|
38
|
+
}
|
|
39
|
+
return Math.sqrt(sum / buf.length);
|
|
40
|
+
},
|
|
41
|
+
bands(n, out) {
|
|
42
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
43
|
+
throw new Error("@weasel-js/audio: bands(n) requires a positive integer");
|
|
44
|
+
}
|
|
45
|
+
if (out && out.length !== n) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`@weasel-js/audio: bands(${n}, out) requires an out array of length ${n}, got ${out.length}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const buf = freqBuf();
|
|
51
|
+
node.getByteFrequencyData(buf);
|
|
52
|
+
const target = out ?? new Float32Array(n);
|
|
53
|
+
const per = buf.length / n;
|
|
54
|
+
for (let b = 0; b < n; b += 1) {
|
|
55
|
+
const lo = Math.floor(b * per);
|
|
56
|
+
const hi = Math.max(lo + 1, Math.floor((b + 1) * per));
|
|
57
|
+
let sum = 0;
|
|
58
|
+
for (let i = lo; i < hi; i += 1) sum += buf[i];
|
|
59
|
+
target[b] = sum / (hi - lo) / 255;
|
|
60
|
+
}
|
|
61
|
+
return target;
|
|
62
|
+
},
|
|
63
|
+
// `node.disconnect()` would cut the analyser's outgoing edges, and it has
|
|
64
|
+
// none: the edge that keeps it alive is the source's.
|
|
65
|
+
dispose() {
|
|
66
|
+
if (!attached) return;
|
|
67
|
+
attached = false;
|
|
68
|
+
source.disconnect(node);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/param.ts
|
|
74
|
+
function writeParam(ctx, param, value, rampMs) {
|
|
75
|
+
const now = ctx.currentTime;
|
|
76
|
+
param.cancelScheduledValues(now);
|
|
77
|
+
if (rampMs && rampMs > 0) {
|
|
78
|
+
param.setValueAtTime(param.value, now);
|
|
79
|
+
param.linearRampToValueAtTime(value, now + rampMs / 1e3);
|
|
80
|
+
} else {
|
|
81
|
+
param.setValueAtTime(value, now);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/buses.ts
|
|
86
|
+
function createBusGraph(ctx, names) {
|
|
87
|
+
const master = ctx.createGain();
|
|
88
|
+
master.connect(ctx.destination);
|
|
89
|
+
const states = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const name of names) {
|
|
91
|
+
const node = ctx.createGain();
|
|
92
|
+
node.connect(master);
|
|
93
|
+
states.set(name, { node, gain: 1, muted: false, soloed: false });
|
|
94
|
+
}
|
|
95
|
+
const anySoloed = () => {
|
|
96
|
+
for (const s of states.values()) if (s.soloed) return true;
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
const audibleUnder = (s, soloing) => !s.muted && (!soloing || s.soloed);
|
|
100
|
+
const apply = (ramp) => {
|
|
101
|
+
const soloing = anySoloed();
|
|
102
|
+
for (const s of states.values()) {
|
|
103
|
+
const value = audibleUnder(s, soloing) ? s.gain : 0;
|
|
104
|
+
writeParam(ctx, s.node.gain, value, ramp?.bus === s ? ramp.ms : void 0);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const get = (name) => {
|
|
108
|
+
const s = states.get(name);
|
|
109
|
+
if (!s) throw new Error(`@weasel-js/audio: unknown bus "${name}"`);
|
|
110
|
+
return s;
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
master,
|
|
114
|
+
node: (name) => get(name).node,
|
|
115
|
+
names: () => [...states.keys()],
|
|
116
|
+
bus(name) {
|
|
117
|
+
const s = get(name);
|
|
118
|
+
return {
|
|
119
|
+
setGain(value, rampMs) {
|
|
120
|
+
s.gain = value;
|
|
121
|
+
apply(rampMs && rampMs > 0 ? { bus: s, ms: rampMs } : void 0);
|
|
122
|
+
},
|
|
123
|
+
mute(on) {
|
|
124
|
+
s.muted = on;
|
|
125
|
+
apply();
|
|
126
|
+
},
|
|
127
|
+
solo(on) {
|
|
128
|
+
s.soloed = on;
|
|
129
|
+
apply();
|
|
130
|
+
},
|
|
131
|
+
gain: () => s.gain,
|
|
132
|
+
muted: () => s.muted,
|
|
133
|
+
soloed: () => s.soloed,
|
|
134
|
+
audible: () => audibleUnder(s, anySoloed())
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/scheduler.ts
|
|
141
|
+
function createScheduler(opts) {
|
|
142
|
+
const lookahead = opts.lookahead ?? 100;
|
|
143
|
+
const interval = opts.interval ?? 25;
|
|
144
|
+
let queue = [];
|
|
145
|
+
let running = false;
|
|
146
|
+
let handle = null;
|
|
147
|
+
const pass = () => {
|
|
148
|
+
const horizon = opts.now() + lookahead;
|
|
149
|
+
const due = [];
|
|
150
|
+
const rest = [];
|
|
151
|
+
for (const entry of queue) (entry.when <= horizon ? due : rest).push(entry);
|
|
152
|
+
queue = rest;
|
|
153
|
+
due.sort((a, b) => a.when - b.when);
|
|
154
|
+
for (const entry of due) {
|
|
155
|
+
try {
|
|
156
|
+
entry.fire(entry.when);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error("@weasel-js/audio scheduler: callback threw", err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (running) handle = opts.setTimer(pass, interval);
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
start() {
|
|
165
|
+
if (running) return;
|
|
166
|
+
running = true;
|
|
167
|
+
handle = opts.setTimer(pass, interval);
|
|
168
|
+
},
|
|
169
|
+
stop() {
|
|
170
|
+
if (!running) return;
|
|
171
|
+
running = false;
|
|
172
|
+
opts.clearTimer(handle);
|
|
173
|
+
handle = null;
|
|
174
|
+
},
|
|
175
|
+
schedule(when, fire, key) {
|
|
176
|
+
queue.push({ when, fire, key });
|
|
177
|
+
},
|
|
178
|
+
cancelKey(key) {
|
|
179
|
+
queue = queue.filter((e) => e.key !== key);
|
|
180
|
+
},
|
|
181
|
+
clear() {
|
|
182
|
+
queue = [];
|
|
183
|
+
},
|
|
184
|
+
pending: () => queue.length
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/soundCache.ts
|
|
189
|
+
function createSoundCache(ctx, fetchFn = fetch) {
|
|
190
|
+
let counter = 0;
|
|
191
|
+
const buffers = /* @__PURE__ */ new Map();
|
|
192
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
193
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
194
|
+
const store = (buffer) => {
|
|
195
|
+
const id = `snd_${++counter}`;
|
|
196
|
+
buffers.set(id, buffer);
|
|
197
|
+
return { id };
|
|
198
|
+
};
|
|
199
|
+
const cache = {
|
|
200
|
+
async load(url) {
|
|
201
|
+
const existing = byUrl.get(url);
|
|
202
|
+
if (existing) return existing;
|
|
203
|
+
const pending = inflight.get(url);
|
|
204
|
+
if (pending) return pending;
|
|
205
|
+
const task = (async () => {
|
|
206
|
+
const res = await fetchFn(url);
|
|
207
|
+
if (!res.ok) throw new Error(`@weasel-js/audio: failed to load ${url} (${res.status})`);
|
|
208
|
+
const bytes = await res.arrayBuffer();
|
|
209
|
+
const buffer = await ctx.decodeAudioData(bytes);
|
|
210
|
+
const handle = store(buffer);
|
|
211
|
+
byUrl.set(url, handle);
|
|
212
|
+
return handle;
|
|
213
|
+
})();
|
|
214
|
+
inflight.set(url, task);
|
|
215
|
+
try {
|
|
216
|
+
return await task;
|
|
217
|
+
} finally {
|
|
218
|
+
inflight.delete(url);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
async loadAll(urls) {
|
|
222
|
+
const names = Object.keys(urls);
|
|
223
|
+
const handles = await Promise.all(names.map((n) => cache.load(urls[n])));
|
|
224
|
+
return Object.fromEntries(names.map((n, i) => [n, handles[i]]));
|
|
225
|
+
},
|
|
226
|
+
async decode(bytes) {
|
|
227
|
+
return store(await ctx.decodeAudioData(bytes));
|
|
228
|
+
},
|
|
229
|
+
register: (buffer) => store(buffer),
|
|
230
|
+
buffer: (handle) => buffers.get(handle.id) ?? null
|
|
231
|
+
};
|
|
232
|
+
return cache;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/spatialize.ts
|
|
236
|
+
function spatialize(source, listener, opts = {}) {
|
|
237
|
+
const refDistance = opts.refDistance ?? 1;
|
|
238
|
+
const maxDistance = opts.maxDistance ?? 1e4;
|
|
239
|
+
const rolloff = opts.rolloff ?? "inverse";
|
|
240
|
+
const rolloffFactor = opts.rolloffFactor ?? 1;
|
|
241
|
+
const panWidth = opts.panWidth ?? 500;
|
|
242
|
+
const dx = source.x - listener.x;
|
|
243
|
+
const dy = source.y - listener.y;
|
|
244
|
+
const distance = Math.hypot(dx, dy);
|
|
245
|
+
let gain;
|
|
246
|
+
if (distance <= refDistance) {
|
|
247
|
+
gain = 1;
|
|
248
|
+
} else if (rolloff === "linear") {
|
|
249
|
+
const span = maxDistance - refDistance;
|
|
250
|
+
gain = span <= 0 ? 0 : 1 - rolloffFactor * ((distance - refDistance) / span);
|
|
251
|
+
} else {
|
|
252
|
+
gain = refDistance / (refDistance + rolloffFactor * (distance - refDistance));
|
|
253
|
+
}
|
|
254
|
+
gain = Math.min(1, Math.max(0, gain));
|
|
255
|
+
const pan = panWidth <= 0 ? 0 : Math.min(1, Math.max(-1, dx / panWidth));
|
|
256
|
+
return { gain, pan };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/voicePool.ts
|
|
260
|
+
function createVoicePool(opts) {
|
|
261
|
+
if (!(opts.limit >= 1)) {
|
|
262
|
+
throw new RangeError(`@weasel-js/audio: voice pool limit must be at least 1, got ${opts.limit}`);
|
|
263
|
+
}
|
|
264
|
+
const steal = opts.steal ?? "oldest";
|
|
265
|
+
const live = /* @__PURE__ */ new Map();
|
|
266
|
+
const free = [];
|
|
267
|
+
let nextSlot = 0;
|
|
268
|
+
let nextToken = 1;
|
|
269
|
+
const victim = () => {
|
|
270
|
+
let worst = -1;
|
|
271
|
+
let worstScore = Infinity;
|
|
272
|
+
for (const [slot, rec] of live) {
|
|
273
|
+
const score = steal === "quietest" ? rec.gain : rec.startedAt;
|
|
274
|
+
if (score < worstScore) {
|
|
275
|
+
worstScore = score;
|
|
276
|
+
worst = slot;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return worst;
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
acquire(record) {
|
|
283
|
+
const token = nextToken++;
|
|
284
|
+
if (live.size < opts.limit) {
|
|
285
|
+
const slot2 = free.length > 0 ? free.pop() : nextSlot++;
|
|
286
|
+
live.set(slot2, { ...record, token });
|
|
287
|
+
return { slot: slot2, token, stolen: null };
|
|
288
|
+
}
|
|
289
|
+
const slot = victim();
|
|
290
|
+
const stolen = live.get(slot).token;
|
|
291
|
+
live.delete(slot);
|
|
292
|
+
live.set(slot, { ...record, token });
|
|
293
|
+
return { slot, token, stolen };
|
|
294
|
+
},
|
|
295
|
+
release(slot, token) {
|
|
296
|
+
if (live.get(slot)?.token !== token) return;
|
|
297
|
+
live.delete(slot);
|
|
298
|
+
free.push(slot);
|
|
299
|
+
},
|
|
300
|
+
setGain(slot, token, gain) {
|
|
301
|
+
const rec = live.get(slot);
|
|
302
|
+
if (rec?.token === token) rec.gain = gain;
|
|
303
|
+
},
|
|
304
|
+
active: () => live.size
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/createAudioEngine.ts
|
|
309
|
+
function createAudioEngine(opts = {}) {
|
|
310
|
+
const ctx = opts.context ?? new AudioContext();
|
|
311
|
+
const ownsContext = opts.context === void 0;
|
|
312
|
+
const busNames = opts.buses ?? ["sfx", "music", "ui"];
|
|
313
|
+
if (busNames.length === 0) {
|
|
314
|
+
throw new Error("@weasel-js/audio: an engine needs at least one bus");
|
|
315
|
+
}
|
|
316
|
+
const graph = createBusGraph(ctx, busNames);
|
|
317
|
+
const sounds = createSoundCache(ctx, opts.fetchFn);
|
|
318
|
+
const slotsByBus = /* @__PURE__ */ new Map();
|
|
319
|
+
for (const name of busNames) {
|
|
320
|
+
slotsByBus.set(name, {
|
|
321
|
+
pool: createVoicePool({ limit: opts.voiceLimit ?? 32, steal: opts.steal }),
|
|
322
|
+
byToken: /* @__PURE__ */ new Map()
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const slotsFor = (bus) => {
|
|
326
|
+
const s = slotsByBus.get(bus);
|
|
327
|
+
if (!s) throw new Error(`@weasel-js/audio: unknown bus "${bus}"`);
|
|
328
|
+
return s;
|
|
329
|
+
};
|
|
330
|
+
const now = () => ctx.currentTime * 1e3;
|
|
331
|
+
const scheduler = createScheduler({
|
|
332
|
+
now,
|
|
333
|
+
// One-shot, not repeating: the scheduler re-arms at the end of every pass,
|
|
334
|
+
// so an interval would leave the previous one running and double the live
|
|
335
|
+
// timer count per tick.
|
|
336
|
+
setTimer: opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms)),
|
|
337
|
+
clearTimer: opts.clearTimer ?? ((h) => clearTimeout(h)),
|
|
338
|
+
lookahead: opts.lookahead,
|
|
339
|
+
interval: opts.tickInterval
|
|
340
|
+
});
|
|
341
|
+
scheduler.start();
|
|
342
|
+
let listener = { x: 0, y: 0 };
|
|
343
|
+
let spatialOpts = {};
|
|
344
|
+
let nextVoiceId = 1;
|
|
345
|
+
let disposed = false;
|
|
346
|
+
let suspendedSince = false;
|
|
347
|
+
let staleFloor = -Infinity;
|
|
348
|
+
const live = /* @__PURE__ */ new Map();
|
|
349
|
+
const taps = /* @__PURE__ */ new Set();
|
|
350
|
+
let warnedLocked = false;
|
|
351
|
+
const warnLocked = () => {
|
|
352
|
+
if (warnedLocked) return;
|
|
353
|
+
warnedLocked = true;
|
|
354
|
+
console.warn(
|
|
355
|
+
"@weasel-js/audio: play() before the AudioContext was unlocked \u2014 the voice was dropped. Browsers require a user gesture; call engine.unlock() from one, or wait for the automatic gesture listener."
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
let warnedDisposed = false;
|
|
359
|
+
const warnDisposed = () => {
|
|
360
|
+
if (warnedDisposed) return;
|
|
361
|
+
warnedDisposed = true;
|
|
362
|
+
console.warn("@weasel-js/audio: play() after dispose() \u2014 the voice was dropped.");
|
|
363
|
+
};
|
|
364
|
+
let warnedUnknownSound = false;
|
|
365
|
+
const warnUnknownSound = (id) => {
|
|
366
|
+
if (warnedUnknownSound) return;
|
|
367
|
+
warnedUnknownSound = true;
|
|
368
|
+
console.warn(
|
|
369
|
+
`@weasel-js/audio: play() was handed sound "${id}", which this engine never loaded \u2014 the voice was dropped. A handle belongs to the engine that produced it.`
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
const gestures = ["pointerdown", "keydown", "touchstart"];
|
|
373
|
+
const onGesture = () => {
|
|
374
|
+
void engine.unlock();
|
|
375
|
+
};
|
|
376
|
+
const armGestures = () => {
|
|
377
|
+
if (typeof window === "undefined") return;
|
|
378
|
+
for (const g of gestures) window.addEventListener(g, onGesture, { once: true, passive: true });
|
|
379
|
+
};
|
|
380
|
+
const disarmGestures = () => {
|
|
381
|
+
if (typeof window === "undefined") return;
|
|
382
|
+
for (const g of gestures) window.removeEventListener(g, onGesture);
|
|
383
|
+
};
|
|
384
|
+
armGestures();
|
|
385
|
+
const onStateChange = () => {
|
|
386
|
+
if (disposed) return;
|
|
387
|
+
if (ctx.state === "running") {
|
|
388
|
+
if (suspendedSince) staleFloor = now();
|
|
389
|
+
suspendedSince = false;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
suspendedSince = true;
|
|
393
|
+
warnedLocked = false;
|
|
394
|
+
armGestures();
|
|
395
|
+
};
|
|
396
|
+
ctx.addEventListener("statechange", onStateChange);
|
|
397
|
+
const poolGain = (voice) => {
|
|
398
|
+
if (voice.slot === null) return;
|
|
399
|
+
voice.slots.pool.setGain(voice.slot, voice.token, voice.baseGain * voice.spatialGain);
|
|
400
|
+
};
|
|
401
|
+
const applySpatial = (voice) => {
|
|
402
|
+
if (!voice.position) return;
|
|
403
|
+
const s = spatialize(voice.position, listener, spatialOpts);
|
|
404
|
+
voice.spatialGain = s.gain;
|
|
405
|
+
voice.panNode.pan.value = s.pan;
|
|
406
|
+
writeParam(ctx, voice.gainNode.gain, voice.baseGain * s.gain);
|
|
407
|
+
poolGain(voice);
|
|
408
|
+
};
|
|
409
|
+
const teardown = (voice) => {
|
|
410
|
+
voice.cancelled = true;
|
|
411
|
+
if (!voice.playing && voice.source === null) return;
|
|
412
|
+
voice.playing = false;
|
|
413
|
+
if (voice.source) {
|
|
414
|
+
try {
|
|
415
|
+
voice.source.stop();
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
voice.source.disconnect();
|
|
419
|
+
}
|
|
420
|
+
voice.source = null;
|
|
421
|
+
voice.panNode.disconnect();
|
|
422
|
+
voice.gainNode.disconnect();
|
|
423
|
+
live.delete(voice.id);
|
|
424
|
+
if (voice.slot !== null) {
|
|
425
|
+
voice.slots.byToken.delete(voice.token);
|
|
426
|
+
voice.slots.pool.release(voice.slot, voice.token);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const dropped = (id) => ({
|
|
430
|
+
id,
|
|
431
|
+
stop: () => {
|
|
432
|
+
},
|
|
433
|
+
setGain: () => {
|
|
434
|
+
},
|
|
435
|
+
setRate: () => {
|
|
436
|
+
},
|
|
437
|
+
setDetune: () => {
|
|
438
|
+
},
|
|
439
|
+
setPan: () => {
|
|
440
|
+
},
|
|
441
|
+
setPosition: () => {
|
|
442
|
+
},
|
|
443
|
+
isPlaying: () => false
|
|
444
|
+
});
|
|
445
|
+
const engine = {
|
|
446
|
+
context: ctx,
|
|
447
|
+
state: () => ctx.state,
|
|
448
|
+
async unlock() {
|
|
449
|
+
if (disposed) return;
|
|
450
|
+
if (ctx.state !== "running") await ctx.resume();
|
|
451
|
+
},
|
|
452
|
+
now,
|
|
453
|
+
load: sounds.load,
|
|
454
|
+
loadAll: sounds.loadAll,
|
|
455
|
+
decode: sounds.decode,
|
|
456
|
+
register: sounds.register,
|
|
457
|
+
play(sound, playOpts = {}) {
|
|
458
|
+
const id = nextVoiceId++;
|
|
459
|
+
if (disposed) {
|
|
460
|
+
warnDisposed();
|
|
461
|
+
return dropped(id);
|
|
462
|
+
}
|
|
463
|
+
if (ctx.state !== "running") {
|
|
464
|
+
warnLocked();
|
|
465
|
+
return dropped(id);
|
|
466
|
+
}
|
|
467
|
+
const busName = playOpts.bus ?? busNames[0];
|
|
468
|
+
const slots = slotsFor(busName);
|
|
469
|
+
const when = playOpts.when ?? now();
|
|
470
|
+
const baseGain = playOpts.gain ?? 1;
|
|
471
|
+
const spatial = playOpts.position ? spatialize(playOpts.position, listener, spatialOpts) : { gain: 1, pan: playOpts.pan ?? 0 };
|
|
472
|
+
const gainNode = ctx.createGain();
|
|
473
|
+
const panNode = ctx.createStereoPanner();
|
|
474
|
+
gainNode.gain.value = baseGain * spatial.gain;
|
|
475
|
+
panNode.pan.value = spatial.pan;
|
|
476
|
+
panNode.connect(gainNode);
|
|
477
|
+
gainNode.connect(graph.node(busName));
|
|
478
|
+
const voice = {
|
|
479
|
+
id,
|
|
480
|
+
slots,
|
|
481
|
+
slot: null,
|
|
482
|
+
token: 0,
|
|
483
|
+
key: playOpts.cancelKey,
|
|
484
|
+
source: null,
|
|
485
|
+
gainNode,
|
|
486
|
+
panNode,
|
|
487
|
+
baseGain,
|
|
488
|
+
spatialGain: spatial.gain,
|
|
489
|
+
rate: playOpts.rate ?? 1,
|
|
490
|
+
detune: playOpts.detune ?? 0,
|
|
491
|
+
position: playOpts.position,
|
|
492
|
+
playing: true,
|
|
493
|
+
cancelled: false
|
|
494
|
+
};
|
|
495
|
+
live.set(id, voice);
|
|
496
|
+
scheduler.schedule(when, (scheduledWhen) => {
|
|
497
|
+
if (voice.cancelled) return;
|
|
498
|
+
if (scheduledWhen < staleFloor) {
|
|
499
|
+
teardown(voice);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const buffer = sounds.buffer(sound);
|
|
503
|
+
if (!buffer) {
|
|
504
|
+
warnUnknownSound(sound.id);
|
|
505
|
+
teardown(voice);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const acquired = slots.pool.acquire({
|
|
509
|
+
startedAt: scheduledWhen,
|
|
510
|
+
gain: voice.baseGain * voice.spatialGain
|
|
511
|
+
});
|
|
512
|
+
voice.slot = acquired.slot;
|
|
513
|
+
voice.token = acquired.token;
|
|
514
|
+
slots.byToken.set(acquired.token, voice);
|
|
515
|
+
if (acquired.stolen !== null) {
|
|
516
|
+
const victim = slots.byToken.get(acquired.stolen);
|
|
517
|
+
if (victim) teardown(victim);
|
|
518
|
+
}
|
|
519
|
+
const source = ctx.createBufferSource();
|
|
520
|
+
source.buffer = buffer;
|
|
521
|
+
source.loop = playOpts.loop ?? false;
|
|
522
|
+
source.playbackRate.value = voice.rate;
|
|
523
|
+
source.detune.value = voice.detune;
|
|
524
|
+
source.connect(panNode);
|
|
525
|
+
source.onended = () => {
|
|
526
|
+
teardown(voice);
|
|
527
|
+
playOpts.onDone?.();
|
|
528
|
+
};
|
|
529
|
+
voice.source = source;
|
|
530
|
+
source.start(scheduledWhen / 1e3);
|
|
531
|
+
}, playOpts.cancelKey);
|
|
532
|
+
return {
|
|
533
|
+
id,
|
|
534
|
+
stop(fadeMs) {
|
|
535
|
+
if (fadeMs && fadeMs > 0 && voice.source && !voice.cancelled) {
|
|
536
|
+
voice.cancelled = true;
|
|
537
|
+
writeParam(ctx, gainNode.gain, 0, fadeMs);
|
|
538
|
+
try {
|
|
539
|
+
voice.source.stop(ctx.currentTime + fadeMs / 1e3);
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
teardown(voice);
|
|
545
|
+
},
|
|
546
|
+
setGain(value, rampMs) {
|
|
547
|
+
voice.baseGain = value;
|
|
548
|
+
writeParam(ctx, gainNode.gain, value * voice.spatialGain, rampMs);
|
|
549
|
+
poolGain(voice);
|
|
550
|
+
},
|
|
551
|
+
setRate(value) {
|
|
552
|
+
voice.rate = value;
|
|
553
|
+
if (voice.source) voice.source.playbackRate.value = value;
|
|
554
|
+
},
|
|
555
|
+
setDetune(cents) {
|
|
556
|
+
voice.detune = cents;
|
|
557
|
+
if (voice.source) voice.source.detune.value = cents;
|
|
558
|
+
},
|
|
559
|
+
setPan(value) {
|
|
560
|
+
voice.position = void 0;
|
|
561
|
+
panNode.pan.value = value;
|
|
562
|
+
},
|
|
563
|
+
setPosition(p) {
|
|
564
|
+
voice.position = p;
|
|
565
|
+
applySpatial(voice);
|
|
566
|
+
},
|
|
567
|
+
isPlaying: () => voice.playing
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
stopKey(key) {
|
|
571
|
+
scheduler.cancelKey(key);
|
|
572
|
+
for (const voice of [...live.values()]) {
|
|
573
|
+
if (voice.key === key) teardown(voice);
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
stopAll() {
|
|
577
|
+
for (const voice of [...live.values()]) teardown(voice);
|
|
578
|
+
},
|
|
579
|
+
bus: graph.bus,
|
|
580
|
+
busNames: graph.names,
|
|
581
|
+
activeVoices(bus) {
|
|
582
|
+
if (bus !== void 0) return slotsFor(bus).pool.active();
|
|
583
|
+
let total = 0;
|
|
584
|
+
for (const s of slotsByBus.values()) total += s.pool.active();
|
|
585
|
+
return total;
|
|
586
|
+
},
|
|
587
|
+
analyser(busName, tapOpts) {
|
|
588
|
+
const tap = createAnalyserTap(ctx, busName ? graph.node(busName) : graph.master, tapOpts);
|
|
589
|
+
taps.add(tap);
|
|
590
|
+
return {
|
|
591
|
+
...tap,
|
|
592
|
+
dispose() {
|
|
593
|
+
taps.delete(tap);
|
|
594
|
+
tap.dispose();
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
},
|
|
598
|
+
setListener(p, o) {
|
|
599
|
+
listener = p;
|
|
600
|
+
if (o) spatialOpts = o;
|
|
601
|
+
for (const voice of live.values()) applySpatial(voice);
|
|
602
|
+
},
|
|
603
|
+
dispose() {
|
|
604
|
+
if (disposed) return;
|
|
605
|
+
disposed = true;
|
|
606
|
+
engine.stopAll();
|
|
607
|
+
scheduler.stop();
|
|
608
|
+
scheduler.clear();
|
|
609
|
+
for (const tap of [...taps]) tap.dispose();
|
|
610
|
+
taps.clear();
|
|
611
|
+
disarmGestures();
|
|
612
|
+
ctx.removeEventListener("statechange", onStateChange);
|
|
613
|
+
graph.master.disconnect();
|
|
614
|
+
if (ownsContext) void ctx.close().catch(() => void 0);
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
return engine;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export { createAnalyserTap, createAudioEngine, createBusGraph, createScheduler, createSoundCache, createVoicePool, spatialize };
|
|
621
|
+
//# sourceMappingURL=index.js.map
|
|
622
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/analyser.ts","../src/param.ts","../src/buses.ts","../src/scheduler.ts","../src/soundCache.ts","../src/spatialize.ts","../src/voicePool.ts","../src/createAudioEngine.ts"],"names":["slot"],"mappings":"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","file":"index.js","sourcesContent":["export interface AnalyserTapOptions {\n /** Power of two, 32..32768. Default 2048. */\n fftSize?: number;\n}\n\nexport interface AnalyserTap {\n /** The underlying node, exposed for disposal assertions and advanced wiring. */\n node: AnalyserNode;\n /** Frequency data, sized `node.frequencyBinCount` when no array is given. */\n frequencies(out?: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;\n /** Time-domain data, sized `node.fftSize` when no array is given — twice the\n * frequency bin count, which is the whole window rather than half of it. */\n waveform(out?: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;\n /** RMS amplitude over the whole time-domain window, 0..1. */\n level(): number;\n /** `n` averaged frequency bands normalized to 0..1 — the ergonomic form for\n * driving a shader uniform, a vertex color, or a pose. `out`, when given,\n * must be exactly `n` long. */\n bands(n: number, out?: Float32Array<ArrayBuffer>): Float32Array<ArrayBuffer>;\n /** Detach from the tapped node. Idempotent. */\n dispose(): void;\n}\n\nexport function createAnalyserTap(\n ctx: AudioContext,\n source: AudioNode,\n opts: AnalyserTapOptions = {},\n): AnalyserTap {\n const node = ctx.createAnalyser();\n node.fftSize = opts.fftSize ?? 2048;\n source.connect(node);\n let attached = true;\n\n // `node` is public surface, so `fftSize` can change under the tap. Time-domain\n // data is sized by `fftSize`; frequency data by half of it.\n let freqScratch = new Uint8Array(0);\n let timeScratch = new Uint8Array(0);\n const freqBuf = (): Uint8Array<ArrayBuffer> => {\n if (freqScratch.length !== node.frequencyBinCount) {\n freqScratch = new Uint8Array(node.frequencyBinCount);\n }\n return freqScratch;\n };\n const timeBuf = (): Uint8Array<ArrayBuffer> => {\n if (timeScratch.length !== node.fftSize) timeScratch = new Uint8Array(node.fftSize);\n return timeScratch;\n };\n\n return {\n node,\n frequencies(out) {\n const target = out ?? new Uint8Array(node.frequencyBinCount);\n node.getByteFrequencyData(target);\n return target;\n },\n waveform(out) {\n const target = out ?? new Uint8Array(node.fftSize);\n node.getByteTimeDomainData(target);\n return target;\n },\n level() {\n const buf = timeBuf();\n node.getByteTimeDomainData(buf);\n let sum = 0;\n for (let i = 0; i < buf.length; i += 1) {\n // Time-domain bytes center on 128; subtract to get a signed sample.\n const v = (buf[i] - 128) / 128;\n sum += v * v;\n }\n return Math.sqrt(sum / buf.length);\n },\n bands(n, out) {\n if (!Number.isInteger(n) || n < 1) {\n throw new Error('@weasel-js/audio: bands(n) requires a positive integer');\n }\n // Unlike the byte readers, a short `out` here would return fewer bands\n // than asked for with nothing to distinguish it from a correct result.\n if (out && out.length !== n) {\n throw new Error(\n `@weasel-js/audio: bands(${n}, out) requires an out array of length ${n}, got ${out.length}`,\n );\n }\n const buf = freqBuf();\n node.getByteFrequencyData(buf);\n const target = out ?? new Float32Array(n);\n const per = buf.length / n;\n for (let b = 0; b < n; b += 1) {\n const lo = Math.floor(b * per);\n const hi = Math.max(lo + 1, Math.floor((b + 1) * per));\n let sum = 0;\n for (let i = lo; i < hi; i += 1) sum += buf[i];\n target[b] = sum / (hi - lo) / 255;\n }\n return target;\n },\n // `node.disconnect()` would cut the analyser's outgoing edges, and it has\n // none: the edge that keeps it alive is the source's.\n dispose() {\n if (!attached) return;\n attached = false;\n source.disconnect(node);\n },\n };\n}\n","/**\n * Every gain write in the package goes through here.\n *\n * Two traps, both silent: a bare `param.value = x` sits *behind* any scheduled\n * ramp, which keeps running and wins; and a ramp with no anchor interpolates\n * from the previous scheduled endpoint rather than from the value the param\n * holds now, so a second ramp jumps before it moves.\n */\nexport function writeParam(\n ctx: BaseAudioContext,\n param: AudioParam,\n value: number,\n rampMs?: number,\n): void {\n const now = ctx.currentTime;\n param.cancelScheduledValues(now);\n if (rampMs && rampMs > 0) {\n param.setValueAtTime(param.value, now);\n param.linearRampToValueAtTime(value, now + rampMs / 1000);\n } else {\n param.setValueAtTime(value, now);\n }\n}\n","import { writeParam } from './param';\n\nexport interface BusHandle {\n setGain(value: number, rampMs?: number): void;\n mute(on: boolean): void;\n solo(on: boolean): void;\n /** The bus's own gain, as last set — not what a mute or solo is imposing. */\n gain(): number;\n /** The mute flag as set, independent of any solo. */\n muted(): boolean;\n /** The solo flag as set; whether it silences anything is a graph-wide answer. */\n soloed(): boolean;\n /** Whether the bus is being heard: unmuted, and soloed if any bus is soloed.\n * Not derivable from `muted()` and `soloed()` alone. */\n audible(): boolean;\n}\n\nexport interface BusGraph {\n master: GainNode;\n node(name: string): GainNode;\n bus(name: string): BusHandle;\n names(): string[];\n}\n\ninterface BusState { node: GainNode; gain: number; muted: boolean; soloed: boolean }\n\n/**\n * The mix graph: one `GainNode` per named bus, all routed to a master that\n * routes to the destination.\n *\n * Gain, mute and solo are three inputs to one output value rather than three\n * things that write the node directly — otherwise unmuting restores the wrong\n * value whenever a solo changed it in between. All three write through\n * `writeParam`, so `rampMs` survives instead of being overwritten by the\n * recomputation that follows it.\n */\nexport function createBusGraph(ctx: AudioContext, names: string[]): BusGraph {\n const master = ctx.createGain();\n master.connect(ctx.destination);\n\n const states = new Map<string, BusState>();\n for (const name of names) {\n const node = ctx.createGain();\n node.connect(master);\n states.set(name, { node, gain: 1, muted: false, soloed: false });\n }\n\n const anySoloed = (): boolean => {\n for (const s of states.values()) if (s.soloed) return true;\n return false;\n };\n\n const audibleUnder = (s: BusState, soloing: boolean): boolean =>\n !s.muted && (!soloing || s.soloed);\n\n // `ramp` names the one bus whose own value changed, so a solo does not slew\n // every other bus at whatever rate that bus was last set with.\n const apply = (ramp?: { bus: BusState; ms: number }): void => {\n const soloing = anySoloed();\n for (const s of states.values()) {\n const value = audibleUnder(s, soloing) ? s.gain : 0;\n writeParam(ctx, s.node.gain, value, ramp?.bus === s ? ramp.ms : undefined);\n }\n };\n\n const get = (name: string): BusState => {\n const s = states.get(name);\n if (!s) throw new Error(`@weasel-js/audio: unknown bus \"${name}\"`);\n return s;\n };\n\n return {\n master,\n node: (name) => get(name).node,\n names: () => [...states.keys()],\n bus(name) {\n const s = get(name);\n return {\n setGain(value, rampMs) {\n s.gain = value;\n apply(rampMs && rampMs > 0 ? { bus: s, ms: rampMs } : undefined);\n },\n mute(on) { s.muted = on; apply(); },\n solo(on) { s.soloed = on; apply(); },\n gain: () => s.gain,\n muted: () => s.muted,\n soloed: () => s.soloed,\n audible: () => audibleUnder(s, anySoloed()),\n };\n },\n };\n}\n","export interface SchedulerOptions {\n /** Engine time in ms. Backed by `AudioContext.currentTime * 1000` in production. */\n now: () => number;\n /** One-shot: it fires once and the pass re-arms it. `setInterval` here would\n * leave every previous timer running. */\n setTimer: (cb: () => void, ms: number) => unknown;\n clearTimer: (handle: unknown) => void;\n /** How far ahead to book events, in ms. Default 100. */\n lookahead?: number;\n /** Time between passes, in ms. Default 25. */\n interval?: number;\n}\n\ninterface Entry {\n when: number;\n fire: (when: number) => void;\n key?: string;\n}\n\nexport interface Scheduler {\n start(): void;\n /** Stop the timer. The queue survives; `clear()` empties it. */\n stop(): void;\n /** Book `fire` for engine time `when`. It runs on the first pass whose\n * lookahead window reaches it, receiving `when` so it can hand the true\n * time to `source.start()` rather than \"now\". */\n schedule(when: number, fire: (when: number) => void, key?: string): void;\n /** Drop queued events with this key. A pass takes its whole batch out of the\n * queue before firing any of it, so a callback cannot cancel a sibling that\n * came due alongside it. */\n cancelKey(key: string): void;\n /** Drop every queued event. Without this, a stop/start cycle fires\n * everything that came due meanwhile in one burst on the first pass. */\n clear(): void;\n pending(): number;\n}\n\n/**\n * Lookahead scheduler. Each pass fires everything due within `lookahead` ms,\n * in time order, handing each callback its own scheduled time.\n *\n * It runs on its own timer rather than on an animation frame, which stops\n * entirely when nothing is animating. The timer is one-shot: the pass re-arms\n * it at the end, so passes cannot overlap. A hidden tab clamps it to at least a\n * second, which the lookahead cannot cover — see the README.\n */\nexport function createScheduler(opts: SchedulerOptions): Scheduler {\n const lookahead = opts.lookahead ?? 100;\n const interval = opts.interval ?? 25;\n let queue: Entry[] = [];\n let running = false;\n let handle: unknown = null;\n\n const pass = (): void => {\n const horizon = opts.now() + lookahead;\n const due: Entry[] = [];\n const rest: Entry[] = [];\n for (const entry of queue) (entry.when <= horizon ? due : rest).push(entry);\n queue = rest;\n due.sort((a, b) => a.when - b.when);\n for (const entry of due) {\n try {\n entry.fire(entry.when);\n } catch (err) {\n console.error('@weasel-js/audio scheduler: callback threw', err);\n }\n }\n if (running) handle = opts.setTimer(pass, interval);\n };\n\n return {\n start() {\n if (running) return;\n running = true;\n handle = opts.setTimer(pass, interval);\n },\n stop() {\n if (!running) return;\n running = false;\n opts.clearTimer(handle);\n handle = null;\n },\n schedule(when, fire, key) {\n queue.push({ when, fire, key });\n },\n cancelKey(key) {\n queue = queue.filter((e) => e.key !== key);\n },\n clear() {\n queue = [];\n },\n pending: () => queue.length,\n };\n}\n","/** Opaque reference to a decoded sound. Mirrors `TextureHandle` in core. */\nexport interface SoundHandle { readonly id: string }\n\nexport interface SoundCache {\n load(url: string): Promise<SoundHandle>;\n loadAll(urls: Record<string, string>): Promise<Record<string, SoundHandle>>;\n decode(bytes: ArrayBuffer): Promise<SoundHandle>;\n /** Take ownership of a buffer the caller already has — a procedural synth,\n * an OfflineAudioContext render, a recording. Synchronous; nothing to decode. */\n register(buffer: AudioBuffer): SoundHandle;\n buffer(handle: SoundHandle): AudioBuffer | null;\n}\n\nexport function createSoundCache(\n ctx: AudioContext,\n fetchFn: typeof fetch = fetch,\n): SoundCache {\n let counter = 0;\n const buffers = new Map<string, AudioBuffer>();\n const byUrl = new Map<string, SoundHandle>();\n // In-flight loads, so two concurrent `load` calls for one url share a fetch.\n const inflight = new Map<string, Promise<SoundHandle>>();\n\n const store = (buffer: AudioBuffer): SoundHandle => {\n const id = `snd_${++counter}`;\n buffers.set(id, buffer);\n return { id };\n };\n\n const cache: SoundCache = {\n async load(url) {\n const existing = byUrl.get(url);\n if (existing) return existing;\n const pending = inflight.get(url);\n if (pending) return pending;\n\n const task = (async () => {\n const res = await fetchFn(url);\n if (!res.ok) throw new Error(`@weasel-js/audio: failed to load ${url} (${res.status})`);\n const bytes = await res.arrayBuffer();\n const buffer = await ctx.decodeAudioData(bytes);\n const handle = store(buffer);\n byUrl.set(url, handle);\n return handle;\n })();\n\n inflight.set(url, task);\n try {\n return await task;\n } finally {\n // Always clear: a failed load must not be cached, or a retry can never\n // succeed for the lifetime of the page.\n inflight.delete(url);\n }\n },\n async loadAll(urls) {\n const names = Object.keys(urls);\n const handles = await Promise.all(names.map((n) => cache.load(urls[n])));\n return Object.fromEntries(names.map((n, i) => [n, handles[i]]));\n },\n async decode(bytes) {\n return store(await ctx.decodeAudioData(bytes));\n },\n register: (buffer) => store(buffer),\n buffer: (handle) => buffers.get(handle.id) ?? null,\n };\n return cache;\n}\n","export interface Vec2 { x: number; y: number }\n\nexport interface SpatialOptions {\n /** Distance within which gain stays at 1. Default 1. The inverse model needs\n * this above 0: at 0 it cliffs gain from 1 to 0 at any nonzero distance. */\n refDistance?: number;\n /** Distance at which linear rolloff reaches 0. Ignored by inverse. Default\n * 10000. At or below `refDistance` it gives the same 1-to-0 cliff, there. */\n maxDistance?: number;\n /** Default 'inverse' — the natural-sounding one. */\n rolloff?: 'inverse' | 'linear';\n /** How sharply gain falls. Default 1. */\n rolloffFactor?: number;\n /** Horizontal distance mapping to full left/right. Default 500. */\n panWidth?: number;\n}\n\n/**\n * Map a source position to a gain and a stereo pan, relative to the listener.\n *\n * Pure — no Web Audio, no state. This is the whole spatial model.\n */\nexport function spatialize(\n source: Vec2,\n listener: Vec2,\n opts: SpatialOptions = {},\n): { gain: number; pan: number } {\n const refDistance = opts.refDistance ?? 1;\n const maxDistance = opts.maxDistance ?? 10000;\n const rolloff = opts.rolloff ?? 'inverse';\n const rolloffFactor = opts.rolloffFactor ?? 1;\n const panWidth = opts.panWidth ?? 500;\n\n const dx = source.x - listener.x;\n const dy = source.y - listener.y;\n const distance = Math.hypot(dx, dy);\n\n let gain: number;\n if (distance <= refDistance) {\n gain = 1;\n } else if (rolloff === 'linear') {\n const span = maxDistance - refDistance;\n gain = span <= 0 ? 0 : 1 - rolloffFactor * ((distance - refDistance) / span);\n } else {\n gain = refDistance / (refDistance + rolloffFactor * (distance - refDistance));\n }\n gain = Math.min(1, Math.max(0, gain));\n\n const pan = panWidth <= 0 ? 0 : Math.min(1, Math.max(-1, dx / panWidth));\n\n return { gain, pan };\n}\n","export type StealPolicy = 'oldest' | 'quietest';\n\nexport interface VoicePoolOptions {\n /** Maximum concurrent voices, at least 1. Beyond this, `acquire` steals. */\n limit: number;\n /** Default 'oldest'. */\n steal?: StealPolicy;\n}\n\nexport interface VoiceRecord {\n /** Engine time the voice started, in ms. */\n startedAt: number;\n /** Current gain, consulted by the 'quietest' steal policy. */\n gain: number;\n}\n\nexport interface Acquisition {\n slot: number;\n /** Identifies this voice, not its slot: a stolen slot is reissued at once,\n * so `release` and `setGain` take the token and ignore a stale one. */\n token: number;\n /** Token of the voice evicted to make room, or null. The caller is\n * responsible for actually stopping that voice's nodes. */\n stolen: number | null;\n}\n\nexport interface VoicePool {\n acquire(record: VoiceRecord): Acquisition;\n release(slot: number, token: number): void;\n setGain(slot: number, token: number, gain: number): void;\n active(): number;\n}\n\n/**\n * Slot bookkeeping for concurrent voices, with a steal policy for when the\n * limit is reached.\n *\n * This is pure accounting — it owns no audio nodes. The caller builds the\n * `GainNode`/`StereoPannerNode` chain and mints a fresh `AudioBufferSourceNode`\n * per play, because a source node is single-use by specification and cannot be\n * restarted once stopped. Nothing is pooled but the slot numbers.\n */\nexport function createVoicePool(opts: VoicePoolOptions): VoicePool {\n if (!(opts.limit >= 1)) {\n throw new RangeError(`@weasel-js/audio: voice pool limit must be at least 1, got ${opts.limit}`);\n }\n const steal = opts.steal ?? 'oldest';\n const live = new Map<number, VoiceRecord & { token: number }>();\n const free: number[] = [];\n let nextSlot = 0;\n let nextToken = 1;\n\n const victim = (): number => {\n let worst = -1;\n let worstScore = Infinity;\n for (const [slot, rec] of live) {\n const score = steal === 'quietest' ? rec.gain : rec.startedAt;\n if (score < worstScore) { worstScore = score; worst = slot; }\n }\n return worst;\n };\n\n return {\n acquire(record) {\n const token = nextToken++;\n if (live.size < opts.limit) {\n const slot = free.length > 0 ? free.pop()! : nextSlot++;\n live.set(slot, { ...record, token });\n return { slot, token, stolen: null };\n }\n const slot = victim();\n const stolen = live.get(slot)!.token;\n // Re-inserting moves the slot to the back of the iteration order, so\n // voices tied on score take turns being the victim.\n live.delete(slot);\n live.set(slot, { ...record, token });\n return { slot, token, stolen };\n },\n release(slot, token) {\n if (live.get(slot)?.token !== token) return;\n live.delete(slot);\n free.push(slot);\n },\n setGain(slot, token, gain) {\n const rec = live.get(slot);\n if (rec?.token === token) rec.gain = gain;\n },\n active: () => live.size,\n };\n}\n","import { createAnalyserTap, type AnalyserTap, type AnalyserTapOptions } from './analyser';\nimport { createBusGraph, type BusHandle } from './buses';\nimport { writeParam } from './param';\nimport { createScheduler } from './scheduler';\nimport { createSoundCache, type SoundHandle } from './soundCache';\nimport { spatialize, type SpatialOptions, type Vec2 } from './spatialize';\nimport type { AudioEngineOptions, PlayOptions, VoiceHandle } from './types';\nimport { createVoicePool, type VoicePool } from './voicePool';\n\nexport interface AudioEngine {\n /** The engine's `AudioContext` — the one passed as `options.context`, or the\n * one the engine created. Use it for `createBuffer`, for analysis, or for a\n * node graph of your own alongside the engine's.\n *\n * `dispose()` closes a context the engine created, and every call on a\n * closed context throws; a context you passed in is left open, because it is\n * yours to close. The reference stays valid either way — read `state()`\n * rather than assuming. */\n readonly context: AudioContext;\n state(): AudioContextState;\n unlock(): Promise<void>;\n /** Engine time in ms. */\n now(): number;\n load(url: string): Promise<SoundHandle>;\n loadAll(urls: Record<string, string>): Promise<Record<string, SoundHandle>>;\n decode(bytes: ArrayBuffer): Promise<SoundHandle>;\n /** Play a buffer the consumer already holds — a synth, an offline render, a\n * recording. `load` and `decode` both assume encoded bytes. */\n register(buffer: AudioBuffer): SoundHandle;\n play(sound: SoundHandle, opts?: PlayOptions): VoiceHandle;\n stopKey(key: string): void;\n stopAll(): void;\n bus(name: string): BusHandle;\n /** The configured bus names, in order. The first is `play()`'s default. */\n busNames(): string[];\n /** Voices currently sounding, on one bus or on all of them. A voice booked\n * for a future `when` is not counted until it starts. */\n activeVoices(bus?: string): number;\n analyser(busName?: string, opts?: AnalyserTapOptions): AnalyserTap;\n setListener(p: Vec2, opts?: SpatialOptions): void;\n dispose(): void;\n}\n\n/** A voice pool and its token index. Tokens restart at 1 in every pool, so the\n * index has to be per pool — keyed by bus name, a shared pool would look a\n * steal victim up under the wrong name and silently never tear it down. */\ninterface Slots {\n pool: VoicePool;\n byToken: Map<number, LiveVoice>;\n}\n\ninterface LiveVoice {\n id: number;\n slots: Slots;\n /** Null until the scheduler fires it: a voice booked seconds out must not\n * hold a slot, and must not be an eviction candidate, while silent. */\n slot: number | null;\n token: number;\n key?: string;\n source: AudioBufferSourceNode | null;\n gainNode: GainNode;\n panNode: StereoPannerNode;\n baseGain: number;\n spatialGain: number;\n rate: number;\n detune: number;\n /** Kept so `setListener` can re-spatialize a voice that is already playing. */\n position?: Vec2;\n playing: boolean;\n cancelled: boolean;\n}\n\nexport function createAudioEngine(opts: AudioEngineOptions = {}): AudioEngine {\n const ctx = opts.context ?? new AudioContext();\n // A context the engine made is a context the engine closes; a consumer's is\n // theirs to keep.\n const ownsContext = opts.context === undefined;\n const busNames = opts.buses ?? ['sfx', 'music', 'ui'];\n if (busNames.length === 0) {\n throw new Error('@weasel-js/audio: an engine needs at least one bus');\n }\n const graph = createBusGraph(ctx, busNames);\n const sounds = createSoundCache(ctx, opts.fetchFn);\n // One pool PER BUS, not one global pool: the spec's limit is per-bus, and a\n // shared pool lets a burst of one-shots on `sfx` steal the music bed.\n const slotsByBus = new Map<string, Slots>();\n for (const name of busNames) {\n slotsByBus.set(name, {\n pool: createVoicePool({ limit: opts.voiceLimit ?? 32, steal: opts.steal }),\n byToken: new Map(),\n });\n }\n const slotsFor = (bus: string): Slots => {\n const s = slotsByBus.get(bus);\n if (!s) throw new Error(`@weasel-js/audio: unknown bus \"${bus}\"`);\n return s;\n };\n\n const now = (): number => ctx.currentTime * 1000;\n const scheduler = createScheduler({\n now,\n // One-shot, not repeating: the scheduler re-arms at the end of every pass,\n // so an interval would leave the previous one running and double the live\n // timer count per tick.\n setTimer: opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms)),\n clearTimer: opts.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>)),\n lookahead: opts.lookahead,\n interval: opts.tickInterval,\n });\n scheduler.start();\n\n let listener: Vec2 = { x: 0, y: 0 };\n let spatialOpts: SpatialOptions = {};\n let nextVoiceId = 1;\n let disposed = false;\n let suspendedSince = false;\n /** Engine time before which a queued entry is stale — set on resume, so a\n * suspension does not end in one pass that fires its whole backlog. */\n let staleFloor = -Infinity;\n const live = new Map<number, LiveVoice>();\n const taps = new Set<AnalyserTap>();\n\n let warnedLocked = false;\n const warnLocked = (): void => {\n if (warnedLocked) return;\n warnedLocked = true;\n console.warn(\n '@weasel-js/audio: play() before the AudioContext was unlocked — the voice was dropped. ' +\n 'Browsers require a user gesture; call engine.unlock() from one, or wait for the ' +\n 'automatic gesture listener.',\n );\n };\n\n let warnedDisposed = false;\n const warnDisposed = (): void => {\n if (warnedDisposed) return;\n warnedDisposed = true;\n console.warn('@weasel-js/audio: play() after dispose() — the voice was dropped.');\n };\n\n let warnedUnknownSound = false;\n const warnUnknownSound = (id: string): void => {\n if (warnedUnknownSound) return;\n warnedUnknownSound = true;\n console.warn(\n `@weasel-js/audio: play() was handed sound \"${id}\", which this engine never loaded — ` +\n 'the voice was dropped. A handle belongs to the engine that produced it.',\n );\n };\n\n // Resume on the first user gesture, then stop listening.\n const gestures = ['pointerdown', 'keydown', 'touchstart'] as const;\n const onGesture = (): void => { void engine.unlock(); };\n const armGestures = (): void => {\n if (typeof window === 'undefined') return;\n for (const g of gestures) window.addEventListener(g, onGesture, { once: true, passive: true });\n };\n const disarmGestures = (): void => {\n if (typeof window === 'undefined') return;\n for (const g of gestures) window.removeEventListener(g, onGesture);\n };\n armGestures();\n\n // A browser suspends the context when the tab goes away, and the one-shot\n // gesture listeners are long consumed by then. Without this the engine is\n // permanently silent afterwards, with no diagnostic — the drop warning is a\n // once-per-engine latch.\n const onStateChange = (): void => {\n if (disposed) return;\n if (ctx.state === 'running') {\n if (suspendedSince) staleFloor = now();\n suspendedSince = false;\n return;\n }\n suspendedSince = true;\n warnedLocked = false;\n armGestures();\n };\n ctx.addEventListener('statechange', onStateChange);\n\n const poolGain = (voice: LiveVoice): void => {\n if (voice.slot === null) return;\n voice.slots.pool.setGain(voice.slot, voice.token, voice.baseGain * voice.spatialGain);\n };\n\n const applySpatial = (voice: LiveVoice): void => {\n if (!voice.position) return;\n const s = spatialize(voice.position, listener, spatialOpts);\n voice.spatialGain = s.gain;\n voice.panNode.pan.value = s.pan;\n writeParam(ctx, voice.gainNode.gain, voice.baseGain * s.gain);\n poolGain(voice);\n };\n\n const teardown = (voice: LiveVoice): void => {\n // First, and never at a call site: teardown is what makes a voice dead, and\n // a queued callback that outlives it must find it cancelled. The steal path\n // has no other place to set this.\n voice.cancelled = true;\n if (!voice.playing && voice.source === null) return;\n voice.playing = false;\n if (voice.source) {\n try { voice.source.stop(); } catch { /* already stopped */ }\n voice.source.disconnect();\n }\n voice.source = null;\n // Nothing pools the chain, so it goes when the voice does.\n voice.panNode.disconnect();\n voice.gainNode.disconnect();\n live.delete(voice.id);\n if (voice.slot !== null) {\n voice.slots.byToken.delete(voice.token);\n // Safe on the steal path too: the pool has already reissued this slot\n // under a new token, and it ignores a release carrying the old one.\n voice.slots.pool.release(voice.slot, voice.token);\n }\n };\n\n const dropped = (id: number): VoiceHandle => ({\n id,\n stop: () => {}, setGain: () => {}, setRate: () => {}, setDetune: () => {},\n setPan: () => {}, setPosition: () => {}, isPlaying: () => false,\n });\n\n const engine: AudioEngine = {\n context: ctx,\n state: () => ctx.state,\n async unlock() {\n if (disposed) return;\n if (ctx.state !== 'running') await ctx.resume();\n },\n now,\n load: sounds.load,\n loadAll: sounds.loadAll,\n decode: sounds.decode,\n register: sounds.register,\n\n play(sound, playOpts = {}) {\n const id = nextVoiceId++;\n\n if (disposed) {\n warnDisposed();\n return dropped(id);\n }\n if (ctx.state !== 'running') {\n warnLocked();\n return dropped(id);\n }\n\n const busName = playOpts.bus ?? busNames[0];\n const slots = slotsFor(busName);\n const when = playOpts.when ?? now();\n const baseGain = playOpts.gain ?? 1;\n\n const spatial = playOpts.position\n ? spatialize(playOpts.position, listener, spatialOpts)\n : { gain: 1, pan: playOpts.pan ?? 0 };\n\n const gainNode = ctx.createGain();\n const panNode = ctx.createStereoPanner();\n gainNode.gain.value = baseGain * spatial.gain;\n panNode.pan.value = spatial.pan;\n panNode.connect(gainNode);\n gainNode.connect(graph.node(busName));\n\n const voice: LiveVoice = {\n id, slots, slot: null, token: 0, key: playOpts.cancelKey,\n source: null, gainNode, panNode,\n baseGain, spatialGain: spatial.gain,\n rate: playOpts.rate ?? 1, detune: playOpts.detune ?? 0,\n position: playOpts.position,\n playing: true, cancelled: false,\n };\n live.set(id, voice);\n\n scheduler.schedule(when, (scheduledWhen) => {\n if (voice.cancelled) return;\n // Came due while the context was suspended: starting it now would play\n // a backlog at once, which is what dropping a locked play avoids.\n if (scheduledWhen < staleFloor) {\n teardown(voice);\n return;\n }\n const buffer = sounds.buffer(sound);\n if (!buffer) {\n warnUnknownSound(sound.id);\n teardown(voice);\n return;\n }\n\n // The slot is taken here, not at play() time. A voice booked a bar out\n // would otherwise hold one while silent — and, its `startedAt` being in\n // the future, be the last thing the 'oldest' policy evicts, so booking\n // a bar of events would evict everything currently audible.\n const acquired = slots.pool.acquire({\n startedAt: scheduledWhen,\n gain: voice.baseGain * voice.spatialGain,\n });\n voice.slot = acquired.slot;\n voice.token = acquired.token;\n slots.byToken.set(acquired.token, voice);\n if (acquired.stolen !== null) {\n const victim = slots.byToken.get(acquired.stolen);\n if (victim) teardown(victim);\n }\n\n // A fresh source per play: AudioBufferSourceNode is single-use by\n // specification and cannot be restarted once stopped.\n const source = ctx.createBufferSource();\n source.buffer = buffer;\n source.loop = playOpts.loop ?? false;\n source.playbackRate.value = voice.rate;\n source.detune.value = voice.detune;\n source.connect(panNode);\n source.onended = () => { teardown(voice); playOpts.onDone?.(); };\n voice.source = source;\n source.start(scheduledWhen / 1000);\n }, playOpts.cancelKey);\n\n return {\n id,\n stop(fadeMs) {\n if (fadeMs && fadeMs > 0 && voice.source && !voice.cancelled) {\n // Cancelled but not torn down: the fade is what ends this voice, and\n // stopping the source now would cut the ramp scheduled a line above\n // it — which is the click `fadeMs` exists to avoid. `onended` does\n // the bookkeeping when the fade reaches the end.\n voice.cancelled = true;\n writeParam(ctx, gainNode.gain, 0, fadeMs);\n try { voice.source.stop(ctx.currentTime + fadeMs / 1000); } catch { /* ended */ }\n return;\n }\n teardown(voice);\n },\n setGain(value, rampMs) {\n voice.baseGain = value;\n writeParam(ctx, gainNode.gain, value * voice.spatialGain, rampMs);\n poolGain(voice);\n },\n setRate(value) {\n voice.rate = value;\n if (voice.source) voice.source.playbackRate.value = value;\n },\n setDetune(cents) {\n voice.detune = cents;\n if (voice.source) voice.source.detune.value = cents;\n },\n setPan(value) {\n voice.position = undefined;\n panNode.pan.value = value;\n },\n setPosition(p) {\n voice.position = p;\n applySpatial(voice);\n },\n isPlaying: () => voice.playing,\n };\n },\n\n stopKey(key) {\n scheduler.cancelKey(key);\n for (const voice of [...live.values()]) {\n if (voice.key === key) teardown(voice);\n }\n },\n stopAll() {\n for (const voice of [...live.values()]) teardown(voice);\n },\n bus: graph.bus,\n busNames: graph.names,\n activeVoices(bus) {\n if (bus !== undefined) return slotsFor(bus).pool.active();\n let total = 0;\n for (const s of slotsByBus.values()) total += s.pool.active();\n return total;\n },\n analyser(busName, tapOpts) {\n // Every tap is one more live FFT on the graph. They are tracked so\n // `dispose()` detaches them; a consumer calling this per frame still\n // wants to keep one tap rather than mint one.\n const tap = createAnalyserTap(ctx, busName ? graph.node(busName) : graph.master, tapOpts);\n taps.add(tap);\n return {\n ...tap,\n dispose() {\n taps.delete(tap);\n tap.dispose();\n },\n };\n },\n setListener(p, o) {\n listener = p;\n if (o) spatialOpts = o;\n for (const voice of live.values()) applySpatial(voice);\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n engine.stopAll();\n scheduler.stop();\n scheduler.clear();\n for (const tap of [...taps]) tap.dispose();\n taps.clear();\n disarmGestures();\n ctx.removeEventListener('statechange', onStateChange);\n graph.master.disconnect();\n if (ownsContext) void ctx.close().catch(() => undefined);\n },\n };\n\n return engine;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weasel-js/audio",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Web Audio engine for 2D scenes: voices, buses, lookahead scheduling, spatialization and analysis. No weasel dependencies.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"build": "tsup"
|
|
21
|
+
},
|
|
22
|
+
"author": "orochi235",
|
|
23
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
27
|
+
"directory": "packages/audio"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true
|
|
43
|
+
}
|
|
44
|
+
}
|