@hkdigital/lib-core 0.4.27 → 0.4.29

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,20 +1,6 @@
1
1
  import * as expect from '../../../util/expect.js';
2
2
 
3
- import { LoadingStateMachine } from '../../../state/machines.js';
4
-
5
- import {
6
- STATE_INITIAL,
7
- STATE_LOADING,
8
- STATE_LOADED,
9
- STATE_ABORTING,
10
- STATE_ABORTED,
11
- STATE_ERROR,
12
- LOAD,
13
- LOADED,
14
- ABORT,
15
- ABORTED
16
- } from '../../../state/machines.js';
17
-
3
+ import SceneBase from '../base/SceneBase.svelte.js';
18
4
  import AudioLoader from './AudioLoader.svelte.js';
19
5
 
20
6
  /**
@@ -29,15 +15,7 @@ import AudioLoader from './AudioLoader.svelte.js';
29
15
  * @property {SourceConfig} [config]
30
16
  */
31
17
 
32
- export default class AudioScene {
33
- #state = new LoadingStateMachine();
34
-
35
- // @note this exported state is set by onenter
36
- state = $state(STATE_INITIAL);
37
-
38
- loaded = $derived.by(() => {
39
- return this.state === STATE_LOADED;
40
- });
18
+ export default class AudioScene extends SceneBase {
41
19
 
42
20
  #targetGain = $state(1);
43
21
 
@@ -56,143 +34,35 @@ export default class AudioScene {
56
34
  /** @type {MemorySource[]} */
57
35
  #memorySources = $state([]);
58
36
 
59
- #progress = $derived.by(() => {
60
- // console.log('update progress');
61
-
62
- let totalSize = 0;
63
- let totalBytesLoaded = 0;
64
- let sourcesLoaded = 0;
65
-
66
- const sources = this.#memorySources;
67
- const numberOfSources = sources.length;
68
-
69
- for (let j = 0; j < numberOfSources; j++) {
70
- const source = sources[j];
71
- const { audioLoader } = source;
72
-
73
- const { bytesLoaded, size, loaded } = audioLoader.progress;
74
-
75
- totalSize += size;
76
- totalBytesLoaded += bytesLoaded;
77
-
78
- if (loaded) {
79
- sourcesLoaded++;
80
- }
81
- } // end for
82
-
83
- return {
84
- totalBytesLoaded,
85
- totalSize,
86
- sourcesLoaded,
87
- numberOfSources
88
- };
89
- });
90
-
91
- #abortProgress = $derived.by(() => {
92
- let sourcesAborted = 0;
93
- const sources = this.#memorySources;
94
- const numberOfSources = sources.length;
95
-
96
- for (let j = 0; j < numberOfSources; j++) {
97
- const source = sources[j];
98
- const { audioLoader } = source;
99
- const loaderState = audioLoader.state;
100
-
101
- if (loaderState === STATE_ABORTED || loaderState === STATE_ERROR) {
102
- sourcesAborted++;
103
- }
104
- } // end for
105
-
106
- return {
107
- sourcesAborted,
108
- numberOfSources
109
- };
110
- });
111
37
 
112
38
  /**
113
39
  * Construct AudioScene
114
40
  */
115
41
  constructor() {
116
- const state = this.#state;
117
-
118
- $effect(() => {
119
- if (state.current === STATE_LOADING) {
120
- // console.log(
121
- // 'progress',
122
- // JSON.stringify($state.snapshot(this.#progress))
123
- // );
124
-
125
- const { sourcesLoaded, numberOfSources } = this.#progress;
126
-
127
- if (sourcesLoaded === numberOfSources) {
128
- // console.debug(`AudioScene: ${numberOfSources} sources loaded`);
129
- this.#state.send(LOADED);
130
- }
131
- }
132
- });
133
-
134
- $effect(() => {
135
- if (state.current === STATE_ABORTING) {
136
- const { sourcesAborted, numberOfSources } = this.#abortProgress;
137
-
138
- if (sourcesAborted === numberOfSources) {
139
- // console.debug(`AudioScene: ${numberOfSources} sources aborted`);
140
- this.#state.send(ABORTED);
141
- }
142
- }
143
- });
144
-
145
- state.onenter = ( currentState ) => {
146
- // console.log('onenter', currentState );
147
-
148
- if(currentState === STATE_LOADING )
149
- {
150
- // console.log('AudioScene:loading');
151
- this.#startLoading();
152
- }
153
- else if(currentState === STATE_ABORTING )
154
- {
155
- // console.log('AudioScene:aborting');
156
- this.#startAbort();
157
- }
158
-
159
- this.state = state.current;
160
- };
42
+ super();
161
43
  }
162
44
 
163
- /* ==== Common loader interface */
45
+ /* ==== SceneBase implementation */
164
46
 
165
47
  /**
166
- * Get audio scene loading progress
167
- */
168
- get progress() {
169
- return this.#progress;
170
- }
171
-
172
- /**
173
- * Get audio scene abort progress
174
- */
175
- get abortProgress() {
176
- return this.#abortProgress;
177
- }
178
-
179
- /**
180
- * Start loading all audio sources
48
+ * Get the array of memory sources managed by this scene
49
+ *
50
+ * @returns {MemorySource[]}
181
51
  */
182
- load() {
183
- this.#state.send(LOAD);
52
+ get sources() {
53
+ return this.#memorySources;
184
54
  }
185
55
 
186
56
  /**
187
- * Abort loading all audio sources
57
+ * Extract the audio loader from a source object
58
+ *
59
+ * @param {MemorySource} source
60
+ *
61
+ * @returns {AudioLoader}
188
62
  */
189
- abort() {
190
- this.#state.send(ABORT);
191
- }
192
-
193
- destroy() {
194
- // TODO: disconnect all audio sources?
195
- // TODO: Unload AUdioLoaders?
63
+ // eslint-disable-next-line no-unused-vars
64
+ getLoaderFromSource(source) {
65
+ return source.audioLoader;
196
66
  }
197
67
 
198
68
  /* ==== Source definitions */
@@ -260,28 +130,16 @@ export default class AudioScene {
260
130
  this.#audioContext = audioContext;
261
131
  }
262
132
 
263
- async #startLoading() {
264
- // console.log('#startLoading');
265
-
266
- for (const { audioLoader } of this.#memorySources) {
267
- audioLoader.load();
268
- }
269
- }
270
-
271
- #startAbort() {
272
- // console.log('#startAbort');
273
-
274
- for (const { audioLoader } of this.#memorySources) {
275
- audioLoader.abort();
276
- }
277
- }
278
133
 
279
134
  /* ==== Audio specific */
280
135
 
281
136
  /**
282
- * Set target gain
137
+ * Set target gain (volume level) for all audio in this scene
138
+ * - Currently applies immediately, but "target" allows for future
139
+ * smooth transitions using Web Audio API's gain scheduling
140
+ * - Range: 0.0 (silence) to 1.0 (original) to 1.0+ (amplified)
283
141
  *
284
- * @param {number} value
142
+ * @param {number} value - Target gain value (0.0 to 1.0+)
285
143
  */
286
144
  setTargetGain( value ) {
287
145
  this.#targetGain = value;
@@ -291,15 +149,21 @@ export default class AudioScene {
291
149
  }
292
150
 
293
151
  /**
294
- * Get the scene gain
152
+ * Get the current target gain (volume level)
295
153
  *
296
- * @returns {number} value
154
+ * @returns {number} Target gain value (0.0 to 1.0+)
297
155
  */
298
156
  getTargetGain()
299
157
  {
300
158
  return this.#targetGain;
301
159
  }
302
160
 
161
+ /**
162
+ * Mute all audio in this scene
163
+ * - Saves current volume level for restoration
164
+ * - Sets target gain to 0 (silence)
165
+ * - Safe to call multiple times
166
+ */
303
167
  mute() {
304
168
  if( this.muted )
305
169
  {
@@ -310,6 +174,12 @@ export default class AudioScene {
310
174
  this.setTargetGain(0);
311
175
  }
312
176
 
177
+ /**
178
+ * Unmute all audio in this scene
179
+ * - Restores volume level from before muting
180
+ * - Safe to call multiple times
181
+ * - No effect if scene is not currently muted
182
+ */
313
183
  unmute() {
314
184
  if( !this.muted )
315
185
  {
@@ -0,0 +1,195 @@
1
+ # Audio Loaders
2
+
3
+ Audio loading and scene management for web applications using the Web Audio API.
4
+
5
+ ## Overview
6
+
7
+ The audio loading system consists of two main components:
8
+
9
+ - **AudioLoader** - Loads individual audio files from network sources
10
+ - **AudioScene** - Manages collections of audio sources with centralized playback control
11
+
12
+ ## AudioScene
13
+
14
+ `AudioScene` extends `SceneBase` to provide audio-specific functionality including volume control, muting, and Web Audio API integration.
15
+
16
+ ### Key Features
17
+
18
+ - **Scene-wide volume control** via gain nodes
19
+ - **Mute/unmute functionality** with volume restoration
20
+ - **Web Audio API integration** for precise audio control
21
+ - **Multiple audio source management** with unified loading states
22
+ - **Audio context management** with automatic creation or custom injection
23
+
24
+ ### Audio Context Usage
25
+
26
+ AudioScene uses the Web Audio API's `AudioContext` for advanced audio processing:
27
+
28
+ ```javascript
29
+ const audioScene = new AudioScene();
30
+
31
+ // Option 1: Use automatic context creation (default)
32
+ audioScene.load(); // Creates AudioContext internally
33
+
34
+ // Option 2: Provide your own context for integration
35
+ const customContext = new AudioContext();
36
+ audioScene.setAudioContext(customContext);
37
+ ```
38
+
39
+ ### Volume Control (Target Gain)
40
+
41
+ The scene uses a "target gain" system based on Web Audio API's gain scheduling:
42
+
43
+ - **Target Gain**: The desired volume level that the audio system transitions towards
44
+ - `0.0` = silence (muted)
45
+ - `1.0` = original volume
46
+ - `> 1.0` = amplified (use carefully to avoid distortion)
47
+
48
+ **Why "Target"?**
49
+ The Web Audio API allows smooth transitions between gain values over time. When you set a target gain, the audio system can gradually transition from the current level to the target level, preventing audio pops and clicks.
50
+
51
+ ```javascript
52
+ // Set immediate volume change to 50%
53
+ audioScene.setTargetGain(0.5);
54
+
55
+ // Future: Could support smooth transitions
56
+ // gainNode.gain.setTargetAtTime(0.5, audioContext.currentTime, 0.3);
57
+
58
+ // Get current target volume
59
+ const volume = audioScene.getTargetGain(); // 0.5
60
+
61
+ // Mute (remembers previous volume)
62
+ audioScene.mute();
63
+
64
+ // Restore previous volume
65
+ audioScene.unmute();
66
+ ```
67
+
68
+ The current implementation sets gain immediately, but the "target" naming prepares for potential smooth volume transitions in future versions.
69
+
70
+ ### Audio Source Management
71
+
72
+ Define audio sources before loading:
73
+
74
+ ```javascript
75
+ // Add audio sources
76
+ audioScene.defineMemorySource({
77
+ label: 'background-music',
78
+ url: '/audio/bg-music.mp3'
79
+ });
80
+
81
+ audioScene.defineMemorySource({
82
+ label: 'sound-effect',
83
+ url: '/audio/beep.wav'
84
+ });
85
+
86
+ // Load all sources
87
+ audioScene.load();
88
+
89
+ // Wait for loading completion
90
+ await waitForState(() => audioScene.loaded);
91
+ ```
92
+
93
+ ### Playback
94
+
95
+ Get playable audio sources after loading:
96
+
97
+ ```javascript
98
+ // Get a source node for one-time playback
99
+ const sourceNode = await audioScene.getSourceNode('sound-effect');
100
+
101
+ // Play immediately
102
+ sourceNode.start();
103
+
104
+ // Play with delay
105
+ sourceNode.start(audioContext.currentTime + 0.5);
106
+
107
+ // Cleanup is automatic when playback ends
108
+ ```
109
+
110
+ ### Web Audio API Architecture
111
+
112
+ ```
113
+ AudioSources → GainNode → AudioContext.destination → Speakers
114
+
115
+ Volume Control
116
+ ```
117
+
118
+ Each audio source connects through a gain node that provides scene-wide volume control before reaching the audio output.
119
+
120
+ ### State Management
121
+
122
+ AudioScene inherits state management from SceneBase:
123
+
124
+ - `STATE_INITIAL` - Scene created, sources defined
125
+ - `STATE_LOADING` - Audio files downloading
126
+ - `STATE_LOADED` - All sources ready for playback
127
+ - `STATE_ABORTING` - Canceling downloads
128
+ - `STATE_ABORTED` - Downloads canceled
129
+
130
+ ### Preloading with Progress and Abort
131
+
132
+ AudioScene supports convenient preloading with automatic progress tracking:
133
+
134
+ ```javascript
135
+ // Basic preload - returns promise and abort function
136
+ const { promise, abort } = audioScene.preload();
137
+
138
+ // Preload with options
139
+ const { promise, abort } = audioScene.preload({
140
+ timeoutMs: 5000, // Timeout after 5 seconds
141
+ onProgress: (progress) => {
142
+ console.log(`Loading: ${progress.sourcesLoaded}/${progress.numberOfSources}`);
143
+ console.log(`Bytes: ${progress.totalBytesLoaded}/${progress.totalSize}`);
144
+ }
145
+ });
146
+
147
+ // Wait for completion
148
+ try {
149
+ const loadedScene = await promise;
150
+ console.log('All audio loaded successfully!');
151
+ } catch (error) {
152
+ console.error('Preload failed:', error.message);
153
+ }
154
+
155
+ // Can abort anytime
156
+ document.getElementById('cancelBtn').onclick = () => abort();
157
+ ```
158
+
159
+ **Preload Features:**
160
+ - **Promise-based**: Returns `{ promise, abort }` object
161
+ - **Progress tracking**: Optional callback with loading progress
162
+ - **Timeout support**: Configurable timeout with automatic abort
163
+ - **Manual abort**: Call `abort()` function to cancel loading
164
+ - **Error handling**: Promise rejects on timeout, abort, or loading errors
165
+
166
+ ### Progress Tracking
167
+
168
+ Monitor loading progress across all sources:
169
+
170
+ ```javascript
171
+ const progress = audioScene.progress;
172
+ console.log(`${progress.sourcesLoaded}/${progress.numberOfSources} loaded`);
173
+ console.log(`${progress.totalBytesLoaded}/${progress.totalSize} bytes`);
174
+ ```
175
+
176
+ ## Best Practices
177
+
178
+ 1. **Audio Context Lifecycle**: Create AudioContext in response to user interaction to avoid browser autoplay restrictions
179
+
180
+ 2. **Memory Management**: Audio sources are loaded into memory - consider file sizes for mobile devices
181
+
182
+ 3. **Source Node Usage**: Each `getSourceNode()` call creates a new playable instance - don't reuse source nodes
183
+
184
+ 4. **Volume Levels**: Keep target gain ≤ 1.0 to avoid audio clipping and distortion
185
+
186
+ 5. **Error Handling**: Check scene state before attempting playback
187
+
188
+ ```javascript
189
+ if (audioScene.loaded) {
190
+ const source = await audioScene.getSourceNode('audio-label');
191
+ source.start();
192
+ } else {
193
+ console.warn('Audio scene not ready for playback');
194
+ }
195
+ ```
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Base class for scene loaders that manage collections of media sources
3
+ */
4
+ export default class SceneBase {
5
+ state: string;
6
+ loaded: boolean;
7
+ /**
8
+ * Get the array of sources managed by this scene
9
+ *
10
+ * @returns {Array} Array of source objects
11
+ */
12
+ get sources(): any[];
13
+ /**
14
+ * Extract the loader from a source object
15
+ *
16
+ * @param {*} source
17
+ *
18
+ * @returns {*} Loader object with progress and state properties
19
+ */
20
+ getLoaderFromSource(source: any): any;
21
+ /**
22
+ * Get scene loading progress
23
+ */
24
+ get progress(): {
25
+ totalBytesLoaded: number;
26
+ totalSize: number;
27
+ sourcesLoaded: number;
28
+ numberOfSources: number;
29
+ };
30
+ /**
31
+ * Get scene abort progress
32
+ */
33
+ get abortProgress(): {
34
+ sourcesAborted: number;
35
+ numberOfSources: number;
36
+ };
37
+ /**
38
+ * Start loading all sources
39
+ */
40
+ load(): void;
41
+ /**
42
+ * Abort loading all sources
43
+ */
44
+ abort(): void;
45
+ /**
46
+ * Preload all sources with progress tracking and abort capability
47
+ * - Starts loading and waits for completion
48
+ * - Supports timeout and progress callbacks
49
+ * - Returns object with promise and abort function
50
+ *
51
+ * @param {object} [options]
52
+ * @param {number} [options.timeoutMs=10000] - Timeout in milliseconds
53
+ * @param {Function} [options.onProgress] - Progress callback function
54
+ *
55
+ * @returns {object} Object with promise and abort function
56
+ * @returns {Promise<SceneBase>} returns.promise - Promise that resolves when loaded
57
+ * @returns {Function} returns.abort - Function to abort preloading
58
+ */
59
+ preload({ timeoutMs, onProgress }?: {
60
+ timeoutMs?: number;
61
+ onProgress?: Function;
62
+ }): object;
63
+ destroy(): void;
64
+ #private;
65
+ }