@driftengine/audio 3.61.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.
Files changed (88) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +11 -0
  4. package/dist/ambientLoop.d.ts +45 -0
  5. package/dist/ambientLoop.js +88 -0
  6. package/dist/audioHarness.d.ts +180 -0
  7. package/dist/audioHarness.js +244 -0
  8. package/dist/filters.d.ts +91 -0
  9. package/dist/filters.js +103 -0
  10. package/dist/formats.d.ts +18 -0
  11. package/dist/formats.js +19 -0
  12. package/dist/graph.d.ts +406 -0
  13. package/dist/graph.js +656 -0
  14. package/dist/index.d.ts +47 -0
  15. package/dist/index.js +39 -0
  16. package/dist/manifest.d.ts +28 -0
  17. package/dist/manifest.js +71 -0
  18. package/dist/mix/bus.d.ts +203 -0
  19. package/dist/mix/bus.js +293 -0
  20. package/dist/mix/console.d.ts +96 -0
  21. package/dist/mix/console.js +131 -0
  22. package/dist/mix/defaultLayout.d.ts +37 -0
  23. package/dist/mix/defaultLayout.js +63 -0
  24. package/dist/mix/inserts.d.ts +64 -0
  25. package/dist/mix/inserts.js +187 -0
  26. package/dist/mix/returns.d.ts +38 -0
  27. package/dist/mix/returns.js +86 -0
  28. package/dist/mix/snapshot.d.ts +30 -0
  29. package/dist/mix/snapshot.js +55 -0
  30. package/dist/positional.d.ts +37 -0
  31. package/dist/positional.js +47 -0
  32. package/dist/registry.d.ts +91 -0
  33. package/dist/registry.js +128 -0
  34. package/dist/rhythm/bands.d.ts +60 -0
  35. package/dist/rhythm/bands.js +12 -0
  36. package/dist/rhythm/beatGrid.d.ts +32 -0
  37. package/dist/rhythm/beatGrid.js +98 -0
  38. package/dist/rhythm/beatMap.d.ts +42 -0
  39. package/dist/rhythm/beatMap.js +405 -0
  40. package/dist/rhythm/kickCore.d.ts +79 -0
  41. package/dist/rhythm/kickCore.js +166 -0
  42. package/dist/rhythm/kickDetector.d.ts +65 -0
  43. package/dist/rhythm/kickDetector.js +202 -0
  44. package/dist/rhythm/renderedPulse.d.ts +15 -0
  45. package/dist/rhythm/renderedPulse.js +138 -0
  46. package/dist/session.d.ts +62 -0
  47. package/dist/session.js +83 -0
  48. package/dist/spatial/ambisonic.d.ts +135 -0
  49. package/dist/spatial/ambisonic.js +299 -0
  50. package/dist/spatial/listener.d.ts +109 -0
  51. package/dist/spatial/listener.js +186 -0
  52. package/dist/spatial/occlusion.d.ts +39 -0
  53. package/dist/spatial/occlusion.js +92 -0
  54. package/dist/spatial/source.d.ts +185 -0
  55. package/dist/spatial/source.js +366 -0
  56. package/dist/spatial/zones.d.ts +129 -0
  57. package/dist/spatial/zones.js +166 -0
  58. package/dist/synth.d.ts +92 -0
  59. package/dist/synth.js +282 -0
  60. package/package.json +54 -0
  61. package/src/ambientLoop.ts +101 -0
  62. package/src/audioHarness.ts +280 -0
  63. package/src/filters.ts +109 -0
  64. package/src/formats.ts +22 -0
  65. package/src/graph.ts +805 -0
  66. package/src/index.ts +84 -0
  67. package/src/manifest.ts +73 -0
  68. package/src/mix/bus.ts +356 -0
  69. package/src/mix/console.ts +181 -0
  70. package/src/mix/defaultLayout.ts +118 -0
  71. package/src/mix/inserts.ts +242 -0
  72. package/src/mix/returns.ts +114 -0
  73. package/src/mix/snapshot.ts +75 -0
  74. package/src/positional.ts +47 -0
  75. package/src/registry.ts +167 -0
  76. package/src/rhythm/bands.ts +45 -0
  77. package/src/rhythm/beatGrid.ts +106 -0
  78. package/src/rhythm/beatMap.ts +514 -0
  79. package/src/rhythm/kickCore.ts +197 -0
  80. package/src/rhythm/kickDetector.ts +233 -0
  81. package/src/rhythm/renderedPulse.ts +147 -0
  82. package/src/session.ts +93 -0
  83. package/src/spatial/ambisonic.ts +358 -0
  84. package/src/spatial/listener.ts +249 -0
  85. package/src/spatial/occlusion.ts +95 -0
  86. package/src/spatial/source.ts +452 -0
  87. package/src/spatial/zones.ts +213 -0
  88. package/src/synth.ts +351 -0
@@ -0,0 +1,186 @@
1
+ import { resolveZones } from './zones.js';
2
+ /**
3
+ * The largest speed a listener is allowed to be moving, metres per second.
4
+ *
5
+ * Not a physical limit — it is a guard against a position that jumped. Roughly Mach 1, so anything
6
+ * a game moves a camera at is under it and a teleport that slipped past `warp` is clamped to
7
+ * something that merely sounds wrong rather than dividing by zero in the doppler ratio.
8
+ */
9
+ const MAX_SPEED = 340;
10
+ export class AudioListenerGraph {
11
+ mix;
12
+ /** Set by the consumer; called by whatever wants to know if something is in the way. */
13
+ probe = null;
14
+ posX = 0;
15
+ posY = 0;
16
+ posZ = 0;
17
+ velX = 0;
18
+ velY = 0;
19
+ velZ = 0;
20
+ placed = false;
21
+ elapsed = 0;
22
+ zones = [];
23
+ /**
24
+ * What each zone is being sent, owned here rather than read back off the send.
25
+ *
26
+ * Reused across frames rather than rebuilt: this is written every time the listener moves, and a
27
+ * map per frame is an allocation in a per-frame path.
28
+ */
29
+ zoneAmounts = new Map();
30
+ /** How many sources are placed against this listener, for spreading their probes apart. */
31
+ sourceCount = 0;
32
+ constructor(mix) {
33
+ this.mix = mix;
34
+ }
35
+ /** The mix this listener belongs to. A source needs it for a bus and for the context. */
36
+ get console() {
37
+ return this.mix;
38
+ }
39
+ /**
40
+ * Seconds of listener time, accumulated from the frame steps it is given.
41
+ *
42
+ * Accumulated rather than read off a clock, because nothing under this package may reach for
43
+ * `performance.now` on a path a consumer might simulate, and because an offline render has no
44
+ * wall time at all — a mix rendered faster than real time still has to stagger its probes the
45
+ * same way the live one does.
46
+ */
47
+ get elapsedSec() {
48
+ return this.elapsed;
49
+ }
50
+ /** Register a space this listener can be inside. See `addReverbZone`. */
51
+ addZone(zone) {
52
+ this.zones.push(zone);
53
+ }
54
+ /** What that zone is currently being sent, from where the listener is standing. */
55
+ zoneSend(zone) {
56
+ return this.zoneAmounts.get(zone) ?? 0;
57
+ }
58
+ /** Claim a probe slot. The index is what spreads one source's turn away from its neighbours'. */
59
+ claimProbeSlot() {
60
+ return this.sourceCount++;
61
+ }
62
+ get probeSlots() {
63
+ return this.sourceCount;
64
+ }
65
+ get x() {
66
+ return this.posX;
67
+ }
68
+ get y() {
69
+ return this.posY;
70
+ }
71
+ get z() {
72
+ return this.posZ;
73
+ }
74
+ get velocityX() {
75
+ return this.velX;
76
+ }
77
+ get velocityY() {
78
+ return this.velY;
79
+ }
80
+ get velocityZ() {
81
+ return this.velZ;
82
+ }
83
+ /**
84
+ * Place and point the listener for this frame.
85
+ *
86
+ * `yaw` 0 faces −Z and `pitch` is positive looking up, which is the convention every camera in
87
+ * this engine uses and the one `stereoPan` already documents. The two agree by construction here
88
+ * rather than by coincidence, so a source panned the cheap way and a source panned through a
89
+ * panner land on the same side of the head.
90
+ *
91
+ * **Velocity is derived rather than taken.** A caller passing both a position and a velocity can
92
+ * make them disagree, and doppler needs only the component along the line to a source. Cost: a
93
+ * position that jumps reads as enormous speed, which is what `warp` and `MAX_SPEED` are between.
94
+ */
95
+ set(x, y, z, yaw, pitch, dtSec) {
96
+ if (dtSec > 0)
97
+ this.elapsed += dtSec;
98
+ if (this.placed && dtSec > 0) {
99
+ this.velX = clampSpeed((x - this.posX) / dtSec);
100
+ this.velY = clampSpeed((y - this.posY) / dtSec);
101
+ this.velZ = clampSpeed((z - this.posZ) / dtSec);
102
+ }
103
+ this.posX = x;
104
+ this.posY = y;
105
+ this.posZ = z;
106
+ this.placed = true;
107
+ const cosPitch = Math.cos(pitch);
108
+ const forwardX = Math.sin(yaw) * cosPitch;
109
+ const forwardY = Math.sin(pitch);
110
+ const forwardZ = -Math.cos(yaw) * cosPitch;
111
+ const sinPitch = Math.sin(pitch);
112
+ const upX = -Math.sin(yaw) * sinPitch;
113
+ const upY = cosPitch;
114
+ const upZ = Math.cos(yaw) * sinPitch;
115
+ this.write(x, y, z, forwardX, forwardY, forwardZ, upX, upY, upZ);
116
+ if (this.zones.length > 0) {
117
+ resolveZones(this.zones, x, y, z, this.zoneAmounts);
118
+ for (const [zone, amount] of this.zoneAmounts)
119
+ zone.from.send(zone.bus, amount);
120
+ }
121
+ }
122
+ /**
123
+ * Move the listener without it having travelled.
124
+ *
125
+ * A teleport, a respawn, a camera cut. The next frame is measured from here, not from where the
126
+ * listener was, so nothing derives a speed from a jump that never happened.
127
+ */
128
+ warp(x, y, z) {
129
+ this.posX = x;
130
+ this.posY = y;
131
+ this.posZ = z;
132
+ this.velX = 0;
133
+ this.velY = 0;
134
+ this.velZ = 0;
135
+ this.placed = true;
136
+ }
137
+ /**
138
+ * Write the listener, through whichever surface this browser has.
139
+ *
140
+ * `positionX` and the parameters beside it are the modern form and can be ramped; `setPosition`
141
+ * and `setOrientation` are deprecated and step. Both are present in Chromium here, measured
142
+ * 2026-08-24; **WebKit is the reason the second branch exists and it cannot be verified from this
143
+ * machine.** Cost: on the legacy path a fast listener steps rather than glides, which is audible
144
+ * as a faint zipper on a hard turn. What would make this wrong is WebKit gaining the parameters,
145
+ * at which point the branch is dead code and should be deleted rather than kept for symmetry.
146
+ *
147
+ * Values are assigned rather than ramped even on the modern path. A ramp per component per frame
148
+ * is nine scheduled events sixty times a second for a value that is already being sampled every
149
+ * block, and the smoothing that matters — the one a listener can hear — is the panner's own
150
+ * interpolation between blocks.
151
+ */
152
+ write(x, y, z, fx, fy, fz, ux, uy, uz) {
153
+ const listener = this.mix.context.listener;
154
+ if (listener === undefined || listener === null)
155
+ return;
156
+ const modern = listener;
157
+ if (modern.positionX !== undefined && modern.forwardX !== undefined) {
158
+ setValue(modern.positionX, x);
159
+ setValue(modern.positionY, y);
160
+ setValue(modern.positionZ, z);
161
+ setValue(modern.forwardX, fx);
162
+ setValue(modern.forwardY, fy);
163
+ setValue(modern.forwardZ, fz);
164
+ setValue(modern.upX, ux);
165
+ setValue(modern.upY, uy);
166
+ setValue(modern.upZ, uz);
167
+ return;
168
+ }
169
+ const legacy = listener;
170
+ legacy.setPosition?.(x, y, z);
171
+ legacy.setOrientation?.(fx, fy, fz, ux, uy, uz);
172
+ }
173
+ }
174
+ function setValue(param, value) {
175
+ if (param !== undefined)
176
+ param.value = value;
177
+ }
178
+ function clampSpeed(value) {
179
+ if (!Number.isFinite(value))
180
+ return 0;
181
+ return Math.min(Math.max(value, -MAX_SPEED), MAX_SPEED);
182
+ }
183
+ /** The listener for a console. One per mix; everything placed in the world reads it. */
184
+ export function createListener(mix) {
185
+ return new AudioListenerGraph(mix);
186
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * What a wall does to a sound, and how often it is worth asking whether there is one.
3
+ *
4
+ * **The engine owns the curve and the consumer owns the number.** How blocked a path is depends on
5
+ * a world this package has never seen; what blocking *sounds* like does not. So a consumer answers
6
+ * "how much wall" and everything below decides what that means.
7
+ */
8
+ /** Geometric, for the reason `liftFrequencyHz` gives. Open above hearing, shut at 500 Hz. */
9
+ export declare function occlusionCutoffHz(amount: number): number;
10
+ /** Level for a given occlusion. 1 clear, `SHUT_GAIN` solid, linear between. */
11
+ export declare function occlusionGainFor(amount: number): number;
12
+ /**
13
+ * Exponential approach, in time.
14
+ *
15
+ * `rate` is per second and means the same thing at 60 Hz, at 144 Hz and at whatever a background
16
+ * tab does. A per-step factor is the bug `AGENTS.md` records: the same constant meant 47 ms in one
17
+ * caller and 158 ms in another, and moved with the frame rate in the second.
18
+ */
19
+ export declare function smoothToward(current: number, target: number, rate: number, dtSec: number): number;
20
+ /**
21
+ * When a source is allowed to ask the world whether something is in the way.
22
+ *
23
+ * A segment test per source per frame is the most expensive thing occlusion can do, and it is paid
24
+ * by the consumer's own collision code — which is exactly why the throttling lives here rather
25
+ * than being left for them to discover. Each source is probed once per period, and the phase is
26
+ * spread across its index, so twenty sources are twenty single tests rather than one spike.
27
+ *
28
+ * Cost: a wall that appears between a source and the listener takes up to one period to be heard,
29
+ * and the smoothing above adds its own approach on top of that. At the default that is well under
30
+ * the time a player takes to walk through a doorway. What would make this wrong is a source moving
31
+ * faster than the period — a projectile — which should be probed every frame and say so.
32
+ */
33
+ export declare class ProbeScheduler {
34
+ private readonly periodSec;
35
+ private lastAt;
36
+ constructor(periodSec?: number);
37
+ due(index: number, count: number, nowSec: number): boolean;
38
+ forget(index: number): void;
39
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * What a wall does to a sound, and how often it is worth asking whether there is one.
3
+ *
4
+ * **The engine owns the curve and the consumer owns the number.** How blocked a path is depends on
5
+ * a world this package has never seen; what blocking *sounds* like does not. So a consumer answers
6
+ * "how much wall" and everything below decides what that means.
7
+ */
8
+ /** Wide open: above hearing, so an unoccluded source is not filtered at all. */
9
+ const OPEN_HZ = 20000;
10
+ /**
11
+ * Shut: 500 Hz.
12
+ *
13
+ * Low enough that consonants and the top of anything percussive are gone, which is what makes a
14
+ * sound read as being *through* something. Not lower: below about 300 Hz a voice stops sounding
15
+ * muffled and starts sounding like a different sound in a different place.
16
+ */
17
+ const SHUT_HZ = 500;
18
+ /**
19
+ * How far a fully blocked source drops, as a gain.
20
+ *
21
+ * About twelve decibels. **Not zero, and that is the decision here.** A source that goes silent
22
+ * behind a wall pops out of existence, and a player learns that walls delete sounds rather than
23
+ * muffle them — which is worse than no occlusion at all, because it removes the information that
24
+ * the thing is still there. Cost: a source behind a wall is never *gone*, so a consumer wanting
25
+ * something genuinely inaudible has to stop it rather than occlude it. What would make this wrong
26
+ * is a game where hearing through a wall is a mechanic to be denied, which is a design decision
27
+ * rather than an acoustic one.
28
+ */
29
+ const SHUT_GAIN = 0.25;
30
+ /** Geometric, for the reason `liftFrequencyHz` gives. Open above hearing, shut at 500 Hz. */
31
+ export function occlusionCutoffHz(amount) {
32
+ const t = clamp01(amount);
33
+ return OPEN_HZ * (SHUT_HZ / OPEN_HZ) ** t;
34
+ }
35
+ /** Level for a given occlusion. 1 clear, `SHUT_GAIN` solid, linear between. */
36
+ export function occlusionGainFor(amount) {
37
+ return 1 - (1 - SHUT_GAIN) * clamp01(amount);
38
+ }
39
+ /**
40
+ * Exponential approach, in time.
41
+ *
42
+ * `rate` is per second and means the same thing at 60 Hz, at 144 Hz and at whatever a background
43
+ * tab does. A per-step factor is the bug `AGENTS.md` records: the same constant meant 47 ms in one
44
+ * caller and 158 ms in another, and moved with the frame rate in the second.
45
+ */
46
+ export function smoothToward(current, target, rate, dtSec) {
47
+ if (!(dtSec > 0))
48
+ return current;
49
+ return current + (target - current) * (1 - Math.exp(-rate * dtSec));
50
+ }
51
+ /**
52
+ * When a source is allowed to ask the world whether something is in the way.
53
+ *
54
+ * A segment test per source per frame is the most expensive thing occlusion can do, and it is paid
55
+ * by the consumer's own collision code — which is exactly why the throttling lives here rather
56
+ * than being left for them to discover. Each source is probed once per period, and the phase is
57
+ * spread across its index, so twenty sources are twenty single tests rather than one spike.
58
+ *
59
+ * Cost: a wall that appears between a source and the listener takes up to one period to be heard,
60
+ * and the smoothing above adds its own approach on top of that. At the default that is well under
61
+ * the time a player takes to walk through a doorway. What would make this wrong is a source moving
62
+ * faster than the period — a projectile — which should be probed every frame and say so.
63
+ */
64
+ export class ProbeScheduler {
65
+ periodSec;
66
+ lastAt = new Map();
67
+ constructor(periodSec = 0.2) {
68
+ this.periodSec = periodSec;
69
+ }
70
+ due(index, count, nowSec) {
71
+ const spread = count > 0 ? (index / count) * this.periodSec : 0;
72
+ const last = this.lastAt.get(index);
73
+ if (last === undefined) {
74
+ // The first turn is taken at this source's own phase, not immediately, or every source
75
+ // created in one frame probes in that frame and the stagger never starts.
76
+ if (nowSec < spread)
77
+ return false;
78
+ this.lastAt.set(index, nowSec);
79
+ return true;
80
+ }
81
+ if (nowSec - last < this.periodSec)
82
+ return false;
83
+ this.lastAt.set(index, nowSec);
84
+ return true;
85
+ }
86
+ forget(index) {
87
+ this.lastAt.delete(index);
88
+ }
89
+ }
90
+ function clamp01(value) {
91
+ return Number.isFinite(value) ? Math.min(Math.max(value, 0), 1) : 0;
92
+ }
@@ -0,0 +1,185 @@
1
+ import type { MixBus } from '../mix/bus.ts';
2
+ import type { AudioListenerGraph } from './listener.ts';
3
+ import { type ReverbZone } from './zones.ts';
4
+ /**
5
+ * A sound placed in the world, heard from where the listener is standing.
6
+ *
7
+ * ```
8
+ * buffer source ─ detune ─→ occlusion lowpass ─→ occlusion gain ─→ panner (HRTF) ─→ a bus
9
+ * ```
10
+ *
11
+ * **A function rather than a method on the console, and that is a size decision rather than a
12
+ * style one.** A method would be a static reference from the mix to the panner, which no bundler
13
+ * can shake out, so every consumer that imports the mix would carry the HRTF machinery whether or
14
+ * not it ever places a sound. `scripts/size-gate.test.mjs` publishes what each package costs and
15
+ * promises you pay only for what you import; this is what keeps that true.
16
+ *
17
+ * **This is the expensive path and it is meant to be.** `distanceGain` and `stereoPan` still exist
18
+ * and are still right for a fire, a shoreline or a storm column — diffuse things where what a
19
+ * player reads is "how close" and "which way". Reach for a panner when the *direction* is
20
+ * information: a footstep behind you, a voice through a doorway, something you are meant to turn
21
+ * towards.
22
+ */
23
+ export interface SpatialOptions {
24
+ /** Where this lands in the mix. The console's `effects` bus when omitted. */
25
+ readonly bus?: MixBus;
26
+ readonly loop?: boolean;
27
+ /**
28
+ * Shift the pitch with relative motion.
29
+ *
30
+ * **Off by default, deliberately.** On a looping bed — a fire, a waterfall — a listener walking
31
+ * past makes the bed's pitch wander, which reads as the sound being broken rather than as motion.
32
+ * Turn it on for something that genuinely passes: a vehicle, a projectile, a siren.
33
+ */
34
+ readonly doppler?: boolean;
35
+ /** Metres per second. 343 in air; a game whose world is not in metres will want its own. */
36
+ readonly speedOfSound?: number;
37
+ /** How far the pitch is allowed to move, in cents. A whole tone each way by default. */
38
+ readonly maxDopplerCents?: number;
39
+ /** Distance-model parameters, passed to the panner unchanged. */
40
+ readonly refDistance?: number;
41
+ readonly maxDistance?: number;
42
+ readonly rolloff?: number;
43
+ /**
44
+ * How fast occlusion follows the world, per second.
45
+ *
46
+ * Slow enough that a probe landing on a different answer does not click, fast enough that walking
47
+ * through a doorway is heard as walking through a doorway.
48
+ */
49
+ readonly occlusionRate?: number;
50
+ }
51
+ export declare class SpatialSource {
52
+ private readonly listener;
53
+ private readonly options;
54
+ private readonly node;
55
+ private readonly filter;
56
+ private readonly gain;
57
+ private readonly panner;
58
+ private readonly probes;
59
+ private readonly slot;
60
+ private posX;
61
+ private posY;
62
+ private posZ;
63
+ private velX;
64
+ private velY;
65
+ private velZ;
66
+ private placed;
67
+ private occlusionTarget;
68
+ private occlusionNow;
69
+ private cents;
70
+ private started;
71
+ private stopped;
72
+ /**
73
+ * The zones this source carries the tail of, from its own position.
74
+ *
75
+ * An array rather than a map, because it is walked every frame and never looked up by key — the
76
+ * budget below caps it at a handful, and `resolveZones`'s own reason for a two-slot scan over a
77
+ * sort applies here one level down.
78
+ */
79
+ private readonly zoneSends;
80
+ constructor(listener: AudioListenerGraph, buffer: AudioBuffer, options?: SpatialOptions);
81
+ /** Where this source's dry signal lands, which is what decides whether a zone would double. */
82
+ private readonly bus;
83
+ /**
84
+ * Carry the tail of a space this **source** is in, rather than one the listener is in.
85
+ *
86
+ * The case `zones.ts` names as the one its own model cannot serve: a sound inside a cave heard
87
+ * from outside carries whatever space the *listener* stands in, which is wrong exactly when
88
+ * somebody is listening *into* a space. A source-attached zone is a second **routing** into a
89
+ * return that already carries a convolver, so it costs one `GainNode` rather than one convolution.
90
+ *
91
+ * **`MAX_OPEN_ZONES` still binds, and it binds per source.** The budget is not about the sends,
92
+ * which are nearly free; it is about how many convolvers are audible at once, and a source driving
93
+ * four returns makes four of them audible. Two is the space being left and the space being
94
+ * entered, here as for the listener.
95
+ *
96
+ * The send is taken from the **occluded, unpanned** signal: a reverb send comes off the channel on
97
+ * any console, and the return is itself a stereo space, so a point-panned tail would arrive from
98
+ * the source's direction rather than from the room. Occlusion is upstream because a sound behind a
99
+ * wall has a muffled tail too.
100
+ */
101
+ attachZone(zone: ReverbZone): void;
102
+ /** What this source is currently sending into that zone's return. */
103
+ zoneSend(zone: ReverbZone): number;
104
+ /** The pitch shift currently applied, in cents. Zero with doppler off. */
105
+ get detuneCents(): number;
106
+ /** How blocked this source is right now, after smoothing. */
107
+ get occlusion(): number;
108
+ /**
109
+ * Where this source is, this frame.
110
+ *
111
+ * Velocity is derived here exactly as the listener derives its own, and for the same reason. A
112
+ * source that teleports should be moved with `warp`.
113
+ */
114
+ place(x: number, y: number, z: number, dtSec: number): void;
115
+ /** Move without having travelled: a respawn, a cut, an object put back at the start. */
116
+ warp(x: number, y: number, z: number): void;
117
+ /**
118
+ * Say how blocked this source is, 0 clear to 1 solid.
119
+ *
120
+ * For a consumer that already knows — a door with a state, a sound that is definitionally
121
+ * indoors — and for anyone who would rather not hand the listener a probe. Overrides whatever the
122
+ * probe last answered until the probe answers again.
123
+ */
124
+ setOcclusion(amount: number): void;
125
+ start(when?: number): void;
126
+ stop(): void;
127
+ /**
128
+ * Which of this source's zones sound, from where the source is standing.
129
+ *
130
+ * The same two-slot scan `resolveZones` makes for the listener, written out here rather than
131
+ * shared because the listener's version fills a `Map` keyed by zone and this one walks an array
132
+ * of sends — the ranking is six lines and a shared version taking both shapes would be longer
133
+ * than either. What would make that wrong is a third caller.
134
+ *
135
+ * **Two slots is `MAX_OPEN_ZONES`**, written out rather than looped because two is what a
136
+ * crossfade needs. `sourceZone.test.ts` asserts the constant is still two, so raising it fails
137
+ * there and sends whoever raised it to read this.
138
+ */
139
+ private updateZones;
140
+ dispose(): void;
141
+ /**
142
+ * Where the panner thinks this source is, through whichever surface the browser has.
143
+ *
144
+ * The same pair as the listener's, for the same reason and with the same cost: the deprecated
145
+ * setter steps where the parameters can glide. Assigned rather than ramped on both paths, because
146
+ * this is written every frame and the interpolation that matters is the panner's own, between
147
+ * render blocks.
148
+ */
149
+ private writePosition;
150
+ /**
151
+ * The doppler ratio, and why it is ours to compute.
152
+ *
153
+ * `PannerNode` carried `dopplerFactor` and `speedOfSound` and the specification removed both, so
154
+ * there is nothing to configure and nothing to fall back to: the shift is arithmetic on two
155
+ * velocities projected onto the line between the two objects.
156
+ *
157
+ * With `d` the unit vector from listener to source, the ratio is
158
+ * `(c + vListener·d) / (c + vSource·d)` — a listener closing on a source raises the numerator, a
159
+ * source closing on the listener lowers the denominator, and both raise the pitch.
160
+ *
161
+ * Clamped in cents rather than in ratio, because cents are what a listener hears: a whole tone
162
+ * each way is a strong, obviously-moving effect and anything past it stops reading as motion.
163
+ * Cost: a deliberately supersonic source stops shifting at the clamp instead of doing something
164
+ * dramatic. What would make this wrong is a game whose subject *is* the sonic boom, which wants a
165
+ * different model rather than a wider clamp.
166
+ */
167
+ private updateDoppler;
168
+ /**
169
+ * Apply the shift, through `detune` where the browser has it and `playbackRate` where it does not.
170
+ *
171
+ * `detune` is in cents, which is the unit the arithmetic above produces and the unit a musician
172
+ * would state it in. `playbackRate` is the ratio, and it is the older surface: converting back is
173
+ * exact, so the fallback is a different spelling rather than a different effect.
174
+ */
175
+ private writeDetune;
176
+ /**
177
+ * Ask the world whether something is in the way, at most once per period, then approach it.
178
+ *
179
+ * The probe is the consumer's own collision code and is the expensive part; the smoothing is what
180
+ * keeps a probe that lands on a different answer from clicking.
181
+ */
182
+ private updateOcclusion;
183
+ }
184
+ /** Place a sound in the world. See `SpatialSource` for when this is the right tool and when it is not. */
185
+ export declare function createSpatialSource(listener: AudioListenerGraph, buffer: AudioBuffer, options?: SpatialOptions): SpatialSource;