@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,137 @@
1
+ export type AudioOcclusionLevel = "none" | "light" | "medium" | "heavy" | "complete";
2
+
3
+ export interface AudioEnvironmentFixtureOptions {
4
+ readonly sourcePosition?: readonly [number, number, number];
5
+ readonly listenerPosition?: readonly [number, number, number];
6
+ readonly sourceVelocity?: readonly [number, number, number];
7
+ readonly listenerVelocity?: readonly [number, number, number];
8
+ readonly obstacleCount?: number;
9
+ readonly reverbZoneRadius?: number;
10
+ readonly reverbZoneDistance?: number;
11
+ readonly baseFrequencyHz?: number;
12
+ }
13
+
14
+ export interface AudioEnvironmentFixture {
15
+ readonly source: "origin-master-spatial-audio-environment-adapted";
16
+ readonly distance: number;
17
+ readonly occlusion: {
18
+ readonly level: AudioOcclusionLevel;
19
+ readonly obstacleCount: number;
20
+ readonly lowpassHz: number;
21
+ readonly volume: number;
22
+ };
23
+ readonly doppler: {
24
+ readonly relativeVelocity: number;
25
+ readonly approaching: boolean;
26
+ readonly pitchFactor: number;
27
+ readonly frequencyShiftHz: number;
28
+ };
29
+ readonly reverb: {
30
+ readonly zoneRadius: number;
31
+ readonly zoneDistance: number;
32
+ readonly blend: number;
33
+ readonly wetLevel: number;
34
+ readonly dryLevel: number;
35
+ };
36
+ readonly hash: string;
37
+ readonly claimBoundary: string;
38
+ }
39
+
40
+ const OCCLUSION: Record<AudioOcclusionLevel, { readonly lowpassHz: number; readonly volume: number }> = {
41
+ none: { lowpassHz: 20_000, volume: 1 },
42
+ light: { lowpassHz: 8_000, volume: 0.9 },
43
+ medium: { lowpassHz: 3_000, volume: 0.7 },
44
+ heavy: { lowpassHz: 800, volume: 0.4 },
45
+ complete: { lowpassHz: 200, volume: 0.1 }
46
+ };
47
+
48
+ export function sampleAudioEnvironmentFixture(options: AudioEnvironmentFixtureOptions = {}): AudioEnvironmentFixture {
49
+ const sourcePosition = options.sourcePosition ?? [0.85, 0.15, 0] as const;
50
+ const listenerPosition = options.listenerPosition ?? [0, 0, 3] as const;
51
+ const sourceVelocity = options.sourceVelocity ?? [-2.5, 0.2, 0] as const;
52
+ const listenerVelocity = options.listenerVelocity ?? [0.7, 0, 0] as const;
53
+ const obstacleCount = Math.max(0, Math.floor(options.obstacleCount ?? 2));
54
+ const distance = vectorLength(sub(sourcePosition, listenerPosition));
55
+ const direction = normalize(sub(listenerPosition, sourcePosition));
56
+ const relativeVelocity = dot(sub(sourceVelocity, listenerVelocity), direction);
57
+ const approaching = relativeVelocity > 0;
58
+ const speedOfSound = 343;
59
+ const pitchFactor = clamp(speedOfSound / Math.max(1, speedOfSound - relativeVelocity), 0.5, 2);
60
+ const baseFrequencyHz = options.baseFrequencyHz ?? 440;
61
+ const level = occlusionLevel(obstacleCount);
62
+ const occlusion = OCCLUSION[level];
63
+ const zoneRadius = Math.max(0.001, options.reverbZoneRadius ?? 8);
64
+ const zoneDistance = Math.max(0, options.reverbZoneDistance ?? distance);
65
+ const blend = clamp(1 - zoneDistance / zoneRadius, 0, 1);
66
+ const fixture = {
67
+ source: "origin-master-spatial-audio-environment-adapted" as const,
68
+ distance: round(distance),
69
+ occlusion: {
70
+ level,
71
+ obstacleCount,
72
+ lowpassHz: occlusion.lowpassHz,
73
+ volume: occlusion.volume
74
+ },
75
+ doppler: {
76
+ relativeVelocity: round(relativeVelocity),
77
+ approaching,
78
+ pitchFactor: round(pitchFactor),
79
+ frequencyShiftHz: round(baseFrequencyHz * (pitchFactor - 1))
80
+ },
81
+ reverb: {
82
+ zoneRadius: round(zoneRadius),
83
+ zoneDistance: round(zoneDistance),
84
+ blend: round(blend),
85
+ wetLevel: round(0.55 * blend),
86
+ dryLevel: round(0.85 * (1 - blend * 0.5))
87
+ },
88
+ hash: "",
89
+ claimBoundary: "Spatial audio environment fixture adapts old occlusion, doppler, and reverb-zone math for deterministic telemetry; it does not implement authored audio middleware or Unity/Unreal acoustic parity."
90
+ };
91
+ return {
92
+ ...fixture,
93
+ hash: stableHash(`${fixture.distance}|${fixture.occlusion.level}|${fixture.occlusion.obstacleCount}|${fixture.doppler.pitchFactor}|${fixture.reverb.blend}`)
94
+ };
95
+ }
96
+
97
+ function occlusionLevel(obstacleCount: number): AudioOcclusionLevel {
98
+ if (obstacleCount <= 0) return "none";
99
+ if (obstacleCount === 1) return "light";
100
+ if (obstacleCount === 2) return "medium";
101
+ if (obstacleCount === 3) return "heavy";
102
+ return "complete";
103
+ }
104
+
105
+ function sub(left: readonly [number, number, number], right: readonly [number, number, number]): [number, number, number] {
106
+ return [left[0] - right[0], left[1] - right[1], left[2] - right[2]];
107
+ }
108
+
109
+ function dot(left: readonly [number, number, number], right: readonly [number, number, number]): number {
110
+ return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
111
+ }
112
+
113
+ function normalize(value: readonly [number, number, number]): [number, number, number] {
114
+ const length = vectorLength(value);
115
+ return length > 0 ? [value[0] / length, value[1] / length, value[2] / length] : [0, 0, 0];
116
+ }
117
+
118
+ function vectorLength(value: readonly [number, number, number]): number {
119
+ return Math.hypot(value[0], value[1], value[2]);
120
+ }
121
+
122
+ function clamp(value: number, min: number, max: number): number {
123
+ return Math.max(min, Math.min(max, Number.isFinite(value) ? value : min));
124
+ }
125
+
126
+ function round(value: number): number {
127
+ return Number(value.toFixed(4));
128
+ }
129
+
130
+ function stableHash(value: string): string {
131
+ let hash = 0x811c9dc5;
132
+ for (let index = 0; index < value.length; index += 1) {
133
+ hash ^= value.charCodeAt(index);
134
+ hash = Math.imul(hash, 0x01000193);
135
+ }
136
+ return (hash >>> 0).toString(16).padStart(8, "0");
137
+ }
@@ -0,0 +1,39 @@
1
+ import type { AudioContextLike } from "../AudioContextManager";
2
+ import type { AudioEffect } from "../AudioEffect";
3
+
4
+ export class FilterEffect implements AudioEffect {
5
+ readonly input: BiquadFilterNode;
6
+ readonly output: BiquadFilterNode;
7
+
8
+ constructor(context: AudioContextLike, type: BiquadFilterType = "lowpass") {
9
+ this.input = context.createBiquadFilter();
10
+ this.input.type = type;
11
+ this.output = this.input;
12
+ }
13
+
14
+ setFrequency(value: number): void {
15
+ if (!Number.isFinite(value) || value <= 0) {
16
+ throw new Error("Filter frequency must be positive");
17
+ }
18
+ this.input.frequency.value = value;
19
+ }
20
+
21
+ setQ(value: number): void {
22
+ if (!Number.isFinite(value) || value < 0) {
23
+ throw new Error("Filter Q must be non-negative");
24
+ }
25
+ this.input.Q.value = value;
26
+ }
27
+
28
+ connect(destination: AudioNode): AudioNode {
29
+ return this.output.connect(destination);
30
+ }
31
+
32
+ disconnect(): void {
33
+ this.output.disconnect();
34
+ }
35
+
36
+ dispose(): void {
37
+ this.disconnect();
38
+ }
39
+ }
@@ -0,0 +1,30 @@
1
+ import type { AudioContextLike } from "../AudioContextManager";
2
+ import type { AudioEffect } from "../AudioEffect";
3
+
4
+ export class ReverbEffect implements AudioEffect {
5
+ readonly input: ConvolverNode;
6
+ readonly output: ConvolverNode;
7
+
8
+ constructor(context: AudioContextLike, impulse?: AudioBuffer) {
9
+ this.input = context.createConvolver();
10
+ this.input.buffer = impulse ?? null;
11
+ this.output = this.input;
12
+ }
13
+
14
+ setImpulse(buffer: AudioBuffer): void {
15
+ this.input.buffer = buffer;
16
+ }
17
+
18
+ connect(destination: AudioNode): AudioNode {
19
+ return this.output.connect(destination);
20
+ }
21
+
22
+ disconnect(): void {
23
+ this.output.disconnect();
24
+ }
25
+
26
+ dispose(): void {
27
+ this.input.buffer = null;
28
+ this.disconnect();
29
+ }
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ export { AudioBus } from "./AudioBus";
2
+ export { AudioClip } from "./AudioClip";
3
+ export type { AudioClipOptions } from "./AudioClip";
4
+ export { sampleAudioEffectsAnalysisFixture } from "./AudioEffectsAnalysisFixtures";
5
+ export type {
6
+ AudioChorusPreset,
7
+ AudioCompressorPreset,
8
+ AudioDelayPreset,
9
+ AudioDistortionCurve,
10
+ AudioEffectsAnalysisFixture,
11
+ AudioEffectsAnalysisFixtureOptions,
12
+ AudioEqBandFixture,
13
+ AudioSpectrumBandFixture
14
+ } from "./AudioEffectsAnalysisFixtures";
15
+ export { sampleAdaptiveMusicFixture } from "./AdaptiveMusicFixtures";
16
+ export type { AdaptiveMusicCrossfadeCurve, AdaptiveMusicFixture, AdaptiveMusicFixtureOptions, AdaptiveMusicFixtureState, AdaptiveMusicLayerMix } from "./AdaptiveMusicFixtures";
17
+ export { sampleAudioEnvironmentFixture } from "./SpatialAudioFixtures";
18
+ export type { AudioEnvironmentFixture, AudioEnvironmentFixtureOptions, AudioOcclusionLevel } from "./SpatialAudioFixtures";
19
+ export { AudioContextManager } from "./AudioContextManager";
20
+ export type { AudioContextLike, AudioContextManagerOptions, AudioContextState } from "./AudioContextManager";
21
+ export type { AudioEffect } from "./AudioEffect";
22
+ export { AudioListener } from "./AudioListener";
23
+ export type { Vec3Like } from "./AudioListener";
24
+ export { AudioMixer } from "./AudioMixer";
25
+ export { AudioSource } from "./AudioSource";
26
+ export type { AudioSourceOptions, AudioSourceState } from "./AudioSource";
27
+ export { AudioSystem } from "./AudioSystem";
28
+ export { SceneAudioBridge } from "./SceneAudioBridge";
29
+ export type { SceneAudioSourceBinding } from "./SceneAudioBridge";
30
+ export { SpatialAudio } from "./SpatialAudio";
31
+ export type { SpatialAudioOptions } from "./SpatialAudio";
32
+ export { FilterEffect } from "./effects/Filter";
33
+ export { ReverbEffect } from "./effects/Reverb";
@@ -0,0 +1,121 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { Scene } from "@aura3d/scene";
4
+ import { AudioClip, AudioContextManager, AudioListener, AudioSource, SceneAudioBridge } from "../src/index";
5
+
6
+ class MockParam {
7
+ value = 0;
8
+ }
9
+
10
+ class MockNode {
11
+ connections: unknown[] = [];
12
+ connect(destination: unknown) {
13
+ this.connections.push(destination);
14
+ return destination;
15
+ }
16
+ disconnect() {
17
+ this.connections = [];
18
+ }
19
+ }
20
+
21
+ class MockGain extends MockNode {
22
+ gain = new MockParam();
23
+ }
24
+
25
+ class MockSource extends MockNode {
26
+ buffer: AudioBuffer | null = null;
27
+ loop = false;
28
+ onended: (() => void) | null = null;
29
+ started = false;
30
+ stopped = false;
31
+ start() {
32
+ this.started = true;
33
+ }
34
+ stop() {
35
+ this.stopped = true;
36
+ }
37
+ }
38
+
39
+ test("AudioContextManager unlocks, suspends, resumes, and disposes a mock context", async () => {
40
+ const context = {
41
+ state: "suspended",
42
+ destination: new MockNode() as unknown as AudioNode,
43
+ currentTime: 0,
44
+ async resume() {
45
+ this.state = "running";
46
+ },
47
+ async suspend() {
48
+ this.state = "suspended";
49
+ },
50
+ async close() {
51
+ this.state = "closed";
52
+ },
53
+ createGain: () => new MockGain() as unknown as GainNode,
54
+ createBufferSource: () => new MockSource() as unknown as AudioBufferSourceNode,
55
+ createPanner: () => new MockNode() as unknown as PannerNode,
56
+ createBiquadFilter: () => new MockNode() as unknown as BiquadFilterNode,
57
+ createConvolver: () => new MockNode() as unknown as ConvolverNode
58
+ };
59
+ const manager = new AudioContextManager({ context });
60
+
61
+ assert.equal(manager.state, "locked");
62
+ await manager.unlock();
63
+ assert.equal(manager.state, "running");
64
+ await manager.suspend();
65
+ assert.equal(manager.state, "suspended");
66
+ await manager.dispose();
67
+ assert.equal(manager.state, "closed");
68
+ });
69
+
70
+ test("AudioSource guards missing clip and tracks play/stop state", () => {
71
+ const sourceNode = new MockSource();
72
+ const context = {
73
+ state: "running",
74
+ destination: new MockNode() as unknown as AudioNode,
75
+ currentTime: 0,
76
+ resume: async () => {},
77
+ suspend: async () => {},
78
+ close: async () => {},
79
+ createGain: () => new MockGain() as unknown as GainNode,
80
+ createBufferSource: () => sourceNode as unknown as AudioBufferSourceNode,
81
+ createPanner: () => new MockNode() as unknown as PannerNode,
82
+ createBiquadFilter: () => new MockNode() as unknown as BiquadFilterNode,
83
+ createConvolver: () => new MockNode() as unknown as ConvolverNode
84
+ };
85
+ const source = new AudioSource({ context });
86
+ assert.throws(() => source.play(), /without an AudioClip/);
87
+
88
+ source.clip = new AudioClip({
89
+ buffer: { duration: 1, numberOfChannels: 1, sampleRate: 44100 } as AudioBuffer
90
+ });
91
+ source.play();
92
+ assert.equal(source.state, "playing");
93
+ assert.equal(sourceNode.started, true);
94
+ source.stop();
95
+ assert.equal(source.state, "stopped");
96
+ });
97
+
98
+ test("SceneAudioBridge syncs listener and spatial source positions from scene nodes", () => {
99
+ const scene = new Scene();
100
+ const listenerNode = scene.createNode("listener");
101
+ const sourceNode = scene.createNode("source");
102
+ scene.root.addChild(listenerNode);
103
+ scene.root.addChild(sourceNode);
104
+ listenerNode.transform.setPosition(1, 2, 3);
105
+ sourceNode.transform.setPosition(4, 5, 6);
106
+
107
+ const listener = new AudioListener();
108
+ const spatial = {
109
+ position: { x: 0, y: 0, z: 0 },
110
+ setPosition(position: { readonly x: number; readonly y: number; readonly z: number }) {
111
+ this.position = { ...position };
112
+ }
113
+ };
114
+ const bridge = new SceneAudioBridge(scene);
115
+ bridge.bindListener(listenerNode, listener);
116
+ bridge.bindSource(sourceNode, spatial as never);
117
+ bridge.update();
118
+
119
+ assert.deepEqual(listener.position, { x: 1, y: 2, z: 3 });
120
+ assert.deepEqual(spatial.position, { x: 4, y: 5, z: 6 });
121
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "noEmit": true
5
+ },
6
+ "include": ["src/**/*.ts"]
7
+ }