@hkdigital/lib-core 0.4.26 → 0.4.28

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.
@@ -8,25 +8,23 @@
8
8
  * @property {AudioLoader} audioLoader
9
9
  * @property {SourceConfig} [config]
10
10
  */
11
- export default class AudioScene {
12
- state: string;
13
- loaded: boolean;
11
+ export default class AudioScene extends SceneBase {
14
12
  muted: boolean;
15
13
  targetGain: number;
16
14
  /**
17
- * Get audio scene loading progress
18
- */
19
- get progress(): {
20
- totalBytesLoaded: number;
21
- totalSize: number;
22
- sourcesLoaded: number;
23
- numberOfSources: number;
24
- };
15
+ * Get the array of memory sources managed by this scene
16
+ *
17
+ * @returns {MemorySource[]}
18
+ */
19
+ get sources(): MemorySource[];
25
20
  /**
26
- * Start loading all audio sources
21
+ * Extract the audio loader from a source object
22
+ *
23
+ * @param {MemorySource} source
24
+ *
25
+ * @returns {AudioLoader}
27
26
  */
28
- load(): void;
29
- destroy(): void;
27
+ getLoaderFromSource(source: MemorySource): AudioLoader;
30
28
  /**
31
29
  * Add in-memory audio source
32
30
  * - Uses an AudioLoader instance to load audio data from network
@@ -56,18 +54,33 @@ export default class AudioScene {
56
54
  */
57
55
  setAudioContext(audioContext?: AudioContext): void;
58
56
  /**
59
- * Set target gain
57
+ * Set target gain (volume level) for all audio in this scene
58
+ * - Currently applies immediately, but "target" allows for future
59
+ * smooth transitions using Web Audio API's gain scheduling
60
+ * - Range: 0.0 (silence) to 1.0 (original) to 1.0+ (amplified)
60
61
  *
61
- * @param {number} value
62
+ * @param {number} value - Target gain value (0.0 to 1.0+)
62
63
  */
63
64
  setTargetGain(value: number): void;
64
65
  /**
65
- * Get the scene gain
66
+ * Get the current target gain (volume level)
66
67
  *
67
- * @returns {number} value
68
+ * @returns {number} Target gain value (0.0 to 1.0+)
68
69
  */
69
70
  getTargetGain(): number;
71
+ /**
72
+ * Mute all audio in this scene
73
+ * - Saves current volume level for restoration
74
+ * - Sets target gain to 0 (silence)
75
+ * - Safe to call multiple times
76
+ */
70
77
  mute(): void;
78
+ /**
79
+ * Unmute all audio in this scene
80
+ * - Restores volume level from before muting
81
+ * - Safe to call multiple times
82
+ * - No effect if scene is not currently muted
83
+ */
71
84
  unmute(): void;
72
85
  #private;
73
86
  }
@@ -80,4 +93,5 @@ export type MemorySource = {
80
93
  audioLoader: AudioLoader;
81
94
  config?: SourceConfig;
82
95
  };
96
+ import SceneBase from '../base/SceneBase.svelte.js';
83
97
  import AudioLoader from './AudioLoader.svelte.js';
@@ -1,15 +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
- LOAD,
10
- LOADED
11
- } from '../../../state/machines.js';
12
-
3
+ import SceneBase from '../base/SceneBase.svelte.js';
13
4
  import AudioLoader from './AudioLoader.svelte.js';
14
5
 
15
6
  /**
@@ -24,15 +15,7 @@ import AudioLoader from './AudioLoader.svelte.js';
24
15
  * @property {SourceConfig} [config]
25
16
  */
26
17
 
27
- export default class AudioScene {
28
- #state = new LoadingStateMachine();
29
-
30
- // @note this exported state is set by onenter
31
- state = $state(STATE_INITIAL);
32
-
33
- loaded = $derived.by(() => {
34
- return this.state === STATE_LOADED;
35
- });
18
+ export default class AudioScene extends SceneBase {
36
19
 
37
20
  #targetGain = $state(1);
38
21
 
@@ -51,92 +34,35 @@ export default class AudioScene {
51
34
  /** @type {MemorySource[]} */
52
35
  #memorySources = $state([]);
53
36
 
54
- #progress = $derived.by(() => {
55
- // console.log('update progress');
56
-
57
- let totalSize = 0;
58
- let totalBytesLoaded = 0;
59
- let sourcesLoaded = 0;
60
-
61
- const sources = this.#memorySources;
62
- const numberOfSources = sources.length;
63
-
64
- for (let j = 0; j < numberOfSources; j++) {
65
- const source = sources[j];
66
- const { audioLoader } = source;
67
-
68
- const { bytesLoaded, size, loaded } = audioLoader.progress;
69
-
70
- totalSize += size;
71
- totalBytesLoaded += bytesLoaded;
72
-
73
- if (loaded) {
74
- sourcesLoaded++;
75
- }
76
- } // end for
77
-
78
- return {
79
- totalBytesLoaded,
80
- totalSize,
81
- sourcesLoaded,
82
- numberOfSources
83
- };
84
- });
85
37
 
86
38
  /**
87
39
  * Construct AudioScene
88
40
  */
89
41
  constructor() {
90
- const state = this.#state;
91
-
92
- $effect(() => {
93
- if (state.current === STATE_LOADING) {
94
- // console.log(
95
- // 'progress',
96
- // JSON.stringify($state.snapshot(this.#progress))
97
- // );
98
-
99
- const { sourcesLoaded, numberOfSources } = this.#progress;
100
-
101
- if (sourcesLoaded === numberOfSources) {
102
- // console.debug(`AudioScene: ${numberOfSources} sources loaded`);
103
- this.#state.send(LOADED);
104
- }
105
- }
106
- });
107
-
108
- state.onenter = ( currentState ) => {
109
- // console.log('onenter', currentState );
110
-
111
- if(currentState === STATE_LOADING )
112
- {
113
- // console.log('AudioScene:loading');
114
- this.#startLoading();
115
- }
116
-
117
- this.state = state.current;
118
- };
42
+ super();
119
43
  }
120
44
 
121
- /* ==== Common loader interface */
45
+ /* ==== SceneBase implementation */
122
46
 
123
47
  /**
124
- * Get audio scene loading progress
125
- */
126
- get progress() {
127
- return this.#progress;
128
- }
129
-
130
- /**
131
- * Start loading all audio sources
48
+ * Get the array of memory sources managed by this scene
49
+ *
50
+ * @returns {MemorySource[]}
132
51
  */
133
- load() {
134
- this.#state.send(LOAD);
52
+ get sources() {
53
+ return this.#memorySources;
135
54
  }
136
55
 
137
- destroy() {
138
- // TODO: disconnect all audio sources?
139
- // TODO: Unload AUdioLoaders?
56
+ /**
57
+ * Extract the audio loader from a source object
58
+ *
59
+ * @param {MemorySource} source
60
+ *
61
+ * @returns {AudioLoader}
62
+ */
63
+ // eslint-disable-next-line no-unused-vars
64
+ getLoaderFromSource(source) {
65
+ return source.audioLoader;
140
66
  }
141
67
 
142
68
  /* ==== Source definitions */
@@ -204,20 +130,16 @@ export default class AudioScene {
204
130
  this.#audioContext = audioContext;
205
131
  }
206
132
 
207
- async #startLoading() {
208
- // console.log('#startLoading');
209
-
210
- for (const { audioLoader } of this.#memorySources) {
211
- audioLoader.load();
212
- }
213
- }
214
133
 
215
134
  /* ==== Audio specific */
216
135
 
217
136
  /**
218
- * 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)
219
141
  *
220
- * @param {number} value
142
+ * @param {number} value - Target gain value (0.0 to 1.0+)
221
143
  */
222
144
  setTargetGain( value ) {
223
145
  this.#targetGain = value;
@@ -227,15 +149,21 @@ export default class AudioScene {
227
149
  }
228
150
 
229
151
  /**
230
- * Get the scene gain
152
+ * Get the current target gain (volume level)
231
153
  *
232
- * @returns {number} value
154
+ * @returns {number} Target gain value (0.0 to 1.0+)
233
155
  */
234
156
  getTargetGain()
235
157
  {
236
158
  return this.#targetGain;
237
159
  }
238
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
+ */
239
167
  mute() {
240
168
  if( this.muted )
241
169
  {
@@ -246,6 +174,12 @@ export default class AudioScene {
246
174
  this.setTargetGain(0);
247
175
  }
248
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
+ */
249
183
  unmute() {
250
184
  if( !this.muted )
251
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
+ }