()
| 257 | // --- lookahead scheduler --- |
| 258 | |
| 259 | async function _scheduleNext() { |
| 260 | const chunkIdx = Math.floor(_scheduledTo / CHUNK_SEC); |
| 261 | _fetchChunk(chunkIdx + 1); // fire-and-forget pre-fetch of the chunk after |
| 262 | |
| 263 | // Use the synchronous result if already decoded, otherwise await. |
| 264 | const hit = _cache.get(chunkIdx); |
| 265 | const buffers = hit?.result ?? await _fetchChunk(chunkIdx); |
| 266 | if (!playing || destroyed) return; |
| 267 | if (!buffers || buffers.size === 0) return; // past end; tick() handles onEnded |
| 268 | |
| 269 | // With SoundTouch, sources play at 1.0x so scheduled wall-clock time equals |
| 270 | // audio time. With tape-effect (_playbackRate != 1, no stNode), sources play |
| 271 | // faster/slower so wall-clock time = audio seconds / _playbackRate. |
| 272 | const playFactor = stNode ? 1.0 : _playbackRate; |
| 273 | const idealWhen = _startCtxTime + (_scheduledTo - _startOffset) / playFactor; |
| 274 | const when = Math.max(idealWhen, ctx.currentTime + 0.01); |
| 275 | // If we're late (slow network), skip the audio portion that already "passed". |
| 276 | const firstBuf = buffers.values().next().value; |
| 277 | const maxSkip = firstBuf ? Math.max(0, firstBuf.duration - 0.001) : 0; |
| 278 | const bufOffset = Math.min(Math.max(0, when - idealWhen) * playFactor, maxSkip); |
| 279 | |
| 280 | const dur = _scheduleChunk(buffers, when, bufOffset); |
| 281 | _scheduledTo += bufOffset + dur; // always advances by ~CHUNK_SEC |
| 282 | } |
| 283 | |
| 284 | function _maybeSchedule() { |
| 285 | if (_filling || !playing || destroyed) return; |
no test coverage detected