-
Notifications
You must be signed in to change notification settings - Fork 15
Fix pitch shift on mid-stream format change #180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||
| import queue | ||||||||||||||||||||||||||||||||||||||||
| import threading | ||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||
| from collections.abc import Callable | ||||||||||||||||||||||||||||||||||||||||
| from dataclasses import dataclass | ||||||||||||||||||||||||||||||||||||||||
| from typing import TYPE_CHECKING, cast | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -192,6 +193,41 @@ def _run( | |||||||||||||||||||||||||||||||||||||||
| chunk_item = cast(_ChunkWorkItem, item) | ||||||||||||||||||||||||||||||||||||||||
| fmt = chunk_item.fmt | ||||||||||||||||||||||||||||||||||||||||
| if current_format != fmt: | ||||||||||||||||||||||||||||||||||||||||
| # Format changed: drain old-format audio before switching | ||||||||||||||||||||||||||||||||||||||||
| # to prevent pitch shift from old PCM played at new sample rate. | ||||||||||||||||||||||||||||||||||||||||
| buffered_chunks: list[_ChunkWorkItem] = [chunk_item] | ||||||||||||||||||||||||||||||||||||||||
| drained = player.is_drained() | ||||||||||||||||||||||||||||||||||||||||
| deadline = time.monotonic() + 60.0 | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+198
to
+200
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| while not drained and time.monotonic() < deadline: | ||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| drain_item = queue_obj.get(timeout=0.01) | ||||||||||||||||||||||||||||||||||||||||
| except queue.Empty: | ||||||||||||||||||||||||||||||||||||||||
| drained = player.is_drained() | ||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| drain_type = type(drain_item) | ||||||||||||||||||||||||||||||||||||||||
| if drain_type is _StopWorkItem: | ||||||||||||||||||||||||||||||||||||||||
| player.stop() | ||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||
| if drain_type is _ClearWorkItem: | ||||||||||||||||||||||||||||||||||||||||
| player.clear() | ||||||||||||||||||||||||||||||||||||||||
| drained = True | ||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+213
to
+216
|
||||||||||||||||||||||||||||||||||||||||
| if drain_type is _SetVolumeWorkItem: | ||||||||||||||||||||||||||||||||||||||||
| vol = cast(_SetVolumeWorkItem, drain_item) | ||||||||||||||||||||||||||||||||||||||||
| software_volume = vol.volume | ||||||||||||||||||||||||||||||||||||||||
| software_muted = vol.muted | ||||||||||||||||||||||||||||||||||||||||
| player.set_volume(software_volume, muted=software_muted) | ||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||
| # Buffer incoming new-format chunks during drain | ||||||||||||||||||||||||||||||||||||||||
| buffered_chunks.append(cast(_ChunkWorkItem, drain_item)) | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+223
to
+224
|
||||||||||||||||||||||||||||||||||||||||
| # Buffer incoming new-format chunks during drain | |
| buffered_chunks.append(cast(_ChunkWorkItem, drain_item)) | |
| # Handle incoming chunk work items during drain based on format | |
| chunk = cast(_ChunkWorkItem, drain_item) | |
| if chunk.fmt == current_format: | |
| # Still receiving old-format audio: play it immediately | |
| payload = chunk.audio_data | |
| if current_format.codec == AudioCodec.FLAC and flac_decoder is not None: | |
| payload = flac_decoder.decode(payload) | |
| if not payload: | |
| drained = player.is_drained() | |
| continue | |
| player.submit(chunk.server_timestamp_us, payload) | |
| elif chunk.fmt == fmt: | |
| # Buffer incoming new-format chunks during drain | |
| buffered_chunks.append(chunk) | |
| else: | |
| # Unexpected third format during drain; buffer to preserve data | |
| buffered_chunks.append(chunk) |
Copilot
AI
Mar 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inside the buffered chunk processing loop, the if flac_decoder is None: flac_decoder = FlacDecoder(fmt) branch is redundant because flac_decoder was just initialized immediately above when fmt.codec == AudioCodec.FLAC (and set to None otherwise). Consider removing the inner None-check to simplify control flow (or, if you intend to allow lazy init here, avoid eager init above).
| if flac_decoder is None: | |
| flac_decoder = FlacDecoder(fmt) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new mid-stream format-switch behavior is fairly subtle (draining, buffering, handling stop/clear/volume during the drain). Since there are already unit tests around AudioStreamHandler in tests/test_audio_connector.py, it would be good to add a targeted test that simulates a format change with queued old-format chunks and verifies no old-format chunk is submitted after set_format(), and that clear during a pending switch drops buffered chunks.