Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions src/openai/helpers/local_audio_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,26 @@ def callback(
buffer_pos = 0

producer_task = asyncio.create_task(buffer_producer())

with sd.OutputStream(
samplerate=SAMPLE_RATE,
channels=self.channels,
dtype=self.dtype,
callback=callback,
):
await event.wait()

await producer_task
playback_task = asyncio.create_task(event.wait())

try:
with sd.OutputStream(
samplerate=SAMPLE_RATE,
channels=self.channels,
dtype=self.dtype,
callback=callback,
):
done, _ = await asyncio.wait(
(producer_task, playback_task),
return_when=asyncio.FIRST_COMPLETED,
)
if producer_task in done:
producer_task.result()
await playback_task

await producer_task
finally:
for task in (producer_task, playback_task):
if not task.done():
task.cancel()
await asyncio.gather(producer_task, playback_task, return_exceptions=True)
35 changes: 35 additions & 0 deletions tests/test_local_audio_player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from __future__ import annotations

import asyncio
from typing import Any
from collections.abc import AsyncGenerator

import pytest

from openai.helpers import local_audio_player


class SilentOutputStream:
def __init__(self, **kwargs: Any) -> None:
pass

def __enter__(self) -> SilentOutputStream:
return self

def __exit__(self, *args: Any) -> None:
pass


async def test_play_stream_propagates_producer_failure(monkeypatch: pytest.MonkeyPatch) -> None:
async def broken_stream() -> AsyncGenerator[None, None]:
if asyncio.current_task() is None:
yield None
raise RuntimeError("synthetic producer failure")

monkeypatch.setattr(local_audio_player.sd, "OutputStream", SilentOutputStream)

with pytest.raises(RuntimeError, match="synthetic producer failure"):
await asyncio.wait_for(
local_audio_player.LocalAudioPlayer().play_stream(broken_stream()),
timeout=1,
)