()
| 310 | // --- public API --- |
| 311 | |
| 312 | function play() { |
| 313 | if (playing || destroyed) return; |
| 314 | if (ctx.state === "suspended") ctx.resume().catch(() => {}); |
| 315 | playing = true; |
| 316 | |
| 317 | const chunkIdx = Math.floor(_startOffset / CHUNK_SEC); |
| 318 | const offsetWithin = _startOffset - chunkIdx * CHUNK_SEC; |
| 319 | |
| 320 | const startWith = (buffers) => { |
| 321 | if (!playing || destroyed) return; |
| 322 | const when = ctx.currentTime + 0.05; |
| 323 | _startCtxTime = when; |
| 324 | const dur = _scheduleChunk(buffers, when, offsetWithin); |
| 325 | _scheduledTo = _startOffset + dur; |
| 326 | _audioStarted = true; |
| 327 | _fetchChunk(chunkIdx + 1); // pre-fetch next chunk |
| 328 | rafId = requestAnimationFrame(_tick); |
| 329 | }; |
| 330 | |
| 331 | // chunk 0 is pre-decoded during ready(), so the sync path is the hot path. |
| 332 | const hit = _cache.get(chunkIdx); |
| 333 | if (hit?.result) { |
| 334 | startWith(hit.result); |
| 335 | } else { |
| 336 | _fetchChunk(chunkIdx) |
| 337 | .then(startWith) |
| 338 | .catch((e) => { |
| 339 | console.warn("[chunked] play fetch failed:", e); |
| 340 | playing = false; |
| 341 | _audioStarted = false; |
| 342 | }); |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | function pause() { |
| 347 | if (!playing) return; |
no test coverage detected