fix: stretcher gapless playback race conditions and stability improvements#17
Merged
fix: stretcher gapless playback race conditions and stability improvements#17
Conversation
…nsition timer When onended fires before the transition setTimeout (common at 2x speed and in background tabs), the old code path would stop the already-playing gapless nextSource and create a new one from scratch, causing audio gaps. Extract doTransition() helper and add handleCurrentSourceEnded() that promotes the existing nextSource immediately when onended fires first, with a guard in the setTimeout callback to skip if already promoted.
…ll buffer - Add .catch() to dynamic import in play.ts to transition to stopped state on failure - Replace non-null assertion with null check in chunk-player.ts handleCurrentSourceEnded
- stretched-playback-import-error: verify .catch() on dynamic import failure - chunk-player-null-buffer: verify null buffer guard in handleCurrentSourceEnded
… chunk-player Add 194 tests across 7 new files covering previously untested paths: - Normal playback (play/pause/resume/seek/stop, events, edge cases) - Stretched playback (engine mock, event relay, pending operations) - Engine advanced paths (loop, buffering, seek, tempo change) - WorkerManager (pool init, postConvert, crash/respawn, terminate) - ChunkPlayer advanced (crossfade, lookahead, transition cancellation) Includes shared mock helpers in tests/helpers/audio-mocks.ts. Documents 3 confirmed potential bugs: loopEnd<=loopStart zero division, playbackRate=0 zero division, stop+ended statechange double-fire.
…prevent double statechange - loopEnd <= loopStart: fall back to non-loop position instead of NaN - playbackRate <= 0: clamp initial value to 1, ignore setPlaybackRate(0) - Stretched playback: introduce setState() helper to deduplicate state transitions
…hensive tests - Extract calcLoopPosition/calcPlaybackPosition into playback-position.ts (fixes negative modulo bug when elapsed < loopStart) - Extract createPlaybackStateManager into playback-state.ts (DRY: eliminates duplicated setState/startTimer/stopTimer between normal and stretched modes) - Extract calcPositionInOriginalBuffer into stretcher/position-calc.ts - Extract calcTransitionDelay into stretcher/transition-timing.ts - Export trimOverlap from engine.ts for direct testing - Add 74 new tests covering position calc, state management, trim-overlap, exitBuffering, transition timing, race conditions, conversion-scheduler edge cases, normal playback stress tests, and chunk management
Add comprehensive tests (91 cases) for context, buffer, nodes, waveform, fade, scheduler, synth, and adapters modules. Extend audio-mocks with AnalyserNode, BiquadFilterNode, StereoPannerNode, DynamicsCompressorNode mocks and createMockPlayback helper. Fix three bugs discovered during testing: - waveform: blockSize=0 causing NaN when resolution > data.length - waveform: extractRMS crash on 0-channel buffer with channel=-1 - adapters: whenPosition never resolving for already-passed positions
- Upgrade vitest to ^4.0.0, add @vitest/browser, @vitest/browser-playwright, playwright - Convert vitest.config.ts to projects configuration (unit + browser) - Add test:unit and test:browser npm scripts - Fix URL mock to preserve constructor (vitest 4.x uses new URL() internally) - Fix Worker mock to use function keyword (vitest 4.x requires it for new) - Replace fake timers with real timers in import-error test (vitest 4.x blocks dynamic import() resolution with fake timers)
…cation - context.browser.test.ts: AudioContext lifecycle, resume, currentTime - nodes.browser.test.ts: node creation, chain signal routing, rampGain - fade.browser.test.ts: linear/exponential/equal-power ramps, crossfade - play.browser.test.ts: onended, pause/resume, seek, loop, through chain - stretcher.browser.test.ts: WSOLA integration, tempo change, seek, ended
- Install @biomejs/biome, configure biome.json (space indent, double quotes, semicolons) - Replace `lint` script with Biome, add `format` and `check` scripts - Auto-format all src/ and tests/ files to match Biome rules
- Update architecture table (10 → 14 modules: stretcher, player, playback-state, playback-position) - Update test setup to Vitest 4.x projects config - Add testing policy, bug prevention checklist, and new module guide - Add Biome commands to Commands section
- Add /quality-check, /review, /add-test custom skills - Add PreCommit hook (typecheck) to settings.json
PreCommit is not a valid Claude Code hook event name. Replace with /commit skill for pre-commit checks.
…und tab resilience LOOKAHEAD_THRESHOLD_SEC 1.5→3.0 so throttled setInterval (1000ms) still gets 2-3 check opportunities. New PROACTIVE_SCHEDULE_THRESHOLD_SEC (5.0) used in onChunkReady (Worker callback, not throttled) for earlier scheduling.
chunk-player-throttle: lookahead under 1000ms throttle, onended-before-timer fallback, full background cycle scenario. engine-background-resilience: proactive schedule threshold validation, buffering transitions, disposed safety.
Apply Hann window to prevOutputFrame so NCC comparison is symmetric between reference and search frames. Add early return for tempo within ±0.001 of 1.0 to skip WSOLA entirely, preventing NCC search artifacts that cause audible wobble at identity speed.
Skip handleResult when chunk state is not 'converting', preventing stale worker results from overwriting re-queued chunks after tempo change.
…nce setTempo - Extract tryScheduleNext with dedup guard to prevent double-scheduling from lookahead and proactive paths - Add expectedTransitionFrom to reject stale onTransition callbacks - Scale crossfadeKeep by min(1, ratio) so trimOverlap works at high tempo - Debounce rapid setTempo calls (50ms) to batch re-conversion requests
… on completion - Add allDeadFired flag to prevent onAllDead from being called more than once - Delete postTimes entries on result, cancelled, error, and worker crash to prevent memory leak over long playback sessions
Add skipFadeIn parameter to playChunk/playCurrentChunk and pass true from engine.resume() so that resuming after pause does not apply an unnecessary 0.1s equal-power fade-in.
When setTempo was called during pause, it triggered enterBuffering which led to playCurrentChunk on chunk ready, resuming playback without user intent. Now setTempo during pause only stores the new tempo and defers the buffering flow until resume is explicitly called.
Save position with old tempo before updating currentTempo in setTempo() pause branch, and reuse saved bufferingResumePosition in resume() instead of recalculating with new tempo. Also extend position-calc to return bufferingResumePosition for paused state.
Add disposed checks to handleResult and handleError to prevent state mutations and callback invocations after scheduler disposal.
Handle bufferingResumePosition in resume path so that pause→seek→resume plays from the seek target. Prevent seek during pause from triggering enterBuffering prematurely.
Cover edge cases across chunk-player, engine, and worker-manager: - AudioNode disconnect on consecutive playChunk (P-02, L-01) - scheduleNext after pause (P-01) - buffering/pause/ended state transitions (S-01 through S-10) - stale transition guards on seek/setTempo (C-01 through C-05) - rapid seek and pause/resume toggles (R-01, R-02) - cancelChunk/terminate timing in worker-manager (W-01 through W-03, M-01, M-02)
CS-03: seek→setTempo / setTempo→seek sequential execution CS-05: restorePreviousTempo→handleTempoChange chain P-03: pause immediately after doTransition P-04: getCurrentPosition accuracy during crossfade R-03: start→pause before buffering completes (2 scenarios) MTP-a~d: main-thread-processor postTimes/cancelledChunks lifecycle
Change drawWaveform to accept a progress ratio (0-1) instead of a static fill style, coloring played bars darker and unplayed bars lighter. Wire progress through all call sites in demo-controller (onFrame, resetCursor, resize, seek, updateWaveform).
Interactive demo visualizing WSOLA stretcher chunk buffering behavior. Includes chunk state legend, real-time event log, and tempo select control.
Merge separate demos/ and use-cases/ sections into a single examples/ directory. Remove chunk-buffering and react-integration pages that are no longer needed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
setTempo()で再生位置がずれるバグを修正onAllDead二重呼び出し、スケジューリング競合などを解消examples/に統合Test plan
npm run typecheckパスnpm run lintパスnpm run test:unit— 593 テストすべてパスnpm run test:browser— ブラウザテスト通過確認