Skip to content

fix(tts): serialize shared playback stop and initialization - #242

Merged
Lucas1479 merged 1 commit into
mainfrom
codex/fix-241-playback-lifecycle
Oct 18, 2026
Merged

Lucas1479 merged 1 commit into
mainfrom
codex/fix-241-playback-lifecycle

Conversation

@Lucas1479

@Lucas1479 Lucas1479 commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

What and why

Related to #241.

StreamPlayer.stop() cleared is_playing and drained queued writes before acquiring the device lock. A concurrent initialize() could reopen the output and publish is_playing=True; the unfinished stop would then detach that new stream, leaving is_playing=True, stream=None. Stopping while a native open was in progress could leave the same inconsistent state. A subsequent write can raise audio stream is not initialized.

Serialize the stop's flag change, queue drain, and stream detachment/close under the existing reentrant device lock, matching initialize(). No new state, retry, dependency, or platform branch is added.

The defect is in the shared player and does not require AMD hardware to reproduce. This is a verified upstream race, but we have not established that it is the exact cause of the reporter's installation failure. It is independent of the GPT-SoVITS reference-loading changes in #238.

Change class

  • Routine fix

Owning layer: shared TTS output-stream lifecycle (tts/playback.py).

User-visible effect: concurrent stop/open operations leave either a stopped player or a usable replacement stream, rather than a playing flag with no stream. Existing playback-epoch invalidation still rejects stale audio.

Compatibility/migration: none; public APIs, device selection, backend routing, and bounded output-open recovery are unchanged.

Evidence

Windows / Python 3.12.10, CPU/model-less execution, synthetic PCM and mocked audio devices; no physical playback:

  • Deterministic concurrency regressions against unmodified upstream 7b97e21c63b680d3ad53b1a14198dd52de882593: 6 failed, 10 passed in tests/test_playback_device_recovery.py. Both stop/open orderings reproduce the inconsistent state. The fixed implementation passes all 16 cases.
  • Added cases cover reopening at the same or a different sample rate during stop, stop during cold/replacement open, sync/queued playback, an in-flight native write with epoch invalidation, and reentrant stop from a window callback. Thread events and observed lock acquisition control the interleavings; there are no scheduling sleeps.
  • Focused regressions: 125 passed, no skips, with AMADEUS_E2E_NO_TTS=1 TTS_DEVICE=cpu WORK_WORKTREE_ISOLATION=0:
    python -B -X utf8 -m pytest -q -p no:cacheprovider tests/test_playback_device_recovery.py tests/test_pyaudio_lifecycle.py tests/test_playback_idle_device.py tests/test_playback_mouth_timing.py tests/test_tts_handler.py tests/test_interrupt_flow.py tests/test_tts_contract.py tests/test_shared_presentation_alignment.py tests/test_turn_playback_complete.py
    
  • python -B -m ruff check tts/playback.py tests/test_playback_device_recovery.py and git diff --check: passed.
  • The expanded existing suite is already exercised by Windows full CI / T2, Linux voice, and macOS voice jobs; it is now also explicitly included in the Windows ROCm candidate job.
  • Full Windows model-less CI: all three Python shards passed, 5,953 passed / 35 skipped summed from the runner summaries.
  • Windows ROCm candidate CI: 143 passed / 1 skipped on Python 3.12.10 with the official torch/torchaudio 2.9.1+rocm7.2.1 packages. This includes the expanded playback suite, using mocked audio devices.
  • All 16 PR checks passed at 8b8ad82a771e064a79fd7decccdcd2eb2a676435, including Linux/macOS voice checks, Windows/Linux cu128, macOS MPS, the cu124 single-environment ladder, build, and desktop smoke checks. These do not constitute real headset or GPU playback validation.

Remaining validation: no real AMD/NVIDIA GPU, reporter headset, or Fish Audio end-to-end run was performed. Please test first-utterance playback, subsequent turns, interruption followed by a new reply, and streaming with the normal PyAudio path. The reporter's exact source revision/package and any local audio-library edits still need confirmation before attributing the whole report to this race.

Final check

  • One coherent lifecycle defect; no unrelated cleanup.
  • Existing synchronization contract reused; no speculative fallback or platform exception.
  • No private logs, credentials, model weights, voice assets, or local paths included.
  • Third-party notices and provenance preserved.

@Lucas1479
Lucas1479 marked this pull request as ready for review October 18, 2026 05:56
@Lucas1479
Lucas1479 merged commit d0a056a into main Oct 18, 2026
16 checks passed
@Lucas1479
Lucas1479 deleted the codex/fix-241-playback-lifecycle branch October 18, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant