openaudio-suite 2.4.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/CHANGELOG.md +196 -0
- package/CONTRIBUTING.md +349 -0
- package/LICENSE +674 -0
- package/OpenAudio.js +386 -0
- package/OpenAudio_r.js +863 -0
- package/OpenAudio_s.js +445 -0
- package/README.md +433 -0
- package/docs/COMPARISON.md +449 -0
- package/docs/OPENAUDIO-v1.1.0.md +641 -0
- package/docs/OPENAUDIO_R.md +571 -0
- package/docs/OPENAUDIO_S.md +760 -0
- package/package.json +79 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
# OpenAudio_s.js API Reference
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0.0
|
|
4
|
+
**Class:** `SequentialAudio`
|
|
5
|
+
**Use Case:** Sequential/playlist playback with manual or auto-advance
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
// Create a player
|
|
13
|
+
const player = new SequentialAudio([
|
|
14
|
+
{ src: 'intro.mp3', label: 'Introduction' },
|
|
15
|
+
{ src: 'chapter1.mp3', label: 'Chapter 1' },
|
|
16
|
+
{ src: 'chapter2.mp3', label: 'Chapter 2' }
|
|
17
|
+
], {
|
|
18
|
+
autoAdvance: false, // Require manual click to advance
|
|
19
|
+
onPlay: (clip) => console.log(`Playing: ${clip.label}`),
|
|
20
|
+
onComplete: () => console.log('All clips finished!')
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Start the sequence
|
|
24
|
+
document.getElementById('play-btn').addEventListener('click', () => {
|
|
25
|
+
player.play();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Advance to next clip
|
|
29
|
+
document.getElementById('next-btn').addEventListener('click', () => {
|
|
30
|
+
player.next();
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Constructor
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
new SequentialAudio(clips, options)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Parameters
|
|
43
|
+
|
|
44
|
+
#### `clips` (array, required)
|
|
45
|
+
Array of clip objects in playback order.
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
[
|
|
49
|
+
{ src: 'clip1.mp3', label: 'First Clip' },
|
|
50
|
+
{ src: 'clip2.mp3', label: 'Second Clip' },
|
|
51
|
+
{ src: 'clip3.mp3', label: 'Third Clip' }
|
|
52
|
+
]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Each clip object:
|
|
56
|
+
- **`src`** (string, required) — Path or data URI to audio file
|
|
57
|
+
- **`label`** (string) — Display name for console/callbacks
|
|
58
|
+
|
|
59
|
+
#### `options` (object, optional)
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
{
|
|
63
|
+
autoAdvance: false, // Boolean: auto-play next clip after current ends
|
|
64
|
+
loop: false, // Boolean: loop sequence when complete
|
|
65
|
+
onPlay: (clip) => {}, // Function: called when clip starts
|
|
66
|
+
onEnd: (clip) => {}, // Function: called when clip ends
|
|
67
|
+
onComplete: () => {} // Function: called when sequence finishes
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**All options are optional.**
|
|
72
|
+
|
|
73
|
+
### Constructor Examples
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// Minimal
|
|
77
|
+
const player = new SequentialAudio([
|
|
78
|
+
{ src: 'clip1.mp3' },
|
|
79
|
+
{ src: 'clip2.mp3' }
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// With manual advance (require click between clips)
|
|
83
|
+
const player = new SequentialAudio(clips, {
|
|
84
|
+
autoAdvance: false,
|
|
85
|
+
onPlay: (clip) => updateUI(clip.label)
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// With auto-advance (play next automatically)
|
|
89
|
+
const player = new SequentialAudio(clips, {
|
|
90
|
+
autoAdvance: true,
|
|
91
|
+
onComplete: () => showFinishScreen()
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// With looping
|
|
95
|
+
const player = new SequentialAudio(clips, {
|
|
96
|
+
loop: true, // Restart sequence when complete
|
|
97
|
+
onComplete: () => console.log('Looping...')
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Constructor Errors
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
// Throws TypeError if clips is missing, empty, or invalid
|
|
105
|
+
new SequentialAudio(); // ❌ TypeError
|
|
106
|
+
new SequentialAudio([]); // ❌ TypeError
|
|
107
|
+
new SequentialAudio([{ src: '' }]); // ❌ Empty src
|
|
108
|
+
new SequentialAudio([{ label: 'No src' }]); // ❌ Missing src
|
|
109
|
+
|
|
110
|
+
// Valid
|
|
111
|
+
new SequentialAudio([
|
|
112
|
+
{ src: 'clip.mp3', label: 'Clip' }
|
|
113
|
+
]); // ✅
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Public Methods
|
|
119
|
+
|
|
120
|
+
### `play()`
|
|
121
|
+
|
|
122
|
+
Start playback from the first clip (or current position if paused).
|
|
123
|
+
|
|
124
|
+
**Must be called inside a user gesture on first use.**
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
// First call (inside gesture)
|
|
128
|
+
document.addEventListener('click', () => {
|
|
129
|
+
player.play(); // ✅ Starts from clip 0
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Subsequent calls
|
|
133
|
+
player.play(); // Safe to call anytime
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Behavior:**
|
|
137
|
+
- If already playing: ignored (safe)
|
|
138
|
+
- If paused: resumes from current position
|
|
139
|
+
- If finished: starts from beginning
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### `next()`
|
|
144
|
+
|
|
145
|
+
Advance to the next clip and play it.
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
player.next(); // Play next clip
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Behavior:**
|
|
152
|
+
- Increments clip index by 1
|
|
153
|
+
- Plays the new clip
|
|
154
|
+
- At end of sequence:
|
|
155
|
+
- If `loop: true` → Wraps to beginning and plays clip 0
|
|
156
|
+
- If `loop: false` → Stops and fires `onComplete`
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
const player = new SequentialAudio([clip1, clip2, clip3], {
|
|
160
|
+
loop: false
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
player.goto(0); // Clip 0
|
|
164
|
+
player.next(); // Clip 1
|
|
165
|
+
player.next(); // Clip 2
|
|
166
|
+
player.next(); // Stops, fires onComplete
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### `goto(index)`
|
|
172
|
+
|
|
173
|
+
Jump to a specific clip by index and play it immediately.
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
player.goto(0); // Jump to first clip
|
|
177
|
+
player.goto(2); // Jump to third clip
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Parameters:**
|
|
181
|
+
- **`index`** (number) — 0-based clip index
|
|
182
|
+
|
|
183
|
+
**Behavior:**
|
|
184
|
+
- Jumps to the specified clip and plays it
|
|
185
|
+
- Validates index (warns if out of range, no-op)
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
const player = new SequentialAudio([c1, c2, c3, c4]);
|
|
189
|
+
|
|
190
|
+
player.goto(1); // ✅ Play clip 2
|
|
191
|
+
player.goto(10); // ❌ Warning: out of range
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### `gotoLabel(label)`
|
|
197
|
+
|
|
198
|
+
Jump to a clip by its label and play it.
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
player.gotoLabel('Chapter 5');
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Parameters:**
|
|
205
|
+
- **`label`** (string) — Exact label match
|
|
206
|
+
|
|
207
|
+
**Behavior:**
|
|
208
|
+
- Searches clips for matching label
|
|
209
|
+
- Jumps to first match
|
|
210
|
+
- Warns if label not found
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
const player = new SequentialAudio([
|
|
214
|
+
{ src: 'intro.mp3', label: 'Introduction' },
|
|
215
|
+
{ src: 'ch1.mp3', label: 'Chapter 1' },
|
|
216
|
+
{ src: 'ch2.mp3', label: 'Chapter 2' }
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
player.gotoLabel('Chapter 1'); // ✅ Jumps to index 1
|
|
220
|
+
player.gotoLabel('Chapter 99'); // ❌ Warning: not found
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### `pause()`
|
|
226
|
+
|
|
227
|
+
Pause playback without changing the clip or position.
|
|
228
|
+
|
|
229
|
+
```javascript
|
|
230
|
+
player.pause();
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Behavior:**
|
|
234
|
+
- Pauses audio at current position
|
|
235
|
+
- Sets `isPlaying = false`
|
|
236
|
+
- Call `resume()` to continue from same position
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
player.play(); // Playing clip 1, 2:30 elapsed
|
|
240
|
+
player.pause(); // Paused at 2:30
|
|
241
|
+
player.resume(); // Resume from 2:30
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### `resume()`
|
|
247
|
+
|
|
248
|
+
Resume playback from where it was paused.
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
player.resume();
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Behavior:**
|
|
255
|
+
- If paused: resumes audio
|
|
256
|
+
- If already playing: no-op (safe)
|
|
257
|
+
|
|
258
|
+
```javascript
|
|
259
|
+
player.play(); // Playing
|
|
260
|
+
player.pause(); // Paused
|
|
261
|
+
player.resume(); // Resume from pause point
|
|
262
|
+
player.resume(); // No-op (already playing)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### `stop()`
|
|
268
|
+
|
|
269
|
+
Stop playback and rewind to the beginning of the current clip.
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
player.stop();
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Behavior:**
|
|
276
|
+
- Pauses audio
|
|
277
|
+
- Rewinds to 0:00 of current clip
|
|
278
|
+
- Sets `isPlaying = false`
|
|
279
|
+
- Calling `play()` after will restart from beginning
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
player.play(); // Playing clip 1, 2:30 elapsed
|
|
283
|
+
player.stop(); // Stopped, rewound to 0:00
|
|
284
|
+
player.play(); // Restart from 0:00
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### `reset()`
|
|
290
|
+
|
|
291
|
+
Reset the sequence to the first clip without playing.
|
|
292
|
+
|
|
293
|
+
```javascript
|
|
294
|
+
player.reset();
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Behavior:**
|
|
298
|
+
- Stops playback
|
|
299
|
+
- Resets to clip index 0
|
|
300
|
+
- Resets `isStarted` flag
|
|
301
|
+
- Calling `play()` will start from beginning
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
player.goto(3); // At clip 4
|
|
305
|
+
player.reset(); // Back to clip 0, stopped
|
|
306
|
+
player.play(); // Play clip 0
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
### `getCurrentClip()`
|
|
312
|
+
|
|
313
|
+
Get the current clip object.
|
|
314
|
+
|
|
315
|
+
```javascript
|
|
316
|
+
const clip = player.getCurrentClip();
|
|
317
|
+
console.log(clip.label, clip.src);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Returns:** Current clip object `{ src, label, ... }`
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
### `getCurrentIndex()`
|
|
325
|
+
|
|
326
|
+
Get the current clip index (0-based).
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
const index = player.getCurrentIndex();
|
|
330
|
+
console.log(`Playing clip ${index + 1}`);
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Returns:** Number (0 to clipCount - 1)
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### `getClipCount()`
|
|
338
|
+
|
|
339
|
+
Get total number of clips in the sequence.
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
const total = player.getClipCount();
|
|
343
|
+
console.log(`${total} clips in sequence`);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Returns:** Number
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
### `getClips()`
|
|
351
|
+
|
|
352
|
+
Get a copy of all clips.
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
const allClips = player.getClips();
|
|
356
|
+
console.log(`Sequence has ${allClips.length} clips`);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Returns:** Array of clip objects (copy, not reference)
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
### `destroy()`
|
|
364
|
+
|
|
365
|
+
Clean up and remove all references. Call on SPA unmount.
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
player.destroy();
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Behavior:**
|
|
372
|
+
- Stops playback
|
|
373
|
+
- Clears audio element
|
|
374
|
+
- Nullifies internal references
|
|
375
|
+
- After this, don't call any other methods
|
|
376
|
+
|
|
377
|
+
```javascript
|
|
378
|
+
// React example
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
const player = new SequentialAudio(clips);
|
|
381
|
+
return () => player.destroy();
|
|
382
|
+
}, []);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
### `canPlay(type)` — Static Method
|
|
388
|
+
|
|
389
|
+
Check if browser supports an audio format.
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
SequentialAudio.canPlay('audio/mpeg') → boolean
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Parameters:**
|
|
396
|
+
- **`type`** (string) — MIME type to check
|
|
397
|
+
- `'audio/mpeg'` — MP3
|
|
398
|
+
- `'audio/ogg'` — OGG Vorbis
|
|
399
|
+
- `'audio/wav'` — WAV
|
|
400
|
+
- `'audio/webm'` — WebM
|
|
401
|
+
|
|
402
|
+
**Returns:** `true` if supported, `false` otherwise
|
|
403
|
+
|
|
404
|
+
```javascript
|
|
405
|
+
if (SequentialAudio.canPlay('audio/ogg')) {
|
|
406
|
+
// Use OGG files
|
|
407
|
+
} else {
|
|
408
|
+
// Fall back to MP3
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Public Properties
|
|
415
|
+
|
|
416
|
+
### `isPlaying`
|
|
417
|
+
|
|
418
|
+
**Type:** `boolean` (read-only)
|
|
419
|
+
|
|
420
|
+
`true` if a clip is actively playing, `false` if paused or stopped.
|
|
421
|
+
|
|
422
|
+
```javascript
|
|
423
|
+
player.play();
|
|
424
|
+
|
|
425
|
+
if (player.isPlaying) {
|
|
426
|
+
console.log('Clip is playing');
|
|
427
|
+
} else {
|
|
428
|
+
console.log('Clip is paused or stopped');
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
### `isStarted`
|
|
435
|
+
|
|
436
|
+
**Type:** `boolean` (read-only)
|
|
437
|
+
|
|
438
|
+
`true` after first `play()` call (sequence has been unlocked and started).
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
const player = new SequentialAudio(clips);
|
|
442
|
+
|
|
443
|
+
console.log(player.isStarted); // false
|
|
444
|
+
|
|
445
|
+
player.play();
|
|
446
|
+
|
|
447
|
+
console.log(player.isStarted); // true
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Callbacks
|
|
453
|
+
|
|
454
|
+
### `onPlay(clip)`
|
|
455
|
+
|
|
456
|
+
Called when a clip **starts playing**.
|
|
457
|
+
|
|
458
|
+
```javascript
|
|
459
|
+
const player = new SequentialAudio(clips, {
|
|
460
|
+
onPlay: (clip) => {
|
|
461
|
+
console.log(`Now playing: ${clip.label}`);
|
|
462
|
+
updateProgressBar(clip.label);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Parameter:**
|
|
468
|
+
- **`clip`** — The clip object being played `{ src, label }`
|
|
469
|
+
|
|
470
|
+
**Thrown errors are caught:** If your callback throws, the error is logged but playback continues.
|
|
471
|
+
|
|
472
|
+
```javascript
|
|
473
|
+
const player = new SequentialAudio(clips, {
|
|
474
|
+
onPlay: (clip) => {
|
|
475
|
+
throw new Error('Oops!');
|
|
476
|
+
// Error logged, playback unaffected
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
### `onEnd(clip)`
|
|
484
|
+
|
|
485
|
+
Called when a clip **finishes playing** (reaches natural end).
|
|
486
|
+
|
|
487
|
+
Does NOT fire if you call `stop()` or `pause()` before the clip ends.
|
|
488
|
+
|
|
489
|
+
```javascript
|
|
490
|
+
const player = new SequentialAudio(clips, {
|
|
491
|
+
onEnd: (clip) => {
|
|
492
|
+
console.log(`Finished: ${clip.label}`);
|
|
493
|
+
// Auto-advance is handled separately
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**Parameter:**
|
|
499
|
+
- **`clip`** — The clip object that ended
|
|
500
|
+
|
|
501
|
+
**After `onEnd`:**
|
|
502
|
+
- If `autoAdvance: true` → Next clip starts automatically (after 500ms delay)
|
|
503
|
+
- If `autoAdvance: false` → Sequence pauses, waiting for `next()` call
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
### `onComplete()`
|
|
508
|
+
|
|
509
|
+
Called when the entire sequence finishes (no more clips).
|
|
510
|
+
|
|
511
|
+
Only fires if `autoAdvance: true` OR user explicitly calls `next()` on the last clip.
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
const player = new SequentialAudio(clips, {
|
|
515
|
+
onComplete: () => {
|
|
516
|
+
console.log('Sequence complete!');
|
|
517
|
+
showCongratulationsScreen();
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**After `onComplete`:**
|
|
523
|
+
- If `loop: true` → Sequence wraps to beginning, ready for replay
|
|
524
|
+
- If `loop: false` → Sequence stops
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Usage Patterns
|
|
529
|
+
|
|
530
|
+
### Pattern 1: Narrated Story (Manual Advance)
|
|
531
|
+
|
|
532
|
+
```javascript
|
|
533
|
+
const story = new SequentialAudio([
|
|
534
|
+
{ src: 'chapter1.mp3', label: 'Chapter 1' },
|
|
535
|
+
{ src: 'chapter2.mp3', label: 'Chapter 2' },
|
|
536
|
+
{ src: 'chapter3.mp3', label: 'Chapter 3' }
|
|
537
|
+
], {
|
|
538
|
+
autoAdvance: false, // User controls pacing
|
|
539
|
+
onPlay: (clip) => updateUI(`Reading: ${clip.label}`),
|
|
540
|
+
onComplete: () => showTheEnd()
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
document.addEventListener('click', () => story.play(), { once: true });
|
|
544
|
+
document.getElementById('next-btn').addEventListener('click', () => story.next());
|
|
545
|
+
document.getElementById('prev-btn').addEventListener('click', () => {
|
|
546
|
+
story.goto(Math.max(0, story.getCurrentIndex() - 1));
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
### Pattern 2: Tutorial with Auto-Advance
|
|
553
|
+
|
|
554
|
+
```javascript
|
|
555
|
+
const tutorial = new SequentialAudio([
|
|
556
|
+
{ src: 'intro.mp3', label: 'Introduction' },
|
|
557
|
+
{ src: 'step1.mp3', label: 'Step 1' },
|
|
558
|
+
{ src: 'step2.mp3', label: 'Step 2' },
|
|
559
|
+
{ src: 'conclusion.mp3', label: 'Conclusion' }
|
|
560
|
+
], {
|
|
561
|
+
autoAdvance: true, // Auto-play next step
|
|
562
|
+
onPlay: (clip) => highlightStep(clip.label),
|
|
563
|
+
onComplete: () => showCompletionCertificate()
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
document.getElementById('start-tutorial').addEventListener('click', () => {
|
|
567
|
+
tutorial.play();
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
### Pattern 3: Interactive Quiz with Narration
|
|
574
|
+
|
|
575
|
+
```javascript
|
|
576
|
+
const quiz = new SequentialAudio([
|
|
577
|
+
{ src: 'question1.mp3', label: 'Question 1' },
|
|
578
|
+
{ src: 'question2.mp3', label: 'Question 2' },
|
|
579
|
+
{ src: 'question3.mp3', label: 'Question 3' },
|
|
580
|
+
{ src: 'results.mp3', label: 'Results' }
|
|
581
|
+
], {
|
|
582
|
+
autoAdvance: false,
|
|
583
|
+
onPlay: (clip) => {
|
|
584
|
+
// Show question UI
|
|
585
|
+
showQuestion(clip.label);
|
|
586
|
+
},
|
|
587
|
+
onComplete: () => {
|
|
588
|
+
// Show final score
|
|
589
|
+
showResults();
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// User answers question, then advances
|
|
594
|
+
function submitAnswer(answer) {
|
|
595
|
+
recordAnswer(answer);
|
|
596
|
+
quiz.next(); // Move to next question
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
### Pattern 4: Looping Background Narration
|
|
603
|
+
|
|
604
|
+
```javascript
|
|
605
|
+
const loopingNarration = new SequentialAudio([
|
|
606
|
+
{ src: 'intro.mp3', label: 'Intro' },
|
|
607
|
+
{ src: 'main.mp3', label: 'Main Message' },
|
|
608
|
+
{ src: 'outro.mp3', label: 'Outro' }
|
|
609
|
+
], {
|
|
610
|
+
autoAdvance: true,
|
|
611
|
+
loop: true, // Restart after outro
|
|
612
|
+
onPlay: (clip) => console.log(`[${clip.label}]`)
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
document.addEventListener('click', () => loopingNarration.play(), { once: true });
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
### Pattern 5: Guided Tour with Skip/Rewind
|
|
621
|
+
|
|
622
|
+
```javascript
|
|
623
|
+
const tour = new SequentialAudio([
|
|
624
|
+
{ src: 'welcome.mp3', label: 'Welcome' },
|
|
625
|
+
{ src: 'feature1.mp3', label: 'Feature 1: Dashboard' },
|
|
626
|
+
{ src: 'feature2.mp3', label: 'Feature 2: Settings' },
|
|
627
|
+
{ src: 'feature3.mp3', label: 'Feature 3: Profile' },
|
|
628
|
+
{ src: 'thanks.mp3', label: 'Thanks' }
|
|
629
|
+
], {
|
|
630
|
+
onPlay: (clip) => highlightFeature(clip.label),
|
|
631
|
+
onComplete: () => completeTour()
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// Play from start
|
|
635
|
+
document.getElementById('start-tour').addEventListener('click', () => tour.play());
|
|
636
|
+
|
|
637
|
+
// Jump to specific section
|
|
638
|
+
document.getElementById('skip-to-settings').addEventListener('click', () => {
|
|
639
|
+
tour.gotoLabel('Feature 2: Settings');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Rewind one step
|
|
643
|
+
document.getElementById('previous').addEventListener('click', () => {
|
|
644
|
+
const idx = Math.max(0, tour.getCurrentIndex() - 1);
|
|
645
|
+
tour.goto(idx);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Forward one step
|
|
649
|
+
document.getElementById('next').addEventListener('click', () => {
|
|
650
|
+
tour.next();
|
|
651
|
+
});
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Troubleshooting
|
|
657
|
+
|
|
658
|
+
### Audio Won't Play on First Click
|
|
659
|
+
|
|
660
|
+
**Problem:** Calling `play()` outside a user gesture.
|
|
661
|
+
|
|
662
|
+
**Solution:**
|
|
663
|
+
```javascript
|
|
664
|
+
// ✅ Correct
|
|
665
|
+
document.addEventListener('click', () => player.play());
|
|
666
|
+
|
|
667
|
+
// ❌ Wrong
|
|
668
|
+
setTimeout(() => player.play(), 1000);
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
### Can't Advance to Next Clip
|
|
674
|
+
|
|
675
|
+
**Problem:** Calling `next()` but nothing happens.
|
|
676
|
+
|
|
677
|
+
**Cause:** Already at last clip and `loop: false`.
|
|
678
|
+
|
|
679
|
+
**Solution:**
|
|
680
|
+
```javascript
|
|
681
|
+
if (player.getCurrentIndex() < player.getClipCount() - 1) {
|
|
682
|
+
player.next();
|
|
683
|
+
} else if (loop) {
|
|
684
|
+
// Already handled by library
|
|
685
|
+
player.next();
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
### Progress Bar Shows Wrong Index
|
|
692
|
+
|
|
693
|
+
**Problem:** UI shows wrong clip number.
|
|
694
|
+
|
|
695
|
+
**Solution:** `getCurrentIndex()` is 0-based, so display `index + 1`:
|
|
696
|
+
|
|
697
|
+
```javascript
|
|
698
|
+
const index = player.getCurrentIndex();
|
|
699
|
+
console.log(`Clip ${index + 1} of ${player.getClipCount()}`);
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
### onComplete Fires Too Early
|
|
705
|
+
|
|
706
|
+
**Problem:** Callback fires before last clip actually ends.
|
|
707
|
+
|
|
708
|
+
**Cause:** `autoAdvance: true` fires `next()` → triggers `onComplete` before last clip fully plays.
|
|
709
|
+
|
|
710
|
+
**Solution:** Use manual advance or check if you really want `onComplete` at this time:
|
|
711
|
+
|
|
712
|
+
```javascript
|
|
713
|
+
const player = new SequentialAudio(clips, {
|
|
714
|
+
autoAdvance: false, // User controls advancement
|
|
715
|
+
onComplete: () => {
|
|
716
|
+
// Now fires only when user explicitly ends sequence
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
---
|
|
722
|
+
|
|
723
|
+
## Browser Compatibility
|
|
724
|
+
|
|
725
|
+
| Browser | Support | Notes |
|
|
726
|
+
|---------|---------|-------|
|
|
727
|
+
| Chrome 70+ | ✅ Full | Autoplay policy: gesture required first time |
|
|
728
|
+
| Firefox 65+ | ✅ Full | Same as Chrome |
|
|
729
|
+
| Safari 12+ | ✅ Full | iOS: gesture required; Desktop: autoplay OK |
|
|
730
|
+
| Edge 79+ | ✅ Full | Same as Chrome |
|
|
731
|
+
| iOS Safari 12+ | ✅ Full | Gesture required; extensively tested |
|
|
732
|
+
| Chrome Android | ✅ Full | Gesture required |
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## Performance
|
|
737
|
+
|
|
738
|
+
- **File Size:** ~5 KB (minified)
|
|
739
|
+
- **Gzipped:** ~2 KB
|
|
740
|
+
- **Runtime Memory:** < 150 KB per player
|
|
741
|
+
- **CPU:** Negligible — HTML5 Audio element management only
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
## License
|
|
746
|
+
|
|
747
|
+
GNU General Public License v3.0 or later. See [LICENSE](../LICENSE).
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
## See Also
|
|
752
|
+
|
|
753
|
+
- [OpenAudio_r.js API](./OPENAUDIO_R.md) — Randomized scheduler
|
|
754
|
+
- [OpenAudio.js API](./OPENAUDIO.md) — Single-clip player
|
|
755
|
+
- [Feature Comparison](./COMPARISON.md) — Decide which library fits
|
|
756
|
+
- [Main README](../README.md) — OpenAudio suite overview
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
*Last updated: March 2025*
|