@aura3d/audio 1.4.0 → 1.4.2

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.
@@ -1,252 +0,0 @@
1
- import { AudioClip } from "./AudioClip";
2
- import type { AudioContextLike } from "./AudioContextManager";
3
-
4
- export interface AudioDecodeContextLike extends AudioContextLike {
5
- decodeAudioData(data: ArrayBuffer): Promise<AudioBuffer> | AudioBuffer;
6
- }
7
-
8
- export interface AudioFileAssetLike {
9
- readonly kind?: string;
10
- readonly id?: string;
11
- readonly type?: string;
12
- readonly format?: string;
13
- readonly url: string;
14
- readonly hash?: string;
15
- readonly sizeBytes?: number;
16
- readonly license?: string;
17
- }
18
-
19
- export type AudioFileInput = string | URL | AudioFileAssetLike | AudioClip;
20
-
21
- export interface AudioFileRequest {
22
- readonly input: AudioFileInput;
23
- readonly signal?: AbortSignal;
24
- readonly cacheKey?: string;
25
- readonly name?: string;
26
- }
27
-
28
- export interface AudioFileManagerOptions {
29
- readonly context: AudioDecodeContextLike;
30
- readonly fetch?: (url: string, init?: { readonly signal?: AbortSignal }) => Promise<AudioFileFetchResponseLike>;
31
- readonly cache?: Map<string, AudioClip>;
32
- readonly validateTypedAudioAssets?: boolean;
33
- }
34
-
35
- export interface AudioFileFetchResponseLike {
36
- readonly ok: boolean;
37
- readonly status: number;
38
- arrayBuffer(): Promise<ArrayBuffer>;
39
- }
40
-
41
- export interface ResolvedAudioFileRequest {
42
- readonly url: string;
43
- readonly cacheKey: string;
44
- readonly name?: string;
45
- readonly asset?: AudioFileAssetLike;
46
- }
47
-
48
- export interface EpisodeAudioAssetRequirement {
49
- readonly id: string;
50
- readonly role?: "dialogue" | "music" | "sfx" | "ambient";
51
- readonly required?: boolean;
52
- readonly requireLicense?: boolean;
53
- }
54
-
55
- export interface EpisodeAudioAssetDiagnostic {
56
- readonly severity: "error" | "warning";
57
- readonly code: string;
58
- readonly message: string;
59
- readonly assetId: string;
60
- }
61
-
62
- export interface EpisodeAudioAssetReadiness {
63
- readonly ok: boolean;
64
- readonly requiredCount: number;
65
- readonly readyCount: number;
66
- readonly missingAssetIds: readonly string[];
67
- readonly diagnostics: readonly EpisodeAudioAssetDiagnostic[];
68
- }
69
-
70
- export class AudioFileManager {
71
- private readonly cache: Map<string, AudioClip>;
72
- private readonly inflight = new Map<string, Promise<AudioClip>>();
73
-
74
- constructor(private readonly options: AudioFileManagerOptions) {
75
- this.cache = options.cache ?? new Map();
76
- }
77
-
78
- resolve(input: AudioFileInput, request: Omit<AudioFileRequest, "input"> = {}): ResolvedAudioFileRequest {
79
- if (input instanceof AudioClip) {
80
- const name = request.name ?? input.name;
81
- return {
82
- url: name ? `clip:${name}` : "clip:anonymous",
83
- cacheKey: request.cacheKey ?? (name ? `clip:${name}` : "clip:anonymous"),
84
- ...(name ? { name } : {})
85
- };
86
- }
87
-
88
- if (input instanceof URL) {
89
- const url = input.toString();
90
- return { url, cacheKey: request.cacheKey ?? url, ...(request.name ? { name: request.name } : {}) };
91
- }
92
-
93
- if (typeof input === "string") {
94
- const url = input.trim();
95
- if (!url) {
96
- throw new Error("Audio file URL must be a non-empty string");
97
- }
98
- return { url, cacheKey: request.cacheKey ?? url, ...(request.name ? { name: request.name } : {}) };
99
- }
100
-
101
- if (this.options.validateTypedAudioAssets !== false && input.kind === "aura-asset-ref" && input.type !== "audio") {
102
- throw new Error(`Typed asset "${input.id ?? input.url}" is not an audio asset`);
103
- }
104
- const url = input.url.trim();
105
- if (!url) {
106
- throw new Error(`Audio asset "${input.id ?? "unknown"}" is missing a URL`);
107
- }
108
- const cacheKey = request.cacheKey ?? input.id ?? url;
109
- return {
110
- url,
111
- cacheKey,
112
- name: request.name ?? input.id,
113
- asset: input
114
- };
115
- }
116
-
117
- has(input: AudioFileInput, request: Omit<AudioFileRequest, "input"> = {}): boolean {
118
- return this.cache.has(this.resolve(input, request).cacheKey);
119
- }
120
-
121
- getCached(input: AudioFileInput, request: Omit<AudioFileRequest, "input"> = {}): AudioClip | undefined {
122
- return this.cache.get(this.resolve(input, request).cacheKey);
123
- }
124
-
125
- async load(input: AudioFileInput, request: Omit<AudioFileRequest, "input"> = {}): Promise<AudioClip> {
126
- if (input instanceof AudioClip) {
127
- const resolved = this.resolve(input, request);
128
- this.cache.set(resolved.cacheKey, input);
129
- return input;
130
- }
131
-
132
- const resolved = this.resolve(input, request);
133
- const cached = this.cache.get(resolved.cacheKey);
134
- if (cached) return cached;
135
-
136
- const pending = this.inflight.get(resolved.cacheKey);
137
- if (pending) return pending;
138
-
139
- const promise = this.loadResolved(resolved, request.signal);
140
- this.inflight.set(resolved.cacheKey, promise);
141
- try {
142
- const clip = await promise;
143
- this.cache.set(resolved.cacheKey, clip);
144
- return clip;
145
- } finally {
146
- this.inflight.delete(resolved.cacheKey);
147
- }
148
- }
149
-
150
- async loadMany(inputs: readonly AudioFileInput[], request: Omit<AudioFileRequest, "input"> = {}): Promise<readonly AudioClip[]> {
151
- return Promise.all(inputs.map((input) => this.load(input, request)));
152
- }
153
-
154
- clear(input?: AudioFileInput): void {
155
- if (input === undefined) {
156
- this.cache.clear();
157
- this.inflight.clear();
158
- return;
159
- }
160
- const { cacheKey } = this.resolve(input);
161
- this.cache.delete(cacheKey);
162
- this.inflight.delete(cacheKey);
163
- }
164
-
165
- private async loadResolved(resolved: ResolvedAudioFileRequest, signal?: AbortSignal): Promise<AudioClip> {
166
- const fetchAudio = this.options.fetch ?? defaultFetchAudio;
167
- const response = await fetchAudio(resolved.url, { signal });
168
- if (!response.ok) {
169
- throw new Error(`Audio request failed with ${response.status}: ${resolved.url}`);
170
- }
171
-
172
- const encoded = await response.arrayBuffer();
173
- const decoded = await this.options.context.decodeAudioData(encoded.slice(0));
174
- return new AudioClip({
175
- name: resolved.name ?? resolved.cacheKey,
176
- buffer: decoded
177
- });
178
- }
179
- }
180
-
181
- export function validateEpisodeAudioAssets(
182
- assets: readonly AudioFileAssetLike[],
183
- requirements: readonly EpisodeAudioAssetRequirement[],
184
- options: { readonly requireLicense?: boolean } = {}
185
- ): EpisodeAudioAssetReadiness {
186
- const byId = new Map(assets.filter((asset) => asset.id).map((asset) => [asset.id!, asset]));
187
- const diagnostics: EpisodeAudioAssetDiagnostic[] = [];
188
- const missingAssetIds: string[] = [];
189
- let requiredCount = 0;
190
- let readyCount = 0;
191
-
192
- for (const requirement of requirements) {
193
- const required = requirement.required ?? true;
194
- if (required) requiredCount++;
195
- const asset = byId.get(requirement.id);
196
- if (!asset) {
197
- if (required) {
198
- missingAssetIds.push(requirement.id);
199
- diagnostics.push({
200
- severity: "error",
201
- code: "audio-asset-missing",
202
- assetId: requirement.id,
203
- message: `Required ${requirement.role ?? "audio"} asset "${requirement.id}" is missing.`
204
- });
205
- }
206
- continue;
207
- }
208
- if (asset.kind === "aura-asset-ref" && asset.type !== "audio") {
209
- diagnostics.push({
210
- severity: "error",
211
- code: "audio-asset-wrong-type",
212
- assetId: requirement.id,
213
- message: `Typed asset "${requirement.id}" is "${asset.type ?? "unknown"}", not audio.`
214
- });
215
- continue;
216
- }
217
- if (!asset.url.trim()) {
218
- diagnostics.push({
219
- severity: "error",
220
- code: "audio-asset-missing-url",
221
- assetId: requirement.id,
222
- message: `Audio asset "${requirement.id}" is missing a URL.`
223
- });
224
- continue;
225
- }
226
- if ((options.requireLicense || requirement.requireLicense) && !asset.license) {
227
- diagnostics.push({
228
- severity: "error",
229
- code: "audio-asset-missing-license",
230
- assetId: requirement.id,
231
- message: `Audio asset "${requirement.id}" is missing license metadata.`
232
- });
233
- continue;
234
- }
235
- readyCount++;
236
- }
237
-
238
- return {
239
- ok: diagnostics.every((diagnostic) => diagnostic.severity !== "error"),
240
- requiredCount,
241
- readyCount,
242
- missingAssetIds,
243
- diagnostics
244
- };
245
- }
246
-
247
- async function defaultFetchAudio(url: string, init?: { readonly signal?: AbortSignal }): Promise<AudioFileFetchResponseLike> {
248
- if (typeof fetch !== "function") {
249
- throw new Error("AudioFileManager requires fetch or a custom fetch implementation");
250
- }
251
- return fetch(url, init);
252
- }
@@ -1,17 +0,0 @@
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
- }
package/src/AudioMixer.ts DELETED
@@ -1,123 +0,0 @@
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
- hasBus(name: string): boolean {
31
- return this.buses.has(name);
32
- }
33
-
34
- listBuses(): readonly AudioBus[] {
35
- return [...this.buses.values()];
36
- }
37
-
38
- evidence(options: { readonly unlocked?: boolean; readonly errors?: readonly string[] } = {}): AudioMixerEvidence {
39
- return createAudioMixerEvidence(this, options);
40
- }
41
-
42
- dispose(): void {
43
- for (const bus of [...this.buses.values()].reverse()) {
44
- bus.dispose();
45
- }
46
- this.buses.clear();
47
- }
48
- }
49
-
50
- export interface AudioMixerBusEvidence {
51
- readonly name: string;
52
- readonly volume: number;
53
- readonly muted: boolean;
54
- }
55
-
56
- export interface AudioMixerEvidence {
57
- readonly kind: "audio-mixer-evidence";
58
- readonly unlocked: boolean;
59
- readonly busCount: number;
60
- readonly buses: readonly AudioMixerBusEvidence[];
61
- readonly errors: readonly string[];
62
- }
63
-
64
- export interface AnimationAudioMixer {
65
- readonly mixer: AudioMixer;
66
- readonly buses: {
67
- readonly voice: AudioBus;
68
- readonly music: AudioBus;
69
- readonly sfx: AudioBus;
70
- readonly ambient: AudioBus;
71
- };
72
- evidence(options?: { readonly unlocked?: boolean; readonly errors?: readonly string[] }): AudioMixerEvidence;
73
- }
74
-
75
- export interface AnimationAudioMixerOptions {
76
- readonly voiceVolume?: number;
77
- readonly musicVolume?: number;
78
- readonly sfxVolume?: number;
79
- readonly ambientVolume?: number;
80
- readonly muted?: boolean;
81
- }
82
-
83
- export function createAnimationAudioMixer(
84
- context: AudioContextLike,
85
- options: AnimationAudioMixerOptions = {}
86
- ): AnimationAudioMixer {
87
- const mixer = new AudioMixer(context);
88
- const voice = mixer.createBus("voice");
89
- const music = mixer.createBus("music");
90
- const sfx = mixer.createBus("sfx");
91
- const ambient = mixer.createBus("ambient");
92
- voice.setVolume(options.voiceVolume ?? 1);
93
- music.setVolume(options.musicVolume ?? 0.55);
94
- sfx.setVolume(options.sfxVolume ?? 0.9);
95
- ambient.setVolume(options.ambientVolume ?? 0.45);
96
- if (options.muted) {
97
- for (const bus of [mixer.master, voice, music, sfx, ambient]) {
98
- bus.mute(true);
99
- }
100
- }
101
- return {
102
- mixer,
103
- buses: { voice, music, sfx, ambient },
104
- evidence: (evidenceOptions = {}) => mixer.evidence(evidenceOptions)
105
- };
106
- }
107
-
108
- export function createAudioMixerEvidence(
109
- mixer: AudioMixer,
110
- options: { readonly unlocked?: boolean; readonly errors?: readonly string[] } = {}
111
- ): AudioMixerEvidence {
112
- return {
113
- kind: "audio-mixer-evidence",
114
- unlocked: options.unlocked ?? false,
115
- busCount: mixer.listBuses().length,
116
- buses: mixer.listBuses().map((bus) => ({
117
- name: bus.name,
118
- volume: bus.volume,
119
- muted: bus.isMuted
120
- })),
121
- errors: [...(options.errors ?? [])]
122
- };
123
- }
@@ -1,75 +0,0 @@
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
- }
@@ -1,34 +0,0 @@
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
- }