Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ public APIs may still change while the backend design stabilizes.

## [Unreleased]

### Added

- Added the public `DuplexStream` API contract for backend-level full-duplex
capture and playback.

## [0.2.0a3] - 2026-06-12

### Changed
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ Use blocking helpers when callers need complete buffer transfer:
- `InputStream.read_exactly(frame_count, timeout=None)`: wait until exactly the
requested number of frames has been captured

Full-duplex capture/playback is exposed as `DuplexStream`. The public API is in
place, but native backend implementations are still under development.

Lifecycle semantics:

- `stop()`: stop playback without discarding queued frames
Expand Down
5 changes: 5 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ the output ring can hold, and `read()` returns currently available captured
frames. Blocking helpers (`write_all()` and `read_exactly()`) are Python-level
conveniences layered on top of those primitives.

Full-duplex support is modeled as a backend-level `DuplexStream`, not as a
Python wrapper around one `OutputStream` and one `InputStream`. Backends should
use a single native duplex callback where available so capture and playback share
one scheduling clock.

Stream statistics distinguish queue state from hardware behavior. `queued_frames`
and `queued_latency` describe the native ring buffer. `hardware_latency`
describes backend-reported device latency when available. `buffer_size`
Expand Down
14 changes: 13 additions & 1 deletion src/tachyaudio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@
from tachyaudio._backend import get_backend, list_devices, set_backend
from tachyaudio._device import DeviceInfo, DeviceKind
from tachyaudio._errors import BackendUnavailable, StreamClosed, TachyAudioError
from tachyaudio._stream import InputStream, OutputStream, StreamConfig, StreamStats, play
from tachyaudio._stream import (
DuplexStream,
DuplexStreamConfig,
DuplexStreamStats,
InputStream,
OutputStream,
StreamConfig,
StreamStats,
play,
)
from tachyaudio._version import __version__

__all__ = [
"BackendUnavailable",
"DeviceInfo",
"DeviceKind",
"DuplexStream",
"DuplexStreamConfig",
"DuplexStreamStats",
"InputStream",
"OutputStream",
"StreamClosed",
Expand Down
25 changes: 25 additions & 0 deletions src/tachyaudio/_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ def read(self, frame_count: int) -> memoryview: ...
def stats(self) -> object: ...


class DuplexStreamHandle(Protocol):
"""Backend-owned full-duplex stream handle."""

def start(self) -> None: ...

def stop(self) -> None: ...

def flush(self) -> None: ...

def close(self) -> None: ...

def write(self, frames: Any) -> int: ...

def read(self, frame_count: int) -> memoryview: ...

def stats(self) -> object: ...


class AudioBackend(Protocol):
"""Protocol implemented by concrete audio backends."""

Expand All @@ -53,6 +71,8 @@ def open_output_stream(self, config: object) -> OutputStreamHandle: ...

def open_input_stream(self, config: object) -> InputStreamHandle: ...

def open_duplex_stream(self, config: object) -> DuplexStreamHandle: ...


class _UnavailableBackend:
name = "unavailable"
Expand All @@ -70,6 +90,11 @@ def open_input_stream(self, config: object) -> InputStreamHandle:
"no tachyaudio backend is available yet; install or build the native backend"
)

def open_duplex_stream(self, config: object) -> DuplexStreamHandle:
raise BackendUnavailable(
"no tachyaudio backend is available yet; install or build the native backend"
)


def _load_default_backend() -> AudioBackend:
try:
Expand Down
7 changes: 7 additions & 0 deletions src/tachyaudio/_native_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

from __future__ import annotations

from typing import NoReturn

from tachyaudio import _native
from tachyaudio._device import DeviceInfo, DeviceKind
from tachyaudio._errors import BackendUnavailable


class NativeOutputStream:
Expand Down Expand Up @@ -121,3 +124,7 @@ def open_output_stream(self, config: object) -> NativeOutputStream:

def open_input_stream(self, config: object) -> NativeInputStream:
return NativeInputStream(config)

def open_duplex_stream(self, config: object) -> NoReturn:
del config
raise BackendUnavailable("native duplex streams are not implemented yet")
Loading
Loading