@aura3d/audio 1.0.6

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 (84) hide show
  1. package/README.md +16 -0
  2. package/dist/AdaptiveMusicFixtures.d.ts +36 -0
  3. package/dist/AdaptiveMusicFixtures.d.ts.map +1 -0
  4. package/dist/AdaptiveMusicFixtures.js +90 -0
  5. package/dist/AdaptiveMusicFixtures.js.map +1 -0
  6. package/dist/AudioBus.d.ts +13 -0
  7. package/dist/AudioBus.d.ts.map +1 -0
  8. package/dist/AudioBus.js +32 -0
  9. package/dist/AudioBus.js.map +1 -0
  10. package/dist/AudioClip.d.ts +13 -0
  11. package/dist/AudioClip.d.ts.map +1 -0
  12. package/dist/AudioClip.js +18 -0
  13. package/dist/AudioClip.js.map +1 -0
  14. package/dist/AudioContextManager.d.ts +31 -0
  15. package/dist/AudioContextManager.d.ts.map +1 -0
  16. package/dist/AudioContextManager.js +49 -0
  17. package/dist/AudioContextManager.js.map +1 -0
  18. package/dist/AudioEffect.d.ts +8 -0
  19. package/dist/AudioEffect.d.ts.map +1 -0
  20. package/dist/AudioEffect.js +2 -0
  21. package/dist/AudioEffect.js.map +1 -0
  22. package/dist/AudioEffectsAnalysisFixtures.d.ts +99 -0
  23. package/dist/AudioEffectsAnalysisFixtures.d.ts.map +1 -0
  24. package/dist/AudioEffectsAnalysisFixtures.js +192 -0
  25. package/dist/AudioEffectsAnalysisFixtures.js.map +1 -0
  26. package/dist/AudioListener.d.ts +12 -0
  27. package/dist/AudioListener.d.ts.map +1 -0
  28. package/dist/AudioListener.js +11 -0
  29. package/dist/AudioListener.js.map +1 -0
  30. package/dist/AudioMixer.d.ts +12 -0
  31. package/dist/AudioMixer.d.ts.map +1 -0
  32. package/dist/AudioMixer.js +33 -0
  33. package/dist/AudioMixer.js.map +1 -0
  34. package/dist/AudioSource.d.ts +25 -0
  35. package/dist/AudioSource.d.ts.map +1 -0
  36. package/dist/AudioSource.js +58 -0
  37. package/dist/AudioSource.js.map +1 -0
  38. package/dist/AudioSystem.d.ts +13 -0
  39. package/dist/AudioSystem.d.ts.map +1 -0
  40. package/dist/AudioSystem.js +28 -0
  41. package/dist/AudioSystem.js.map +1 -0
  42. package/dist/SceneAudioBridge.d.ts +19 -0
  43. package/dist/SceneAudioBridge.d.ts.map +1 -0
  44. package/dist/SceneAudioBridge.js +37 -0
  45. package/dist/SceneAudioBridge.js.map +1 -0
  46. package/dist/SpatialAudio.d.ts +17 -0
  47. package/dist/SpatialAudio.d.ts.map +1 -0
  48. package/dist/SpatialAudio.js +22 -0
  49. package/dist/SpatialAudio.js.map +1 -0
  50. package/dist/SpatialAudioFixtures.d.ts +38 -0
  51. package/dist/SpatialAudioFixtures.d.ts.map +1 -0
  52. package/dist/SpatialAudioFixtures.js +94 -0
  53. package/dist/SpatialAudioFixtures.js.map +1 -0
  54. package/dist/effects/Filter.d.ts +13 -0
  55. package/dist/effects/Filter.d.ts.map +1 -0
  56. package/dist/effects/Filter.js +31 -0
  57. package/dist/effects/Filter.js.map +1 -0
  58. package/dist/effects/Reverb.d.ts +12 -0
  59. package/dist/effects/Reverb.d.ts.map +1 -0
  60. package/dist/effects/Reverb.js +23 -0
  61. package/dist/effects/Reverb.js.map +1 -0
  62. package/dist/index.d.ts +25 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +15 -0
  65. package/dist/index.js.map +1 -0
  66. package/package.json +16 -0
  67. package/src/AdaptiveMusicFixtures.ts +132 -0
  68. package/src/AudioBus.ts +39 -0
  69. package/src/AudioClip.ts +26 -0
  70. package/src/AudioContextManager.ts +74 -0
  71. package/src/AudioEffect.ts +7 -0
  72. package/src/AudioEffectsAnalysisFixtures.ts +311 -0
  73. package/src/AudioListener.ts +17 -0
  74. package/src/AudioMixer.ts +36 -0
  75. package/src/AudioSource.ts +75 -0
  76. package/src/AudioSystem.ts +34 -0
  77. package/src/SceneAudioBridge.ts +49 -0
  78. package/src/SpatialAudio.ts +36 -0
  79. package/src/SpatialAudioFixtures.ts +137 -0
  80. package/src/effects/Filter.ts +39 -0
  81. package/src/effects/Reverb.ts +30 -0
  82. package/src/index.ts +33 -0
  83. package/tests/audio.test.ts +121 -0
  84. package/tsconfig.json +7 -0
@@ -0,0 +1,311 @@
1
+ export type AudioCompressorPreset = "gentle" | "moderate" | "aggressive" | "limiter" | "vocal" | "drums" | "master";
2
+ export type AudioDelayPreset = "slapback" | "short_echo" | "medium_echo" | "long_echo" | "ping_pong" | "tape_echo";
3
+ export type AudioChorusPreset = "subtle" | "classic" | "deep" | "wide" | "ensemble" | "vibrato";
4
+ export type AudioDistortionCurve = "sigmoid" | "asymmetric" | "hardclip" | "softclip" | "fuzz" | "saturation";
5
+
6
+ export interface AudioEffectsAnalysisFixtureOptions {
7
+ readonly preset?: AudioCompressorPreset;
8
+ readonly inputPeakDb?: number;
9
+ readonly intensity?: number;
10
+ }
11
+
12
+ export interface AudioEqBandFixture {
13
+ readonly name: string;
14
+ readonly type: "lowshelf" | "peaking" | "highshelf";
15
+ readonly frequencyHz: number;
16
+ readonly q: number;
17
+ readonly gainDb: number;
18
+ readonly enabled: boolean;
19
+ }
20
+
21
+ export interface AudioSpectrumBandFixture {
22
+ readonly name: string;
23
+ readonly minHz: number;
24
+ readonly maxHz: number;
25
+ readonly magnitude: number;
26
+ }
27
+
28
+ export interface AudioEffectsAnalysisFixture {
29
+ readonly source: "origin-master-audio-effects-analysis-adapted";
30
+ readonly effectChain: readonly ["parametric-eq", "dynamic-compressor", "delay", "chorus", "distortion", "filter", "spectrum-analyzer"];
31
+ readonly compressor: {
32
+ readonly preset: AudioCompressorPreset;
33
+ readonly thresholdDb: number;
34
+ readonly kneeDb: number;
35
+ readonly ratio: number;
36
+ readonly attackSeconds: number;
37
+ readonly releaseSeconds: number;
38
+ readonly inputPeakDb: number;
39
+ readonly outputPeakDb: number;
40
+ readonly gainReductionDb: number;
41
+ readonly makeupGainDb: number;
42
+ readonly limiterTriggered: boolean;
43
+ };
44
+ readonly eq: {
45
+ readonly bands: readonly AudioEqBandFixture[];
46
+ readonly activeBandCount: number;
47
+ readonly lowShelfGainDb: number;
48
+ readonly presenceGainDb: number;
49
+ readonly outputGain: number;
50
+ };
51
+ readonly delay: {
52
+ readonly source: "origin-master-delay-effect-adapted";
53
+ readonly preset: AudioDelayPreset;
54
+ readonly delayTimeSeconds: number;
55
+ readonly feedback: number;
56
+ readonly wetDryMix: number;
57
+ readonly filterFrequencyHz: number;
58
+ readonly pingPong: boolean;
59
+ readonly repeatsAboveNoiseFloor: number;
60
+ };
61
+ readonly chorus: {
62
+ readonly source: "origin-master-chorus-effect-adapted";
63
+ readonly preset: AudioChorusPreset;
64
+ readonly rateHz: number;
65
+ readonly depthSeconds: number;
66
+ readonly feedback: number;
67
+ readonly wetDryMix: number;
68
+ readonly delaySeconds: number;
69
+ readonly voices: number;
70
+ readonly stereoWidth: number;
71
+ };
72
+ readonly distortion: {
73
+ readonly source: "origin-master-distortion-effect-adapted";
74
+ readonly curve: AudioDistortionCurve;
75
+ readonly amount: number;
76
+ readonly preGain: number;
77
+ readonly postGain: number;
78
+ readonly wetDryMix: number;
79
+ readonly harmonicBoost: number;
80
+ readonly outputCeiling: number;
81
+ };
82
+ readonly filter: {
83
+ readonly source: "origin-master-filter-effect-adapted";
84
+ readonly type: "lowpass" | "highpass" | "bandpass";
85
+ readonly frequencyHz: number;
86
+ readonly q: number;
87
+ readonly resonanceDb: number;
88
+ readonly enabled: boolean;
89
+ };
90
+ readonly spectrum: {
91
+ readonly bands: readonly AudioSpectrumBandFixture[];
92
+ readonly barCount: number;
93
+ readonly bars: readonly number[];
94
+ readonly peakFrequencyHz: number;
95
+ readonly peakMagnitude: number;
96
+ readonly logarithmicScale: boolean;
97
+ };
98
+ readonly hash: string;
99
+ readonly claimBoundary: string;
100
+ readonly blockedClaims: readonly string[];
101
+ }
102
+
103
+ const COMPRESSOR_PRESETS: Record<AudioCompressorPreset, {
104
+ readonly thresholdDb: number;
105
+ readonly kneeDb: number;
106
+ readonly ratio: number;
107
+ readonly attackSeconds: number;
108
+ readonly releaseSeconds: number;
109
+ }> = {
110
+ gentle: { thresholdDb: -30, kneeDb: 20, ratio: 2, attackSeconds: 0.01, releaseSeconds: 0.3 },
111
+ moderate: { thresholdDb: -24, kneeDb: 30, ratio: 4, attackSeconds: 0.005, releaseSeconds: 0.25 },
112
+ aggressive: { thresholdDb: -18, kneeDb: 10, ratio: 8, attackSeconds: 0.001, releaseSeconds: 0.1 },
113
+ limiter: { thresholdDb: -6, kneeDb: 0, ratio: 20, attackSeconds: 0.001, releaseSeconds: 0.05 },
114
+ vocal: { thresholdDb: -20, kneeDb: 15, ratio: 3, attackSeconds: 0.005, releaseSeconds: 0.2 },
115
+ drums: { thresholdDb: -18, kneeDb: 5, ratio: 6, attackSeconds: 0.001, releaseSeconds: 0.08 },
116
+ master: { thresholdDb: -14, kneeDb: 8, ratio: 3, attackSeconds: 0.004, releaseSeconds: 0.16 }
117
+ };
118
+
119
+ const SPECTRUM_BANDS: readonly Omit<AudioSpectrumBandFixture, "magnitude">[] = [
120
+ { name: "Sub Bass", minHz: 20, maxHz: 60 },
121
+ { name: "Bass", minHz: 60, maxHz: 250 },
122
+ { name: "Low Mid", minHz: 250, maxHz: 500 },
123
+ { name: "Mid", minHz: 500, maxHz: 2_000 },
124
+ { name: "High Mid", minHz: 2_000, maxHz: 4_000 },
125
+ { name: "Presence", minHz: 4_000, maxHz: 6_000 },
126
+ { name: "Brilliance", minHz: 6_000, maxHz: 20_000 }
127
+ ];
128
+
129
+ export function sampleAudioEffectsAnalysisFixture(options: AudioEffectsAnalysisFixtureOptions = {}): AudioEffectsAnalysisFixture {
130
+ const preset = options.preset ?? "master";
131
+ const compressorPreset = COMPRESSOR_PRESETS[preset];
132
+ const intensity = clamp01(options.intensity ?? 0.72);
133
+ const inputPeakDb = clamp(options.inputPeakDb ?? -3.5, -60, 6);
134
+ const overThresholdDb = Math.max(0, inputPeakDb - compressorPreset.thresholdDb);
135
+ const compressedOverage = overThresholdDb / compressorPreset.ratio;
136
+ const gainReductionDb = round(Math.max(0, overThresholdDb - compressedOverage));
137
+ const makeupGainDb = round(Math.min(6, gainReductionDb * 0.38));
138
+ const outputPeakDb = round(inputPeakDb - gainReductionDb + makeupGainDb);
139
+ const eqBands = eqBandsForIntensity(intensity);
140
+ const spectrumBands = SPECTRUM_BANDS.map((band, index) => ({
141
+ ...band,
142
+ magnitude: round(clamp01((Math.sin(index * 1.37 + intensity * 2.1) + 1) * 0.29 + intensity * (index === 1 || index === 5 ? 0.28 : 0.16)))
143
+ }));
144
+ const bars = spectrumBars(spectrumBands, 16);
145
+ const peak = spectrumBands.reduce((best, band) => band.magnitude > best.magnitude ? band : best, spectrumBands[0]!);
146
+ const fixture = {
147
+ source: "origin-master-audio-effects-analysis-adapted" as const,
148
+ effectChain: ["parametric-eq", "dynamic-compressor", "delay", "chorus", "distortion", "filter", "spectrum-analyzer"] as const,
149
+ compressor: {
150
+ preset,
151
+ ...compressorPreset,
152
+ inputPeakDb: round(inputPeakDb),
153
+ outputPeakDb,
154
+ gainReductionDb,
155
+ makeupGainDb,
156
+ limiterTriggered: outputPeakDb >= -0.5 || preset === "limiter"
157
+ },
158
+ eq: {
159
+ bands: eqBands,
160
+ activeBandCount: eqBands.filter((band) => band.enabled).length,
161
+ lowShelfGainDb: eqBands[0]?.gainDb ?? 0,
162
+ presenceGainDb: eqBands.find((band) => band.name === "presence-lift")?.gainDb ?? 0,
163
+ outputGain: round(1 - Math.max(0, outputPeakDb) * 0.05)
164
+ },
165
+ delay: delayFixture(intensity),
166
+ chorus: chorusFixture(intensity),
167
+ distortion: distortionFixture(intensity, inputPeakDb),
168
+ filter: filterFixture(intensity),
169
+ spectrum: {
170
+ bands: spectrumBands,
171
+ barCount: bars.length,
172
+ bars,
173
+ peakFrequencyHz: Math.round((peak.minHz + peak.maxHz) / 2),
174
+ peakMagnitude: peak.magnitude,
175
+ logarithmicScale: true
176
+ },
177
+ hash: "",
178
+ claimBoundary: "Audio effects fixture adapts old compressor, EQ, delay, chorus, distortion, filter, and spectrum-analysis concepts for deterministic telemetry; it does not instantiate a production WebAudio mastering graph or prove audio middleware parity.",
179
+ blockedClaims: [
180
+ "production WebAudio effects graph parity",
181
+ "production delay/chorus/distortion/filter node graph parity",
182
+ "real-time FFT browser analyzer certification",
183
+ "audio middleware mastering parity",
184
+ "Unity Audio Mixer parity",
185
+ "Unreal Audio Mixer parity"
186
+ ] as const
187
+ };
188
+ return {
189
+ ...fixture,
190
+ hash: stableHash([
191
+ fixture.compressor.preset,
192
+ fixture.compressor.outputPeakDb,
193
+ fixture.compressor.gainReductionDb,
194
+ fixture.delay.preset,
195
+ fixture.delay.repeatsAboveNoiseFloor,
196
+ fixture.chorus.voices,
197
+ fixture.chorus.stereoWidth,
198
+ fixture.distortion.curve,
199
+ fixture.distortion.harmonicBoost,
200
+ fixture.filter.frequencyHz,
201
+ fixture.eq.bands.map((band) => `${band.frequencyHz}:${band.gainDb}`).join(","),
202
+ fixture.spectrum.peakFrequencyHz,
203
+ fixture.spectrum.peakMagnitude
204
+ ].join("|"))
205
+ };
206
+ }
207
+
208
+ function delayFixture(intensity: number): AudioEffectsAnalysisFixture["delay"] {
209
+ const preset: AudioDelayPreset = intensity > 0.7 ? "tape_echo" : intensity > 0.52 ? "medium_echo" : "short_echo";
210
+ const delayTimeSeconds = preset === "tape_echo" ? 0.42 : preset === "medium_echo" ? 0.5 : 0.25;
211
+ const feedback = round(preset === "tape_echo" ? 0.58 : 0.22 + intensity * 0.35);
212
+ const wetDryMix = round(0.24 + intensity * 0.34);
213
+ return {
214
+ source: "origin-master-delay-effect-adapted",
215
+ preset,
216
+ delayTimeSeconds,
217
+ feedback,
218
+ wetDryMix,
219
+ filterFrequencyHz: Math.round(7_500 - intensity * 4_600),
220
+ pingPong: preset === "tape_echo" || intensity > 0.82,
221
+ repeatsAboveNoiseFloor: Math.max(1, Math.min(8, Math.ceil(Math.log(0.04) / Math.log(Math.max(0.05, feedback)))))
222
+ };
223
+ }
224
+
225
+ function chorusFixture(intensity: number): AudioEffectsAnalysisFixture["chorus"] {
226
+ const preset: AudioChorusPreset = intensity > 0.72 ? "ensemble" : intensity > 0.5 ? "classic" : "subtle";
227
+ const voices = preset === "ensemble" ? 4 : preset === "classic" ? 2 : 1;
228
+ return {
229
+ source: "origin-master-chorus-effect-adapted",
230
+ preset,
231
+ rateHz: round(0.85 + intensity * 2.2),
232
+ depthSeconds: round(0.0012 + intensity * 0.006),
233
+ feedback: round(0.08 + intensity * 0.26),
234
+ wetDryMix: round(0.18 + intensity * 0.42),
235
+ delaySeconds: round(0.012 + intensity * 0.018),
236
+ voices,
237
+ stereoWidth: round(Math.min(1, 0.36 + voices * 0.13 + intensity * 0.22))
238
+ };
239
+ }
240
+
241
+ function distortionFixture(intensity: number, inputPeakDb: number): AudioEffectsAnalysisFixture["distortion"] {
242
+ const curve: AudioDistortionCurve = intensity > 0.76 ? "saturation" : intensity > 0.58 ? "softclip" : "sigmoid";
243
+ const amount = round(18 + intensity * 62);
244
+ const preGain = round(1 + intensity * 2.4);
245
+ const postGain = round(Math.max(0.42, 1 - intensity * 0.36));
246
+ const harmonicBoost = round(Math.max(0, amount / 100 * preGain * Math.max(0.25, (inputPeakDb + 24) / 24)));
247
+ return {
248
+ source: "origin-master-distortion-effect-adapted",
249
+ curve,
250
+ amount,
251
+ preGain,
252
+ postGain,
253
+ wetDryMix: round(0.2 + intensity * 0.55),
254
+ harmonicBoost,
255
+ outputCeiling: round(Math.min(0.98, postGain + harmonicBoost * 0.08))
256
+ };
257
+ }
258
+
259
+ function filterFixture(intensity: number): AudioEffectsAnalysisFixture["filter"] {
260
+ const type = intensity > 0.62 ? "lowpass" : "bandpass";
261
+ const frequencyHz = Math.round(type === "lowpass" ? 6_800 - intensity * 2_400 : 1_200 + intensity * 2_200);
262
+ const q = round(0.7 + intensity * 1.6);
263
+ return {
264
+ source: "origin-master-filter-effect-adapted",
265
+ type,
266
+ frequencyHz,
267
+ q,
268
+ resonanceDb: round(q * 2.6),
269
+ enabled: true
270
+ };
271
+ }
272
+
273
+ function eqBandsForIntensity(intensity: number): readonly AudioEqBandFixture[] {
274
+ return [
275
+ { name: "low-body", type: "lowshelf", frequencyHz: 120, q: 0.7, gainDb: round(2.4 * intensity), enabled: true },
276
+ { name: "mud-cut", type: "peaking", frequencyHz: 360, q: 1.1, gainDb: round(-1.8 * intensity), enabled: true },
277
+ { name: "presence-lift", type: "peaking", frequencyHz: 3_200, q: 0.9, gainDb: round(2.2 * intensity), enabled: true },
278
+ { name: "air", type: "highshelf", frequencyHz: 8_500, q: 0.8, gainDb: round(1.6 * intensity), enabled: true }
279
+ ];
280
+ }
281
+
282
+ function spectrumBars(bands: readonly AudioSpectrumBandFixture[], count: number): readonly number[] {
283
+ const bars: number[] = [];
284
+ for (let index = 0; index < count; index += 1) {
285
+ const band = bands[Math.min(bands.length - 1, Math.floor(index / count * bands.length))]!;
286
+ const shimmer = 0.85 + 0.15 * Math.sin(index * 0.9);
287
+ bars.push(round(clamp01(band.magnitude * shimmer)));
288
+ }
289
+ return bars;
290
+ }
291
+
292
+ function clamp01(value: number): number {
293
+ return clamp(value, 0, 1);
294
+ }
295
+
296
+ function clamp(value: number, min: number, max: number): number {
297
+ return Math.max(min, Math.min(max, Number.isFinite(value) ? value : min));
298
+ }
299
+
300
+ function round(value: number): number {
301
+ return Number(value.toFixed(4));
302
+ }
303
+
304
+ function stableHash(value: string): string {
305
+ let hash = 0x811c9dc5;
306
+ for (let index = 0; index < value.length; index += 1) {
307
+ hash ^= value.charCodeAt(index);
308
+ hash = Math.imul(hash, 0x01000193);
309
+ }
310
+ return (hash >>> 0).toString(16).padStart(8, "0");
311
+ }
@@ -0,0 +1,17 @@
1
+ export interface Vec3Like {
2
+ readonly x: number;
3
+ readonly y: number;
4
+ readonly z: number;
5
+ }
6
+
7
+ export class AudioListener {
8
+ position: Vec3Like = { x: 0, y: 0, z: 0 };
9
+ forward: Vec3Like = { x: 0, y: 0, z: -1 };
10
+ up: Vec3Like = { x: 0, y: 1, z: 0 };
11
+
12
+ setTransform(position: Vec3Like, forward: Vec3Like = this.forward, up: Vec3Like = this.up): void {
13
+ this.position = { ...position };
14
+ this.forward = { ...forward };
15
+ this.up = { ...up };
16
+ }
17
+ }
@@ -0,0 +1,36 @@
1
+ import { AudioBus } from "./AudioBus";
2
+ import type { AudioContextLike } from "./AudioContextManager";
3
+
4
+ export class AudioMixer {
5
+ readonly master: AudioBus;
6
+ private readonly buses = new Map<string, AudioBus>();
7
+
8
+ constructor(private readonly context: AudioContextLike) {
9
+ this.master = new AudioBus("master", context);
10
+ this.buses.set(this.master.name, this.master);
11
+ }
12
+
13
+ createBus(name: string, destination: AudioNode = this.master.input): AudioBus {
14
+ if (this.buses.has(name)) {
15
+ throw new Error(`Audio bus already exists: ${name}`);
16
+ }
17
+ const bus = new AudioBus(name, this.context, destination);
18
+ this.buses.set(name, bus);
19
+ return bus;
20
+ }
21
+
22
+ getBus(name: string): AudioBus {
23
+ const bus = this.buses.get(name);
24
+ if (!bus) {
25
+ throw new Error(`Unknown audio bus: ${name}`);
26
+ }
27
+ return bus;
28
+ }
29
+
30
+ dispose(): void {
31
+ for (const bus of [...this.buses.values()].reverse()) {
32
+ bus.dispose();
33
+ }
34
+ this.buses.clear();
35
+ }
36
+ }
@@ -0,0 +1,75 @@
1
+ import type { AudioClip } from "./AudioClip";
2
+ import type { AudioContextLike } from "./AudioContextManager";
3
+
4
+ export type AudioSourceState = "idle" | "playing" | "paused" | "stopped";
5
+
6
+ export interface AudioSourceOptions {
7
+ readonly context: AudioContextLike;
8
+ readonly destination?: AudioNode;
9
+ readonly clip?: AudioClip;
10
+ readonly loop?: boolean;
11
+ readonly volume?: number;
12
+ }
13
+
14
+ export class AudioSource {
15
+ clip?: AudioClip;
16
+ loop: boolean;
17
+ readonly gain: GainNode;
18
+
19
+ private stateRef: AudioSourceState = "idle";
20
+ private node?: AudioBufferSourceNode;
21
+
22
+ constructor(private readonly options: AudioSourceOptions) {
23
+ this.clip = options.clip;
24
+ this.loop = options.loop ?? false;
25
+ this.gain = options.context.createGain();
26
+ this.gain.gain.value = options.volume ?? 1;
27
+ this.gain.connect(options.destination ?? options.context.destination);
28
+ }
29
+
30
+ get state(): AudioSourceState {
31
+ return this.stateRef;
32
+ }
33
+
34
+ play(when = 0): void {
35
+ if (!this.clip) {
36
+ throw new Error("Cannot play an AudioSource without an AudioClip");
37
+ }
38
+ this.stop();
39
+ const source = this.options.context.createBufferSource();
40
+ source.buffer = this.clip.buffer;
41
+ source.loop = this.loop;
42
+ source.connect(this.gain);
43
+ source.start(when);
44
+ source.onended = () => {
45
+ if (this.node === source && this.stateRef === "playing") {
46
+ this.stateRef = "stopped";
47
+ }
48
+ };
49
+ this.node = source;
50
+ this.stateRef = "playing";
51
+ }
52
+
53
+ stop(when = 0): void {
54
+ if (this.node) {
55
+ this.node.stop(when);
56
+ this.node.disconnect();
57
+ this.node = undefined;
58
+ }
59
+ if (this.stateRef !== "idle") {
60
+ this.stateRef = "stopped";
61
+ }
62
+ }
63
+
64
+ setVolume(value: number): void {
65
+ if (!Number.isFinite(value) || value < 0) {
66
+ throw new Error("Audio source volume must be a non-negative finite number");
67
+ }
68
+ this.gain.gain.value = value;
69
+ }
70
+
71
+ dispose(): void {
72
+ this.stop();
73
+ this.gain.disconnect();
74
+ }
75
+ }
@@ -0,0 +1,34 @@
1
+ import { AudioContextManager, type AudioContextManagerOptions } from "./AudioContextManager";
2
+ import { AudioMixer } from "./AudioMixer";
3
+
4
+ export class AudioSystem {
5
+ readonly contextManager: AudioContextManager;
6
+ private mixerRef?: AudioMixer;
7
+
8
+ constructor(options: AudioContextManagerOptions = {}) {
9
+ this.contextManager = new AudioContextManager(options);
10
+ }
11
+
12
+ get mixer(): AudioMixer {
13
+ this.mixerRef ??= new AudioMixer(this.contextManager.context);
14
+ return this.mixerRef;
15
+ }
16
+
17
+ async unlock(): Promise<void> {
18
+ await this.contextManager.unlock();
19
+ }
20
+
21
+ async suspend(): Promise<void> {
22
+ await this.contextManager.suspend();
23
+ }
24
+
25
+ async resume(): Promise<void> {
26
+ await this.contextManager.resume();
27
+ }
28
+
29
+ async dispose(): Promise<void> {
30
+ this.mixerRef?.dispose();
31
+ this.mixerRef = undefined;
32
+ await this.contextManager.dispose();
33
+ }
34
+ }
@@ -0,0 +1,49 @@
1
+ import type { Scene, SceneNode } from "@aura3d/scene";
2
+ import type { AudioListener } from "./AudioListener";
3
+ import type { SpatialAudio } from "./SpatialAudio";
4
+
5
+ export interface SceneAudioSourceBinding {
6
+ readonly node: SceneNode;
7
+ readonly spatial: SpatialAudio;
8
+ }
9
+
10
+ export class SceneAudioBridge {
11
+ private listenerBinding?: { readonly node: SceneNode; readonly listener: AudioListener };
12
+ private readonly sources = new Set<SceneAudioSourceBinding>();
13
+
14
+ constructor(private readonly scene?: Scene) {}
15
+
16
+ bindListener(node: SceneNode, listener: AudioListener): void {
17
+ this.listenerBinding = { node, listener };
18
+ }
19
+
20
+ unbindListener(): void {
21
+ this.listenerBinding = undefined;
22
+ }
23
+
24
+ bindSource(node: SceneNode, spatial: SpatialAudio): SceneAudioSourceBinding {
25
+ const binding = { node, spatial };
26
+ this.sources.add(binding);
27
+ return binding;
28
+ }
29
+
30
+ unbindSource(binding: SceneAudioSourceBinding): void {
31
+ this.sources.delete(binding);
32
+ }
33
+
34
+ update(): void {
35
+ this.scene?.updateWorldTransforms();
36
+ if (this.listenerBinding) {
37
+ this.listenerBinding.listener.setTransform(worldPosition(this.listenerBinding.node));
38
+ }
39
+ for (const binding of this.sources) {
40
+ binding.spatial.setPosition(worldPosition(binding.node));
41
+ }
42
+ }
43
+ }
44
+
45
+ function worldPosition(node: SceneNode): { readonly x: number; readonly y: number; readonly z: number } {
46
+ node.updateWorldTransform();
47
+ const matrix = node.transform.worldMatrix as readonly number[];
48
+ return { x: matrix[12], y: matrix[13], z: matrix[14] };
49
+ }
@@ -0,0 +1,36 @@
1
+ import type { AudioContextLike } from "./AudioContextManager";
2
+ import type { Vec3Like } from "./AudioListener";
3
+
4
+ export interface SpatialAudioOptions {
5
+ readonly context: AudioContextLike;
6
+ readonly destination?: AudioNode;
7
+ readonly position?: Vec3Like;
8
+ readonly maxDistance?: number;
9
+ readonly refDistance?: number;
10
+ readonly rolloffFactor?: number;
11
+ }
12
+
13
+ export class SpatialAudio {
14
+ readonly panner: PannerNode;
15
+
16
+ constructor(options: SpatialAudioOptions) {
17
+ this.panner = options.context.createPanner();
18
+ this.panner.panningModel = "HRTF";
19
+ this.panner.distanceModel = "inverse";
20
+ this.panner.maxDistance = options.maxDistance ?? 10_000;
21
+ this.panner.refDistance = options.refDistance ?? 1;
22
+ this.panner.rolloffFactor = options.rolloffFactor ?? 1;
23
+ this.panner.connect(options.destination ?? options.context.destination);
24
+ this.setPosition(options.position ?? { x: 0, y: 0, z: 0 });
25
+ }
26
+
27
+ setPosition(position: Vec3Like): void {
28
+ this.panner.positionX.value = position.x;
29
+ this.panner.positionY.value = position.y;
30
+ this.panner.positionZ.value = position.z;
31
+ }
32
+
33
+ dispose(): void {
34
+ this.panner.disconnect();
35
+ }
36
+ }