openaudio-suite 2.5.5 → 2.6.1

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,7 +1,7 @@
1
1
  # OpenAudio_s.js API Reference
2
2
 
3
- **Version:** 1.0.0
4
- **Class:** `SequentialAudio`
3
+ **Version:** 1.3.0
4
+ **Class:** `SequentialAudio`
5
5
  **Use Case:** Sequential/playlist playback with manual or auto-advance
6
6
 
7
7
  ---
@@ -9,26 +9,21 @@
9
9
  ## Quick Start
10
10
 
11
11
  ```javascript
12
- // Create a player
13
12
  const player = new SequentialAudio([
14
- { src: 'intro.mp3', label: 'Introduction' },
13
+ { src: 'intro.mp3', label: 'Introduction' },
15
14
  { src: 'chapter1.mp3', label: 'Chapter 1' },
16
15
  { src: 'chapter2.mp3', label: 'Chapter 2' }
17
16
  ], {
18
- autoAdvance: false, // Require manual click to advance
19
- onPlay: (clip) => console.log(`Playing: ${clip.label}`),
20
- onComplete: () => console.log('All clips finished!')
17
+ autoAdvance: false,
18
+ onPlay: (clip) => console.log(`Playing: ${clip.label}`),
19
+ onComplete: () => console.log('All clips finished!')
21
20
  });
22
21
 
23
- // Start the sequence
24
- document.getElementById('play-btn').addEventListener('click', () => {
25
- player.play();
26
- });
22
+ // Start must be inside a user gesture
23
+ document.getElementById('play-btn').addEventListener('click', () => player.play());
27
24
 
28
25
  // Advance to next clip
29
- document.getElementById('next-btn').addEventListener('click', () => {
30
- player.next();
31
- });
26
+ document.getElementById('next-btn').addEventListener('click', () => player.next());
32
27
  ```
33
28
 
34
29
  ---
@@ -54,63 +49,35 @@ Array of clip objects in playback order.
54
49
 
55
50
  Each clip object:
56
51
  - **`src`** (string, required) — Path or data URI to audio file
57
- - **`label`** (string) — Display name for console/callbacks
52
+ - **`label`** (string, optional) — Display name for console/callbacks
58
53
 
59
54
  #### `options` (object, optional)
60
55
 
61
56
  ```javascript
62
57
  {
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
58
+ autoAdvance: false, // Boolean: auto-play next clip after current ends
59
+ advanceDelay: 0.5, // Number: seconds to wait before auto-advance
60
+ loop: false, // Boolean: loop sequence when complete
61
+ volume: 1.0, // Number: volume 0.0–1.0
62
+ onPlay: (clip) => {},// Function: called when clip starts
63
+ onEnd: (clip) => {},// Function: called when clip ends
64
+ onComplete: () => {} // Function: called when sequence finishes
68
65
  }
69
66
  ```
70
67
 
71
68
  **All options are optional.**
72
69
 
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
70
  ### Constructor Errors
102
71
 
103
72
  ```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
73
+ // Throws TypeError
74
+ new SequentialAudio(); // ❌ Missing clips
75
+ new SequentialAudio([]); // ❌ Empty array
76
+ new SequentialAudio([{ src: '' }]); // ❌ Empty src
108
77
  new SequentialAudio([{ label: 'No src' }]); // ❌ Missing src
109
78
 
110
79
  // Valid
111
- new SequentialAudio([
112
- { src: 'clip.mp3', label: 'Clip' }
113
- ]); // ✅
80
+ new SequentialAudio([{ src: 'clip.mp3', label: 'Clip' }]); // ✅
114
81
  ```
115
82
 
116
83
  ---
@@ -119,205 +86,135 @@ new SequentialAudio([
119
86
 
120
87
  ### `play()`
121
88
 
122
- Start playback from the first clip (or current position if paused).
123
-
124
- **Must be called inside a user gesture on first use.**
89
+ Start the sequence. Must be called inside a user gesture on first use.
125
90
 
126
91
  ```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
92
+ document.addEventListener('click', () => player.play(), { once: true });
134
93
  ```
135
94
 
136
95
  **Behavior:**
137
- - If already playing: ignored (safe)
138
- - If paused: resumes from current position
139
- - If finished: starts from beginning
96
+ - If already started or playing: ignored (safe)
97
+ - If sequence has not started: unlocks Audio element and plays first clip
140
98
 
141
99
  ---
142
100
 
143
101
  ### `next()`
144
102
 
145
- Advance to the next clip and play it.
103
+ Advance to the next clip and play it. No-op if `play()` has not been called yet.
146
104
 
147
105
  ```javascript
148
- player.next(); // Play next clip
106
+ player.next();
149
107
  ```
150
108
 
151
109
  **Behavior:**
152
110
  - Increments clip index by 1
153
- - Plays the new clip
154
111
  - 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
- ```
112
+ - `loop: true` → wraps to clip 0
113
+ - `loop: false` → fires `onComplete`, stops
168
114
 
169
115
  ---
170
116
 
171
117
  ### `goto(index)`
172
118
 
173
- Jump to a specific clip by index and play it immediately.
119
+ Jump to a clip by zero-based index and play it.
174
120
 
175
121
  ```javascript
176
- player.goto(0); // Jump to first clip
177
- player.goto(2); // Jump to third clip
122
+ player.goto(0); // First clip
123
+ player.goto(2); // Third clip
124
+ player.goto(10); // ❌ Warning if out of range — no-op
178
125
  ```
179
126
 
180
127
  **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
- ```
128
+ - `index` (number) — 0-based clip index
193
129
 
194
130
  ---
195
131
 
196
132
  ### `gotoLabel(label)`
197
133
 
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
134
+ Jump to a clip by label (exact string match) and play it.
211
135
 
212
136
  ```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
137
+ player.gotoLabel('Chapter 1'); // Jumps to matching clip
138
+ player.gotoLabel('Chapter 99'); // Warning if not found — no-op
221
139
  ```
222
140
 
223
141
  ---
224
142
 
225
143
  ### `pause()`
226
144
 
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
145
+ Pause at current playback position.
237
146
 
238
147
  ```javascript
239
- player.play(); // Playing clip 1, 2:30 elapsed
240
- player.pause(); // Paused at 2:30
241
- player.resume(); // Resume from 2:30
148
+ player.play(); // Playing at 2:30
149
+ player.pause(); // Paused at 2:30
150
+ player.resume(); // Resumes from 2:30
242
151
  ```
243
152
 
244
153
  ---
245
154
 
246
155
  ### `resume()`
247
156
 
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)
157
+ Resume from paused position. No-op if not paused.
257
158
 
258
159
  ```javascript
259
- player.play(); // Playing
260
- player.pause(); // Paused
261
- player.resume(); // Resume from pause point
262
- player.resume(); // No-op (already playing)
160
+ player.resume(); // Resumes from pause point
161
+ player.resume(); // No-op if already playing
263
162
  ```
264
163
 
265
164
  ---
266
165
 
267
166
  ### `stop()`
268
167
 
269
- Stop playback and rewind to the beginning of the current clip.
168
+ Pause, rewind current clip to 0:00, and cancel any pending auto-advance timer.
270
169
 
271
170
  ```javascript
272
171
  player.stop();
273
172
  ```
274
173
 
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
174
+ From v1.2.0: `stop()` cancels any pending auto-advance `setTimeout`, so calling `stop()` during the inter-clip delay window correctly prevents the next clip from starting.
280
175
 
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
- ```
176
+ From v1.3.0: `stop()` also sets a `#playCancelled` flag. If `play()` is called and `stop()` (or `reset()`) is called before the silent-MP3 unlock resolves (~50–200 ms), the clip will not start when the unlock completes. Previously this race was unguarded — the clip would play despite the explicit stop.
286
177
 
287
178
  ---
288
179
 
289
180
  ### `reset()`
290
181
 
291
- Reset the sequence to the first clip without playing.
182
+ Stop and reset sequence to clip 0. Clears `isStarted` — next `play()` re-runs the unlock.
292
183
 
293
184
  ```javascript
294
- player.reset();
185
+ player.goto(3); // At clip 4
186
+ player.reset(); // Back to clip 0, stopped
187
+ player.play(); // Starts fresh from clip 0
295
188
  ```
296
189
 
297
- **Behavior:**
298
- - Stops playback
299
- - Resets to clip index 0
300
- - Resets `isStarted` flag
301
- - Calling `play()` will start from beginning
190
+ ---
191
+
192
+ ### `destroy()`
193
+
194
+ Release the Audio element, remove event listeners, and cancel any pending auto-advance timer. All subsequent method calls are safe no-ops.
302
195
 
303
196
  ```javascript
304
- player.goto(3); // At clip 4
305
- player.reset(); // Back to clip 0, stopped
306
- player.play(); // Play clip 0
197
+ player.destroy();
198
+
199
+ // React example
200
+ useEffect(() => {
201
+ const player = new SequentialAudio(clips);
202
+ return () => player.destroy();
203
+ }, []);
307
204
  ```
308
205
 
309
206
  ---
310
207
 
311
208
  ### `getCurrentClip()`
312
209
 
313
- Get the current clip object.
210
+ Get a **copy** of the current clip object. Returns a copy to prevent mutation of the internal playlist.
314
211
 
315
212
  ```javascript
316
213
  const clip = player.getCurrentClip();
317
214
  console.log(clip.label, clip.src);
318
215
  ```
319
216
 
320
- **Returns:** Current clip object `{ src, label, ... }`
217
+ **Returns:** `{ src: string, label: string }` (copy)
321
218
 
322
219
  ---
323
220
 
@@ -327,60 +224,30 @@ Get the current clip index (0-based).
327
224
 
328
225
  ```javascript
329
226
  const index = player.getCurrentIndex();
330
- console.log(`Playing clip ${index + 1}`);
227
+ console.log(`Clip ${index + 1} of ${player.getClipCount()}`);
331
228
  ```
332
229
 
333
- **Returns:** Number (0 to clipCount - 1)
334
-
335
230
  ---
336
231
 
337
232
  ### `getClipCount()`
338
233
 
339
- Get total number of clips in the sequence.
234
+ Get total number of clips.
340
235
 
341
236
  ```javascript
342
237
  const total = player.getClipCount();
343
- console.log(`${total} clips in sequence`);
344
238
  ```
345
239
 
346
- **Returns:** Number
347
-
348
240
  ---
349
241
 
350
242
  ### `getClips()`
351
243
 
352
- Get a copy of all clips.
244
+ Get a **deep copy** of all clip objects. Inner objects are copied to prevent callers from mutating the internal playlist.
353
245
 
354
246
  ```javascript
355
247
  const allClips = player.getClips();
356
- console.log(`Sequence has ${allClips.length} clips`);
357
248
  ```
358
249
 
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
- ```
250
+ **Returns:** `Array<{ src: string, label: string }>` (deep copy)
384
251
 
385
252
  ---
386
253
 
@@ -389,23 +256,17 @@ useEffect(() => {
389
256
  Check if browser supports an audio format.
390
257
 
391
258
  ```javascript
392
- SequentialAudio.canPlay('audio/mpeg') → boolean
259
+ SequentialAudio.canPlay('audio/mpeg') // → boolean
260
+ SequentialAudio.canPlay('audio/ogg') // → boolean
261
+ SequentialAudio.canPlay('audio/wav') // → boolean
262
+ SequentialAudio.canPlay('audio/webm') // → boolean
393
263
  ```
394
264
 
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
265
+ **Returns:** `true` if supported, `false` if unsupported or input is not a string.
403
266
 
404
267
  ```javascript
405
- if (SequentialAudio.canPlay('audio/ogg')) {
406
- // Use OGG files
407
- } else {
408
- // Fall back to MP3
268
+ if (!SequentialAudio.canPlay('audio/ogg')) {
269
+ console.warn('OGG not supported — use MP3 sources instead');
409
270
  }
410
271
  ```
411
272
 
@@ -415,17 +276,13 @@ if (SequentialAudio.canPlay('audio/ogg')) {
415
276
 
416
277
  ### `isPlaying`
417
278
 
418
- **Type:** `boolean` (read-only)
279
+ **Type:** `boolean` (read-only getter)
419
280
 
420
- `true` if a clip is actively playing, `false` if paused or stopped.
281
+ `true` if a clip is actively playing, `false` if paused, stopped, or between clips.
421
282
 
422
283
  ```javascript
423
- player.play();
424
-
425
284
  if (player.isPlaying) {
426
- console.log('Clip is playing');
427
- } else {
428
- console.log('Clip is paused or stopped');
285
+ player.pause();
429
286
  }
430
287
  ```
431
288
 
@@ -433,18 +290,16 @@ if (player.isPlaying) {
433
290
 
434
291
  ### `isStarted`
435
292
 
436
- **Type:** `boolean` (read-only)
293
+ **Type:** `boolean` (read-only getter)
437
294
 
438
- `true` after first `play()` call (sequence has been unlocked and started).
295
+ `true` after `play()` has been called and the sequence has been unlocked. `false` before first `play()` or after `reset()`.
439
296
 
440
297
  ```javascript
441
- const player = new SequentialAudio(clips);
442
-
443
298
  console.log(player.isStarted); // false
444
-
445
299
  player.play();
446
-
447
300
  console.log(player.isStarted); // true
301
+ player.reset();
302
+ console.log(player.isStarted); // false
448
303
  ```
449
304
 
450
305
  ---
@@ -458,76 +313,52 @@ Called when a clip **starts playing**.
458
313
  ```javascript
459
314
  const player = new SequentialAudio(clips, {
460
315
  onPlay: (clip) => {
461
- console.log(`Now playing: ${clip.label}`);
462
316
  updateProgressBar(clip.label);
463
317
  }
464
318
  });
465
319
  ```
466
320
 
467
- **Parameter:**
468
- - **`clip`** — The clip object being played `{ src, label }`
321
+ **Parameter:** `clip` — a copy of the clip object `{ src, label }`.
469
322
 
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
- ```
323
+ **Error handling:** Errors are caught and logged. A throwing callback will not stall playback.
480
324
 
481
325
  ---
482
326
 
483
327
  ### `onEnd(clip)`
484
328
 
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.
329
+ Called when a clip **finishes naturally**. Does not fire if `stop()` or `pause()` interrupts the clip.
488
330
 
489
331
  ```javascript
490
332
  const player = new SequentialAudio(clips, {
491
333
  onEnd: (clip) => {
492
334
  console.log(`Finished: ${clip.label}`);
493
- // Auto-advance is handled separately
494
335
  }
495
336
  });
496
337
  ```
497
338
 
498
- **Parameter:**
499
- - **`clip`** — The clip object that ended
500
-
501
339
  **After `onEnd`:**
502
- - If `autoAdvance: true` → Next clip starts automatically (after 500ms delay)
503
- - If `autoAdvance: false` → Sequence pauses, waiting for `next()` call
340
+ - `autoAdvance: true` → next clip starts after `advanceDelay` seconds
341
+ - `autoAdvance: false` → sequence waits for `next()` call
504
342
 
505
343
  ---
506
344
 
507
345
  ### `onComplete()`
508
346
 
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.
347
+ Called when the entire sequence finishes (no more clips and `loop: false`).
512
348
 
513
349
  ```javascript
514
350
  const player = new SequentialAudio(clips, {
515
351
  onComplete: () => {
516
- console.log('Sequence complete!');
517
352
  showCongratulationsScreen();
518
353
  }
519
354
  });
520
355
  ```
521
356
 
522
- **After `onComplete`:**
523
- - If `loop: true` → Sequence wraps to beginning, ready for replay
524
- - If `loop: false` → Sequence stops
525
-
526
357
  ---
527
358
 
528
359
  ## Usage Patterns
529
360
 
530
- ### Pattern 1: Narrated Story (Manual Advance)
361
+ ### Narrated Story (Manual Advance)
531
362
 
532
363
  ```javascript
533
364
  const story = new SequentialAudio([
@@ -535,9 +366,9 @@ const story = new SequentialAudio([
535
366
  { src: 'chapter2.mp3', label: 'Chapter 2' },
536
367
  { src: 'chapter3.mp3', label: 'Chapter 3' }
537
368
  ], {
538
- autoAdvance: false, // User controls pacing
539
- onPlay: (clip) => updateUI(`Reading: ${clip.label}`),
540
- onComplete: () => showTheEnd()
369
+ autoAdvance: false,
370
+ onPlay: (clip) => updateUI(`Reading: ${clip.label}`),
371
+ onComplete: () => showTheEnd()
541
372
  });
542
373
 
543
374
  document.addEventListener('click', () => story.play(), { once: true });
@@ -549,106 +380,64 @@ document.getElementById('prev-btn').addEventListener('click', () => {
549
380
 
550
381
  ---
551
382
 
552
- ### Pattern 2: Tutorial with Auto-Advance
383
+ ### Tutorial with Auto-Advance
553
384
 
554
385
  ```javascript
555
386
  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' }
387
+ { src: 'intro.mp3', label: 'Introduction' },
388
+ { src: 'step1.mp3', label: 'Step 1' },
389
+ { src: 'step2.mp3', label: 'Step 2' },
390
+ { src: 'conclusion.mp3', label: 'Conclusion' }
560
391
  ], {
561
- autoAdvance: true, // Auto-play next step
562
- onPlay: (clip) => highlightStep(clip.label),
563
- onComplete: () => showCompletionCertificate()
392
+ autoAdvance: true,
393
+ advanceDelay: 0.5,
394
+ onPlay: (clip) => highlightStep(clip.label),
395
+ onComplete: () => showCompletionCertificate()
564
396
  });
565
397
 
566
- document.getElementById('start-tutorial').addEventListener('click', () => {
567
- tutorial.play();
568
- });
398
+ document.getElementById('start-tutorial').addEventListener('click', () => tutorial.play());
569
399
  ```
570
400
 
571
401
  ---
572
402
 
573
- ### Pattern 3: Interactive Quiz with Narration
403
+ ### Interactive Quiz
574
404
 
575
405
  ```javascript
576
406
  const quiz = new SequentialAudio([
577
407
  { src: 'question1.mp3', label: 'Question 1' },
578
408
  { src: 'question2.mp3', label: 'Question 2' },
579
- { src: 'question3.mp3', label: 'Question 3' },
580
- { src: 'results.mp3', label: 'Results' }
409
+ { src: 'results.mp3', label: 'Results' }
581
410
  ], {
582
411
  autoAdvance: false,
583
- onPlay: (clip) => {
584
- // Show question UI
585
- showQuestion(clip.label);
586
- },
587
- onComplete: () => {
588
- // Show final score
589
- showResults();
590
- }
412
+ onPlay: (clip) => showQuestion(clip.label),
413
+ onComplete: () => showResults()
591
414
  });
592
415
 
593
- // User answers question, then advances
594
416
  function submitAnswer(answer) {
595
417
  recordAnswer(answer);
596
- quiz.next(); // Move to next question
418
+ quiz.next();
597
419
  }
598
420
  ```
599
421
 
600
422
  ---
601
423
 
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
424
+ ### Guided Tour with Skip/Rewind
621
425
 
622
426
  ```javascript
623
427
  const tour = new SequentialAudio([
624
- { src: 'welcome.mp3', label: 'Welcome' },
428
+ { src: 'welcome.mp3', label: 'Welcome' },
625
429
  { src: 'feature1.mp3', label: 'Feature 1: Dashboard' },
626
430
  { src: 'feature2.mp3', label: 'Feature 2: Settings' },
627
- { src: 'feature3.mp3', label: 'Feature 3: Profile' },
628
- { src: 'thanks.mp3', label: 'Thanks' }
431
+ { src: 'thanks.mp3', label: 'Thanks' }
629
432
  ], {
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);
433
+ onPlay: (clip) => highlightFeature(clip.label),
434
+ onComplete: () => completeTour()
646
435
  });
647
436
 
648
- // Forward one step
649
- document.getElementById('next').addEventListener('click', () => {
650
- tour.next();
651
- });
437
+ document.getElementById('start-tour').addEventListener('click', () => tour.play());
438
+ document.getElementById('skip-to-settings').addEventListener('click', () => tour.gotoLabel('Feature 2: Settings'));
439
+ document.getElementById('previous').addEventListener('click', () => tour.goto(Math.max(0, tour.getCurrentIndex() - 1)));
440
+ document.getElementById('next').addEventListener('click', () => tour.next());
652
441
  ```
653
442
 
654
443
  ---
@@ -657,9 +446,6 @@ document.getElementById('next').addEventListener('click', () => {
657
446
 
658
447
  ### Audio Won't Play on First Click
659
448
 
660
- **Problem:** Calling `play()` outside a user gesture.
661
-
662
- **Solution:**
663
449
  ```javascript
664
450
  // ✅ Correct
665
451
  document.addEventListener('click', () => player.play());
@@ -668,32 +454,17 @@ document.addEventListener('click', () => player.play());
668
454
  setTimeout(() => player.play(), 1000);
669
455
  ```
670
456
 
671
- ---
672
-
673
- ### Can't Advance to Next Clip
674
-
675
- **Problem:** Calling `next()` but nothing happens.
457
+ ### Stop Doesn't Prevent Auto-Advance (v1.1.0 and earlier)
676
458
 
677
- **Cause:** Already at last clip and `loop: false`.
459
+ This was fixed in v1.2.0. `stop()` now calls `clearTimeout` on the pending advance timer. Upgrade to v1.2.0 to resolve.
678
460
 
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
- ```
461
+ ### Stop During Unlock Still Plays Clip (v1.2.0 and earlier)
688
462
 
689
- ---
463
+ This was fixed in v1.3.0. `stop()` and `reset()` now set a `#playCancelled` flag that the unlock `.then()` checks before starting playback. Previously, calling `stop()` or `reset()` in the ~50–200 ms window between `play()` being called and the silent-MP3 unlock resolving was silently ignored — the clip would start anyway. Upgrade to v1.3.0 to resolve.
690
464
 
691
465
  ### Progress Bar Shows Wrong Index
692
466
 
693
- **Problem:** UI shows wrong clip number.
694
-
695
- **Solution:** `getCurrentIndex()` is 0-based, so display `index + 1`:
696
-
467
+ `getCurrentIndex()` is 0-based:
697
468
  ```javascript
698
469
  const index = player.getCurrentIndex();
699
470
  console.log(`Clip ${index + 1} of ${player.getClipCount()}`);
@@ -701,34 +472,15 @@ console.log(`Clip ${index + 1} of ${player.getClipCount()}`);
701
472
 
702
473
  ---
703
474
 
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
475
  ## Browser Compatibility
724
476
 
725
477
  | Browser | Support | Notes |
726
478
  |---------|---------|-------|
727
- | Chrome 70+ | ✅ Full | Autoplay policy: gesture required first time |
479
+ | Chrome 70+ | ✅ Full | Gesture required first time |
728
480
  | Firefox 65+ | ✅ Full | Same as Chrome |
729
- | Safari 12+ | ✅ Full | iOS: gesture required; Desktop: autoplay OK |
481
+ | Safari 12+ | ✅ Full | iOS: gesture required |
730
482
  | Edge 79+ | ✅ Full | Same as Chrome |
731
- | iOS Safari 12+ | ✅ Full | Gesture required; extensively tested |
483
+ | iOS Safari 12+ | ✅ Full | Extensively tested |
732
484
  | Chrome Android | ✅ Full | Gesture required |
733
485
 
734
486
  ---
@@ -738,13 +490,46 @@ const player = new SequentialAudio(clips, {
738
490
  - **File Size:** ~5 KB (minified)
739
491
  - **Gzipped:** ~2 KB
740
492
  - **Runtime Memory:** < 150 KB per player
741
- - **CPU:** Negligible — HTML5 Audio element management only
493
+ - **CPU:** Negligible
494
+
495
+ ---
496
+
497
+ ## Changelog
498
+
499
+ ### v1.3.0 (March 2026)
500
+ - **`#playCancelled` flag** — `stop()` and `reset()` now plant a cancellation token that the in-flight silent-MP3 unlock `.then()` checks before calling `#playClip()`. Previously, calling `stop()` or `reset()` during the ~50–200 ms unlock window was silently ignored — the clip would start regardless.
501
+ - **`stop()` doc updated** — Behaviour note now covers both the auto-advance timer cancellation (v1.2.0) and the new unlock cancellation (v1.3.0).
502
+ - **Usage example fix** — `goto(getCurrentIndex() - 1)` in the guided-tour example now uses `Math.max(0, …)`, preventing a spurious out-of-range `console.warn` when the user is already on the first clip.
503
+
504
+ ### v1.2.0 (March 2026)
505
+ - `isPlaying` and `isStarted` are now read-only getters backed by private fields
506
+ - `#isPlaying` and `#isStarted` set synchronously before `#audio.play()` in `#playClip()`, closing double-play race window
507
+ - `#advanceTimer` stored and cleared by `stop()`, `reset()`, and `destroy()`
508
+ - `resume()` race fixed — `#isPlaying` set synchronously, reverted in `.catch()`
509
+ - `destroy()` uses `removeAttribute('src')` + `load()` per WHATWG spec
510
+ - `getCurrentClip()` returns shallow copy; `getClips()` deep-copies inner objects
511
+
512
+ ### v1.1.0 (March 2026)
513
+ - Unlock now plays on the shared `#audio` element (fixes `NotAllowedError` on iOS Safari)
514
+ - `play()` guard extended to check `isStarted`
515
+ - `next()` / `goto()` / `gotoLabel()` guard against uninitialised player
516
+ - `pause()` / `resume()` guard on `isStarted`
517
+ - `destroy()` removes `ended` listener via stored `#endedHandler` reference
518
+ - `#isDestroyed` flag — all public methods safe no-ops after `destroy()`
519
+ - `advanceDelay` option added (default `0.5s`)
520
+
521
+ ### v1.0.0 (March 2026)
522
+ - Initial release: sequential playlist player
523
+ - Click-to-advance playback control
524
+ - Jump to clip by index (`goto()`) or label (`gotoLabel()`)
525
+ - Pause/resume support
526
+ - Progress tracking (`getCurrentClip()`, `getCurrentIndex()`, `getClipCount()`)
742
527
 
743
528
  ---
744
529
 
745
530
  ## License
746
531
 
747
- GNU General Public License v3.0 or later. See [LICENSE](../LICENSE).
532
+ Apache License 2.0. See [LICENSE](../LICENSE).
748
533
 
749
534
  ---
750
535
 
@@ -757,4 +542,4 @@ GNU General Public License v3.0 or later. See [LICENSE](../LICENSE).
757
542
 
758
543
  ---
759
544
 
760
- *Last updated: March 2025*
545
+ *Last updated: March 2026*