@antha/audio 0.4.3 → 0.5.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.
@@ -1,4 +1,4 @@
1
- import { type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
1
+ import { DeferredPromise, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
2
2
  import { ListenTarget } from 'typed-event-target';
3
3
  import { type Codec } from './codecs.js';
4
4
  /**
@@ -166,6 +166,17 @@ export type AudioFileCache = {
166
166
  buffer: AudioBuffer;
167
167
  }>;
168
168
  };
169
+ /**
170
+ * State tracked for an active or paused audio playback.
171
+ *
172
+ * @category Internal
173
+ */
174
+ export type AudioPlayback = {
175
+ audioBuffer: AudioBuffer;
176
+ deferredPlayPromise: DeferredPromise<boolean>;
177
+ offsetSeconds: number;
178
+ startedAt: number;
179
+ };
169
180
  /**
170
181
  * Allows creating an array of `AudioNode` instances ("effects") by passing the given `AudioContext`
171
182
  * to `createEffects`. The effects created by `createEffects`, if any, are then sequentially
@@ -241,7 +252,8 @@ export declare class AudioFile extends ListenTarget<AllAudioFileEvents> {
241
252
  readonly isDestroyed: boolean;
242
253
  readonly gainNode: GainNode;
243
254
  readonly sourceKey: string;
244
- protected readonly activeBufferSources: Set<AudioBufferSourceNode>;
255
+ protected readonly activeBufferSources: Map<AudioBufferSourceNode, AudioPlayback>;
256
+ protected readonly pausedPlaybacks: Set<AudioPlayback>;
245
257
  constructor(params: AudioFileParams);
246
258
  /**
247
259
  * Load the audio file so it's ready to play. This will automatically be called on the first
@@ -260,6 +272,10 @@ export declare class AudioFile extends ListenTarget<AllAudioFileEvents> {
260
272
  play(): Promise<boolean>;
261
273
  /** Stops every active playback while preserving the loaded audio buffer. */
262
274
  stop(): void;
275
+ /** Pauses every active playback while preserving its current position. */
276
+ pause(): void;
277
+ /** Resumes every playback paused by {@link pause}. */
278
+ resume(): void;
263
279
  /** Destroys this audio file entirely; it cannot be used anymore. */
264
280
  destroy(): Promise<void>;
265
281
  /**
@@ -276,5 +292,7 @@ export declare class AudioFile extends ListenTarget<AllAudioFileEvents> {
276
292
  protected loadBase64(): ArrayBuffer;
277
293
  /** Load the audio file's `ArrayBuffer` from its source URL. */
278
294
  protected loadFromUrl(): Promise<ArrayBuffer>;
295
+ /** Starts an audio source for the supplied playback state. */
296
+ protected startPlayback(playback: AudioPlayback): void;
279
297
  }
280
298
  export {};
@@ -159,7 +159,8 @@ export class AudioFile extends ListenTarget {
159
159
  isDestroyed = false;
160
160
  gainNode;
161
161
  sourceKey;
162
- activeBufferSources = new Set();
162
+ activeBufferSources = new Map();
163
+ pausedPlaybacks = new Set();
163
164
  constructor(params) {
164
165
  super();
165
166
  this.params = params;
@@ -228,26 +229,51 @@ export class AudioFile extends ListenTarget {
228
229
  return false;
229
230
  }
230
231
  }
231
- const deferredPlayPromise = new DeferredPromise();
232
- const bufferSource = this.audioContext.createBufferSource();
233
- bufferSource.buffer = audioBuffer;
234
- bufferSource.connect(this.outputNode);
235
- this.activeBufferSources.add(bufferSource);
236
- bufferSource.addEventListener('ended', () => {
237
- this.activeBufferSources.delete(bufferSource);
238
- deferredPlayPromise.resolve(true);
239
- this.dispatch(new AudioFilePlayEndEvent());
240
- });
241
- this.dispatch(new AudioFilePlayStartEvent());
242
- bufferSource.start();
243
- return deferredPlayPromise.promise;
232
+ const playback = {
233
+ audioBuffer,
234
+ deferredPlayPromise: new DeferredPromise(),
235
+ offsetSeconds: 0,
236
+ startedAt: this.audioContext.currentTime,
237
+ };
238
+ this.startPlayback(playback);
239
+ return playback.deferredPlayPromise.promise;
244
240
  }
245
241
  /** Stops every active playback while preserving the loaded audio buffer. */
246
242
  stop() {
243
+ const pausedPlaybacks = [...this.pausedPlaybacks];
247
244
  const activeBufferSources = [...this.activeBufferSources];
245
+ this.pausedPlaybacks.clear();
248
246
  this.activeBufferSources.clear();
249
- activeBufferSources.forEach((bufferSource) => {
247
+ pausedPlaybacks.forEach((playback) => {
248
+ playback.deferredPlayPromise.resolve(true);
249
+ this.dispatch(new AudioFilePlayEndEvent());
250
+ });
251
+ activeBufferSources.forEach(([bufferSource, playback,]) => {
250
252
  bufferSource.stop();
253
+ playback.deferredPlayPromise.resolve(true);
254
+ this.dispatch(new AudioFilePlayEndEvent());
255
+ });
256
+ }
257
+ /** Pauses every active playback while preserving its current position. */
258
+ pause() {
259
+ [...this.activeBufferSources].forEach(([bufferSource, playback,]) => {
260
+ const offsetSeconds = Math.min(playback.audioBuffer.duration, playback.offsetSeconds + this.audioContext.currentTime - playback.startedAt);
261
+ if (offsetSeconds >= playback.audioBuffer.duration) {
262
+ return;
263
+ }
264
+ this.activeBufferSources.delete(bufferSource);
265
+ this.pausedPlaybacks.add({
266
+ ...playback,
267
+ offsetSeconds,
268
+ });
269
+ bufferSource.stop();
270
+ });
271
+ }
272
+ /** Resumes every playback paused by {@link pause}. */
273
+ resume() {
274
+ [...this.pausedPlaybacks].forEach((playback) => {
275
+ this.pausedPlaybacks.delete(playback);
276
+ this.startPlayback(playback);
251
277
  });
252
278
  }
253
279
  /** Destroys this audio file entirely; it cannot be used anymore. */
@@ -327,4 +353,21 @@ export class AudioFile extends ListenTarget {
327
353
  async loadFromUrl() {
328
354
  return await (await this.fetch(this.urlOrBase64)).arrayBuffer();
329
355
  }
356
+ /** Starts an audio source for the supplied playback state. */
357
+ startPlayback(playback) {
358
+ const bufferSource = this.audioContext.createBufferSource();
359
+ bufferSource.buffer = playback.audioBuffer;
360
+ bufferSource.connect(this.outputNode);
361
+ playback.startedAt = this.audioContext.currentTime;
362
+ this.activeBufferSources.set(bufferSource, playback);
363
+ bufferSource.addEventListener('ended', () => {
364
+ if (!this.activeBufferSources.delete(bufferSource)) {
365
+ return;
366
+ }
367
+ playback.deferredPlayPromise.resolve(true);
368
+ this.dispatch(new AudioFilePlayEndEvent());
369
+ });
370
+ this.dispatch(new AudioFilePlayStartEvent());
371
+ bufferSource.start(0, playback.offsetSeconds);
372
+ }
330
373
  }
@@ -64,6 +64,18 @@ export declare class AudioPlayer extends ListenTarget<AllAudioFileEvents> {
64
64
  stopFiles(files: ReadonlyArray<Readonly<AudioSetupParams>>): void;
65
65
  /** Stops all active playback without unloading any audio files. */
66
66
  stopAllFiles(): void;
67
+ /** Pauses all active playback for an already-loaded audio file. */
68
+ pauseFile(file: Readonly<AudioSetupParams>): void;
69
+ /** Pauses all active playback for the given already-loaded audio files. */
70
+ pauseFiles(files: ReadonlyArray<Readonly<AudioSetupParams>>): void;
71
+ /** Pauses all active playback without unloading any audio files. */
72
+ pauseAllFiles(): void;
73
+ /** Resumes playback paused for an already-loaded audio file. */
74
+ resumeFile(file: Readonly<AudioSetupParams>): void;
75
+ /** Resumes playback paused for the given already-loaded audio files. */
76
+ resumeFiles(files: ReadonlyArray<Readonly<AudioSetupParams>>): void;
77
+ /** Resumes all playback paused through this {@link AudioPlayer}. */
78
+ resumeAllFiles(): void;
67
79
  /** Load a batch of audio files. */
68
80
  loadFiles(files: ReadonlyArray<Readonly<AudioSetupParams>>, options?: Readonly<PartialWithUndefined<{
69
81
  progressCallback: AudioLoadProgressCallback;
@@ -89,6 +89,40 @@ export class AudioPlayer extends ListenTarget {
89
89
  audioFile.stop();
90
90
  });
91
91
  }
92
+ /** Pauses all active playback for an already-loaded audio file. */
93
+ pauseFile(file) {
94
+ const sourceKey = createAudioSourceKey(file);
95
+ this.audioFiles[sourceKey]?.pause();
96
+ }
97
+ /** Pauses all active playback for the given already-loaded audio files. */
98
+ pauseFiles(files) {
99
+ files.forEach((file) => {
100
+ this.pauseFile(file);
101
+ });
102
+ }
103
+ /** Pauses all active playback without unloading any audio files. */
104
+ pauseAllFiles() {
105
+ getObjectTypedValues(this.audioFiles).forEach((audioFile) => {
106
+ audioFile.pause();
107
+ });
108
+ }
109
+ /** Resumes playback paused for an already-loaded audio file. */
110
+ resumeFile(file) {
111
+ const sourceKey = createAudioSourceKey(file);
112
+ this.audioFiles[sourceKey]?.resume();
113
+ }
114
+ /** Resumes playback paused for the given already-loaded audio files. */
115
+ resumeFiles(files) {
116
+ files.forEach((file) => {
117
+ this.resumeFile(file);
118
+ });
119
+ }
120
+ /** Resumes all playback paused through this {@link AudioPlayer}. */
121
+ resumeAllFiles() {
122
+ getObjectTypedValues(this.audioFiles).forEach((audioFile) => {
123
+ audioFile.resume();
124
+ });
125
+ }
92
126
  /** Load a batch of audio files. */
93
127
  async loadFiles(files, options = {}) {
94
128
  let loadedCount = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antha/audio",
3
- "version": "0.4.3",
3
+ "version": "0.5.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.2.3"
38
38
  },
39
39
  "devDependencies": {
40
- "@antha/engine": "^0.4.3",
40
+ "@antha/engine": "^0.5.0",
41
41
  "@augment-vir/test": "^32.2.3",
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.4.3",
49
+ "@antha/engine": "^0.5.0",
50
50
  "element-vir": ">=26",
51
51
  "typed-event-target": ">=4.3"
52
52
  },