@antha/audio 0.17.0 → 0.18.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/dist/antha-audio.mod.d.ts +25 -2
- package/dist/antha-audio.mod.js +66 -0
- package/dist/antha-background-audio.mod.d.ts +28 -0
- package/dist/antha-background-audio.mod.js +63 -0
- package/dist/audio-file.d.ts +12 -2
- package/dist/audio-file.js +29 -3
- package/dist/audio-player.d.ts +15 -3
- package/dist/audio-player.js +29 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
1
|
+
import { type EmptyFunction, type PartialWithUndefined } from '@augment-vir/common';
|
|
2
2
|
import { AudioPlayer, type AudioPlayerOptions } from './audio-player.js';
|
|
3
3
|
/**
|
|
4
4
|
* State for {@link createAnthaAudioMod}.
|
|
@@ -7,13 +7,36 @@ import { AudioPlayer, type AudioPlayerOptions } from './audio-player.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export type AnthaAudioState = {
|
|
9
9
|
audioPlayer: AudioPlayer;
|
|
10
|
-
}
|
|
10
|
+
} & PartialWithUndefined<{
|
|
11
|
+
audioResumeListenersCleanup: EmptyFunction;
|
|
12
|
+
audioChannelVolume: {
|
|
13
|
+
master: number;
|
|
14
|
+
} & PartialWithUndefined<{
|
|
15
|
+
channels: Record<string, number>;
|
|
16
|
+
}>;
|
|
17
|
+
}>;
|
|
11
18
|
/**
|
|
12
19
|
* Options for {@link createAnthaAudioMod}.
|
|
13
20
|
*
|
|
14
21
|
* @category Internal
|
|
15
22
|
*/
|
|
16
23
|
export type AnthaAudioModOptions = PartialWithUndefined<AudioPlayerOptions>;
|
|
24
|
+
/**
|
|
25
|
+
* Resumes audio after a browser-recognized user interaction.
|
|
26
|
+
*
|
|
27
|
+
* @category Internal
|
|
28
|
+
*/
|
|
29
|
+
export declare function resumeAnthaAudioContext({ audioPlayer, }: Readonly<{
|
|
30
|
+
audioPlayer: AudioPlayer | undefined;
|
|
31
|
+
}>): void;
|
|
32
|
+
/**
|
|
33
|
+
* Attempts to resume audio when it becomes allowed due to the user inputs.
|
|
34
|
+
*
|
|
35
|
+
* @category Internal
|
|
36
|
+
*/
|
|
37
|
+
export declare function createAudioResumeListeners({ audioPlayer, }: Readonly<{
|
|
38
|
+
audioPlayer: AudioPlayer;
|
|
39
|
+
}>): EmptyFunction;
|
|
17
40
|
/**
|
|
18
41
|
* A pre-built mod for playing audio files.
|
|
19
42
|
*
|
package/dist/antha-audio.mod.js
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
import { defineAnthaMod } from '@antha/engine';
|
|
2
|
+
import { assertWrap, check } from '@augment-vir/assert';
|
|
3
|
+
import { getObjectTypedEntries, } from '@augment-vir/common';
|
|
4
|
+
import { listenToGlobal } from 'typed-event-target';
|
|
2
5
|
import { AudioPlayer } from './audio-player.js';
|
|
6
|
+
/**
|
|
7
|
+
* Resumes audio after a browser-recognized user interaction.
|
|
8
|
+
*
|
|
9
|
+
* @category Internal
|
|
10
|
+
*/
|
|
11
|
+
export function resumeAnthaAudioContext({ audioPlayer, }) {
|
|
12
|
+
void audioPlayer?.audioContext.resume().catch(() => { });
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Attempts to resume audio when it becomes allowed due to the user inputs.
|
|
16
|
+
*
|
|
17
|
+
* @category Internal
|
|
18
|
+
*/
|
|
19
|
+
export function createAudioResumeListeners({ audioPlayer, }) {
|
|
20
|
+
let cleanup;
|
|
21
|
+
function resumeAudio() {
|
|
22
|
+
resumeAnthaAudioContext({
|
|
23
|
+
audioPlayer,
|
|
24
|
+
});
|
|
25
|
+
cleanup?.();
|
|
26
|
+
cleanup = undefined;
|
|
27
|
+
}
|
|
28
|
+
const cleanupCallbacks = [
|
|
29
|
+
listenToGlobal('click', resumeAudio, {
|
|
30
|
+
capture: true,
|
|
31
|
+
}),
|
|
32
|
+
listenToGlobal('keydown', resumeAudio, {
|
|
33
|
+
capture: true,
|
|
34
|
+
}),
|
|
35
|
+
];
|
|
36
|
+
cleanup = () => {
|
|
37
|
+
cleanupCallbacks.forEach((cleanupCallback) => cleanupCallback());
|
|
38
|
+
};
|
|
39
|
+
return cleanup;
|
|
40
|
+
}
|
|
41
|
+
function syncChannelVolumes(state) {
|
|
42
|
+
if (!state.audioChannelVolume) {
|
|
43
|
+
state.audioChannelVolume = {
|
|
44
|
+
master: 1,
|
|
45
|
+
channels: {},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const audioPlayer = assertWrap.isDefined(state.audioPlayer);
|
|
49
|
+
const audioChannelVolume = state.audioChannelVolume;
|
|
50
|
+
if (!check.isApproximately(audioPlayer.gainNode.gain.value, audioChannelVolume.master, 0.00001)) {
|
|
51
|
+
audioPlayer.gainNode.gain.value = audioChannelVolume.master;
|
|
52
|
+
}
|
|
53
|
+
getObjectTypedEntries(audioPlayer.audioChannelNodes).forEach(([audioChannel, audioChannelNode,]) => {
|
|
54
|
+
const volume = audioChannelVolume.channels?.[audioChannel] ?? 1;
|
|
55
|
+
if (!check.isApproximately(audioChannelNode.gain.value, volume, 0.00001)) {
|
|
56
|
+
audioChannelNode.gain.value = volume;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
3
60
|
/**
|
|
4
61
|
* A pre-built mod for playing audio files.
|
|
5
62
|
*
|
|
@@ -9,12 +66,21 @@ export function createAnthaAudioMod(audioPlayerOptions = {}) {
|
|
|
9
66
|
return defineAnthaMod({
|
|
10
67
|
modName: 'antha-audio',
|
|
11
68
|
async cleanup({ state }) {
|
|
69
|
+
state.audioResumeListenersCleanup?.();
|
|
70
|
+
state.audioResumeListenersCleanup = undefined;
|
|
12
71
|
await state.audioPlayer?.destroy();
|
|
13
72
|
},
|
|
14
73
|
execute({ state }) {
|
|
15
74
|
if (!state.audioPlayer) {
|
|
16
75
|
state.audioPlayer = new AudioPlayer(audioPlayerOptions);
|
|
17
76
|
}
|
|
77
|
+
syncChannelVolumes(state);
|
|
78
|
+
if (state.audioResumeListenersCleanup) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
state.audioResumeListenersCleanup = createAudioResumeListeners({
|
|
82
|
+
audioPlayer: state.audioPlayer,
|
|
83
|
+
});
|
|
18
84
|
},
|
|
19
85
|
});
|
|
20
86
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { type AnthaAudioState } from './antha-audio.mod.js';
|
|
3
|
+
import { type AudioSetupParams } from './audio-player.js';
|
|
4
|
+
/**
|
|
5
|
+
* State for {@link createAnthaBackgroundAudioMod}.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
*/
|
|
9
|
+
export type AnthaBackgroundAudioState = AnthaAudioState & PartialWithUndefined<{
|
|
10
|
+
currentBackgroundAudio: Readonly<AudioSetupParams>;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* A mod that keeps track of a single background audio (music) file being played, and pauses the
|
|
14
|
+
* previous background audio when it gets switched. Control by setting
|
|
15
|
+
* state.currentBackgroundAudio.
|
|
16
|
+
*
|
|
17
|
+
* The background audio loops: it is played again each time it finishes. If the browser blocks
|
|
18
|
+
* playback until a user interaction, playback is retried once the audio context is running.
|
|
19
|
+
*
|
|
20
|
+
* @category Pre-Built Mods
|
|
21
|
+
*/
|
|
22
|
+
export declare function createAnthaBackgroundAudioMod(): import("@antha/engine").AnthaMod<NoInfer<AnthaBackgroundAudioState>>;
|
|
23
|
+
/**
|
|
24
|
+
* The mod returned / created by {@link createAnthaBackgroundAudioMod}.
|
|
25
|
+
*
|
|
26
|
+
* @category Internal
|
|
27
|
+
*/
|
|
28
|
+
export type AnthaBackgroundAudioMod = ReturnType<typeof createAnthaBackgroundAudioMod>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { defineAnthaMod } from '@antha/engine';
|
|
2
|
+
import { log } from '@augment-vir/common';
|
|
3
|
+
/**
|
|
4
|
+
* A mod that keeps track of a single background audio (music) file being played, and pauses the
|
|
5
|
+
* previous background audio when it gets switched. Control by setting
|
|
6
|
+
* state.currentBackgroundAudio.
|
|
7
|
+
*
|
|
8
|
+
* The background audio loops: it is played again each time it finishes. If the browser blocks
|
|
9
|
+
* playback until a user interaction, playback is retried once the audio context is running.
|
|
10
|
+
*
|
|
11
|
+
* @category Pre-Built Mods
|
|
12
|
+
*/
|
|
13
|
+
export function createAnthaBackgroundAudioMod() {
|
|
14
|
+
let lastPlayingBackgroundAudio;
|
|
15
|
+
/**
|
|
16
|
+
* Defined while `lastPlayingBackgroundAudio` should keep playing. Cleared if playback throws so
|
|
17
|
+
* a broken file isn't retried forever.
|
|
18
|
+
*/
|
|
19
|
+
let pendingPlayback;
|
|
20
|
+
return defineAnthaMod({
|
|
21
|
+
modName: 'antha-background-audio-playback',
|
|
22
|
+
execute({ state }) {
|
|
23
|
+
if (!state.audioPlayer) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (lastPlayingBackgroundAudio !== state.currentBackgroundAudio) {
|
|
27
|
+
if (lastPlayingBackgroundAudio) {
|
|
28
|
+
state.audioPlayer.stopFile(lastPlayingBackgroundAudio);
|
|
29
|
+
}
|
|
30
|
+
lastPlayingBackgroundAudio = state.currentBackgroundAudio;
|
|
31
|
+
pendingPlayback = state.currentBackgroundAudio
|
|
32
|
+
? {
|
|
33
|
+
isBlocked: false,
|
|
34
|
+
isInFlight: false,
|
|
35
|
+
}
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
if (!lastPlayingBackgroundAudio ||
|
|
39
|
+
!pendingPlayback ||
|
|
40
|
+
pendingPlayback.isInFlight ||
|
|
41
|
+
(pendingPlayback.isBlocked && state.audioPlayer.audioContext.state !== 'running')) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const playback = pendingPlayback;
|
|
45
|
+
playback.isInFlight = true;
|
|
46
|
+
void state.audioPlayer
|
|
47
|
+
.play(lastPlayingBackgroundAudio)
|
|
48
|
+
/** `play` resolves once the audio finishes, so the next execute replays it. */
|
|
49
|
+
.then((didPlay) => {
|
|
50
|
+
playback.isBlocked = !didPlay;
|
|
51
|
+
})
|
|
52
|
+
.catch((error) => {
|
|
53
|
+
log.error(error);
|
|
54
|
+
if (pendingPlayback === playback) {
|
|
55
|
+
pendingPlayback = undefined;
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
.finally(() => {
|
|
59
|
+
playback.isInFlight = false;
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
package/dist/audio-file.d.ts
CHANGED
|
@@ -176,7 +176,10 @@ export type AudioPlayback = {
|
|
|
176
176
|
deferredPlayPromise: DeferredPromise<boolean>;
|
|
177
177
|
offsetSeconds: number;
|
|
178
178
|
startedAt: number;
|
|
179
|
-
}
|
|
179
|
+
} & PartialWithUndefined<{
|
|
180
|
+
/** Cached channel input node for this playback. */
|
|
181
|
+
inputNode: AudioNode;
|
|
182
|
+
}>;
|
|
180
183
|
/**
|
|
181
184
|
* Allows creating an array of `AudioNode` instances ("effects") by passing the given `AudioContext`
|
|
182
185
|
* to `createEffects`. The effects created by `createEffects`, if any, are then sequentially
|
|
@@ -253,6 +256,10 @@ export declare class AudioFile extends ListenTarget<AllAudioFileEvents> {
|
|
|
253
256
|
readonly gainNode: GainNode;
|
|
254
257
|
readonly sourceKey: string;
|
|
255
258
|
protected readonly activeBufferSources: Map<AudioBufferSourceNode, AudioPlayback>;
|
|
259
|
+
protected readonly audioChannelOutputNodeMap: Map<AudioNode, {
|
|
260
|
+
gainNode: GainNode;
|
|
261
|
+
inputNode: AudioNode;
|
|
262
|
+
}>;
|
|
256
263
|
protected readonly pausedPlaybacks: Set<AudioPlayback>;
|
|
257
264
|
constructor(params: AudioFileParams);
|
|
258
265
|
/**
|
|
@@ -269,7 +276,10 @@ export declare class AudioFile extends ListenTarget<AllAudioFileEvents> {
|
|
|
269
276
|
* @returns Whether or not the audio file was actually played. The audio file will not be played
|
|
270
277
|
* if audio is currently disabled.
|
|
271
278
|
*/
|
|
272
|
-
play(
|
|
279
|
+
play({ outputNode, }?: Readonly<PartialWithUndefined<{
|
|
280
|
+
/** Sets an output node different from the default one. */
|
|
281
|
+
outputNode: AudioNode;
|
|
282
|
+
}>>): Promise<boolean>;
|
|
273
283
|
/** Stops every active playback while preserving the loaded audio buffer. */
|
|
274
284
|
stop(): void;
|
|
275
285
|
/** Pauses every active playback while preserving its current position. */
|
package/dist/audio-file.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertWrap, check } from '@augment-vir/assert';
|
|
2
|
-
import { clamp, DeferredPromise, ensureArray, ensureError, makeWritable, stringify, } from '@augment-vir/common';
|
|
2
|
+
import { clamp, DeferredPromise, ensureArray, ensureError, getOrSetFromMap, makeWritable, stringify, } from '@augment-vir/common';
|
|
3
3
|
import { defineTypedCustomEvent, defineTypedEvent, ListenTarget } from 'typed-event-target';
|
|
4
4
|
import { isCodecSupported, isFileSupported } from './codecs.js';
|
|
5
5
|
import { isPlayingEnabled } from './detect-play.js';
|
|
@@ -160,6 +160,7 @@ export class AudioFile extends ListenTarget {
|
|
|
160
160
|
gainNode;
|
|
161
161
|
sourceKey;
|
|
162
162
|
activeBufferSources = new Map();
|
|
163
|
+
audioChannelOutputNodeMap = new Map();
|
|
163
164
|
pausedPlaybacks = new Set();
|
|
164
165
|
constructor(params) {
|
|
165
166
|
super();
|
|
@@ -214,7 +215,7 @@ export class AudioFile extends ListenTarget {
|
|
|
214
215
|
* @returns Whether or not the audio file was actually played. The audio file will not be played
|
|
215
216
|
* if audio is currently disabled.
|
|
216
217
|
*/
|
|
217
|
-
async play() {
|
|
218
|
+
async play({ outputNode, } = {}) {
|
|
218
219
|
const audioBuffer = await this.load();
|
|
219
220
|
if (!this.isAudioAllowed) {
|
|
220
221
|
makeWritable(this).isAudioAllowed = await isPlayingEnabled(this.audioContext);
|
|
@@ -234,6 +235,24 @@ export class AudioFile extends ListenTarget {
|
|
|
234
235
|
deferredPlayPromise: new DeferredPromise(),
|
|
235
236
|
offsetSeconds: 0,
|
|
236
237
|
startedAt: this.audioContext.currentTime,
|
|
238
|
+
...(outputNode
|
|
239
|
+
? {
|
|
240
|
+
inputNode: getOrSetFromMap(this.audioChannelOutputNodeMap, outputNode, () => {
|
|
241
|
+
const gainNode = this.audioContext.createGain();
|
|
242
|
+
gainNode.gain.value = clamp(this.params.volume ?? 1, {
|
|
243
|
+
min: 0,
|
|
244
|
+
max: 1,
|
|
245
|
+
});
|
|
246
|
+
gainNode.connect(outputNode);
|
|
247
|
+
const outputNodes = {
|
|
248
|
+
gainNode,
|
|
249
|
+
inputNode: setupEffects(this.audioContext, gainNode, this.params.createEffects).outputNode,
|
|
250
|
+
};
|
|
251
|
+
this.audioChannelOutputNodeMap.set(outputNode, outputNodes);
|
|
252
|
+
return outputNodes;
|
|
253
|
+
}).inputNode,
|
|
254
|
+
}
|
|
255
|
+
: {}),
|
|
237
256
|
};
|
|
238
257
|
this.startPlayback(playback);
|
|
239
258
|
return playback.deferredPlayPromise.promise;
|
|
@@ -292,6 +311,13 @@ export class AudioFile extends ListenTarget {
|
|
|
292
311
|
delete this.audioCache[this.urlOrBase64];
|
|
293
312
|
}
|
|
294
313
|
}
|
|
314
|
+
this.audioChannelOutputNodeMap.forEach((outputNodes) => {
|
|
315
|
+
if (outputNodes.inputNode !== outputNodes.gainNode) {
|
|
316
|
+
outputNodes.inputNode.disconnect();
|
|
317
|
+
}
|
|
318
|
+
outputNodes.gainNode.disconnect();
|
|
319
|
+
});
|
|
320
|
+
this.audioChannelOutputNodeMap.clear();
|
|
295
321
|
this.outputNode.disconnect();
|
|
296
322
|
this.audioCache = {};
|
|
297
323
|
this.loadPromise = undefined;
|
|
@@ -357,7 +383,7 @@ export class AudioFile extends ListenTarget {
|
|
|
357
383
|
startPlayback(playback) {
|
|
358
384
|
const bufferSource = this.audioContext.createBufferSource();
|
|
359
385
|
bufferSource.buffer = playback.audioBuffer;
|
|
360
|
-
bufferSource.connect(this.outputNode);
|
|
386
|
+
bufferSource.connect(playback.inputNode ?? this.outputNode);
|
|
361
387
|
playback.startedAt = this.audioContext.currentTime;
|
|
362
388
|
this.activeBufferSources.set(bufferSource, playback);
|
|
363
389
|
bufferSource.addEventListener('ended', () => {
|
package/dist/audio-player.d.ts
CHANGED
|
@@ -22,7 +22,12 @@ export type AudioLoadProgressCallback = (params: AudioLoadProgressCallbackParams
|
|
|
22
22
|
*
|
|
23
23
|
* @category Internal
|
|
24
24
|
*/
|
|
25
|
-
export type AudioPlayerOptions = Pick<AudioFileParams, 'fetch' | 'volume' | 'createEffects'
|
|
25
|
+
export type AudioPlayerOptions = Pick<AudioFileParams, 'fetch' | 'volume' | 'createEffects'> & PartialWithUndefined<{
|
|
26
|
+
/** Used to pre-populate audio channel gain nodes. The channel name mapped to its volume. */
|
|
27
|
+
initChannels: {
|
|
28
|
+
[ChannelName in string]: number;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
26
31
|
/**
|
|
27
32
|
* Inputs for playing audio.
|
|
28
33
|
*
|
|
@@ -36,6 +41,8 @@ export type AudioSetupParams = Readonly<Pick<AudioFileParams, 'sources' | 'volum
|
|
|
36
41
|
*/
|
|
37
42
|
export declare class AudioPlayer extends ListenTarget<AllAudioFileEvents> {
|
|
38
43
|
protected readonly options: Readonly<PartialWithUndefined<AudioPlayerOptions>>;
|
|
44
|
+
/** Gain nodes for audio channels created by {@link AudioPlayer.play}. */
|
|
45
|
+
readonly audioChannelNodes: Record<string, GainNode>;
|
|
39
46
|
readonly audioFiles: {
|
|
40
47
|
[SourceKey in string]: AudioFile;
|
|
41
48
|
};
|
|
@@ -52,8 +59,13 @@ export declare class AudioPlayer extends ListenTarget<AllAudioFileEvents> {
|
|
|
52
59
|
/** Controls volume for all audio files. Modify `gain.value` on this to change playback volume. */
|
|
53
60
|
readonly gainNode: GainNode;
|
|
54
61
|
constructor(options?: Readonly<PartialWithUndefined<AudioPlayerOptions>>);
|
|
55
|
-
/**
|
|
56
|
-
play(
|
|
62
|
+
/** Plays audio, optionally routing this playback through a named channel. */
|
|
63
|
+
play({ audioChannel, ...audioSetup }: Readonly<AudioSetupParams & PartialWithUndefined<{
|
|
64
|
+
/** Routes this playback through a named channel gain node. */
|
|
65
|
+
audioChannel: string;
|
|
66
|
+
}>>): Promise<boolean>;
|
|
67
|
+
/** Gets a channel gain node, creating it with `volume` when necessary. */
|
|
68
|
+
protected getAudioChannelNode(audioChannel: string, volume: number): GainNode;
|
|
57
69
|
/** Create a new {@link AudioFile} instance at the given `key` and set it up. */
|
|
58
70
|
protected setupAudioFile(params: Readonly<AudioSetupParams>): AudioFile;
|
|
59
71
|
/** Unloads all the attached files. */
|
package/dist/audio-player.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { awaitedBlockingMap, clamp, getObjectTypedValues, makeWritable, } from '@augment-vir/common';
|
|
1
|
+
import { awaitedBlockingMap, clamp, getObjectTypedEntries, getObjectTypedValues, getOrSet, makeWritable, } from '@augment-vir/common';
|
|
2
2
|
import { ListenTarget } from 'typed-event-target';
|
|
3
3
|
import { AudioFile, createAudioSourceKey, PlayingEnabledEvent, setupEffects, } from './audio-file.js';
|
|
4
4
|
/**
|
|
@@ -8,6 +8,8 @@ import { AudioFile, createAudioSourceKey, PlayingEnabledEvent, setupEffects, } f
|
|
|
8
8
|
*/
|
|
9
9
|
export class AudioPlayer extends ListenTarget {
|
|
10
10
|
options;
|
|
11
|
+
/** Gain nodes for audio channels created by {@link AudioPlayer.play}. */
|
|
12
|
+
audioChannelNodes = {};
|
|
11
13
|
audioFiles = {};
|
|
12
14
|
audioContext = new AudioContext();
|
|
13
15
|
audioCache = {};
|
|
@@ -31,10 +33,30 @@ export class AudioPlayer extends ListenTarget {
|
|
|
31
33
|
});
|
|
32
34
|
this.gainNode.connect(this.audioContext.destination);
|
|
33
35
|
this.outputNode = setupEffects(this.audioContext, this.gainNode, options.createEffects).outputNode;
|
|
36
|
+
getObjectTypedEntries(options.initChannels || {}).forEach(([channelName, volume,]) => {
|
|
37
|
+
this.getAudioChannelNode(channelName, volume);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Plays audio, optionally routing this playback through a named channel. */
|
|
41
|
+
async play({ audioChannel, ...audioSetup }) {
|
|
42
|
+
const audioFile = this.setupAudioFile(audioSetup);
|
|
43
|
+
return audioChannel == undefined
|
|
44
|
+
? audioFile.play()
|
|
45
|
+
: audioFile.play({
|
|
46
|
+
outputNode: this.getAudioChannelNode(audioChannel, 1),
|
|
47
|
+
});
|
|
34
48
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
return this.
|
|
49
|
+
/** Gets a channel gain node, creating it with `volume` when necessary. */
|
|
50
|
+
getAudioChannelNode(audioChannel, volume) {
|
|
51
|
+
return getOrSet(this.audioChannelNodes, audioChannel, () => {
|
|
52
|
+
const audioChannelNode = this.audioContext.createGain();
|
|
53
|
+
audioChannelNode.gain.value = clamp(volume, {
|
|
54
|
+
min: 0,
|
|
55
|
+
max: 1,
|
|
56
|
+
});
|
|
57
|
+
audioChannelNode.connect(this.outputNode);
|
|
58
|
+
return audioChannelNode;
|
|
59
|
+
});
|
|
38
60
|
}
|
|
39
61
|
/** Create a new {@link AudioFile} instance at the given `key` and set it up. */
|
|
40
62
|
setupAudioFile(params) {
|
|
@@ -157,6 +179,9 @@ export class AudioPlayer extends ListenTarget {
|
|
|
157
179
|
await audioFile.destroy();
|
|
158
180
|
delete this.audioCache[audioFile.sourceKey];
|
|
159
181
|
}));
|
|
182
|
+
getObjectTypedValues(this.audioChannelNodes).forEach((audioChannelNode) => {
|
|
183
|
+
audioChannelNode.disconnect();
|
|
184
|
+
});
|
|
160
185
|
await this.audioContext.close();
|
|
161
186
|
this.isDestroyed = true;
|
|
162
187
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antha/audio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "An Antha mod for handling audio.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vir",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@augment-vir/common": "^32.3.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@antha/engine": "^0.
|
|
40
|
+
"@antha/engine": "^0.18.0",
|
|
41
41
|
"@augment-vir/test": "^32.3.0",
|
|
42
42
|
"@web/dev-server-esbuild": "^2.0.0",
|
|
43
43
|
"@web/test-runner": "^1.0.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"typed-event-target": "^4.3.3"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@antha/engine": "^0.
|
|
49
|
+
"@antha/engine": "^0.18.0",
|
|
50
50
|
"element-vir": "^27",
|
|
51
51
|
"typed-event-target": "^4.3.3"
|
|
52
52
|
},
|