tachyaudio 0.2.0b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tachyaudio-0.2.0b1/.github/workflows/tests.yml +59 -0
- tachyaudio-0.2.0b1/.github/workflows/wheels.yml +54 -0
- tachyaudio-0.2.0b1/CHANGELOG.md +88 -0
- tachyaudio-0.2.0b1/CONTRIBUTING.md +47 -0
- tachyaudio-0.2.0b1/LICENSE +22 -0
- tachyaudio-0.2.0b1/MANIFEST.in +14 -0
- tachyaudio-0.2.0b1/PKG-INFO +144 -0
- tachyaudio-0.2.0b1/README.md +118 -0
- tachyaudio-0.2.0b1/docs/architecture.md +100 -0
- tachyaudio-0.2.0b1/docs/python-support.md +36 -0
- tachyaudio-0.2.0b1/docs/release.md +51 -0
- tachyaudio-0.2.0b1/examples/capture_level.py +55 -0
- tachyaudio-0.2.0b1/examples/play_tone.py +65 -0
- tachyaudio-0.2.0b1/examples/play_wav.py +377 -0
- tachyaudio-0.2.0b1/pyproject.toml +62 -0
- tachyaudio-0.2.0b1/setup.cfg +4 -0
- tachyaudio-0.2.0b1/setup.py +29 -0
- tachyaudio-0.2.0b1/src/tachyaudio/__init__.py +36 -0
- tachyaudio-0.2.0b1/src/tachyaudio/_backend.py +131 -0
- tachyaudio-0.2.0b1/src/tachyaudio/_device.py +37 -0
- tachyaudio-0.2.0b1/src/tachyaudio/_errors.py +14 -0
- tachyaudio-0.2.0b1/src/tachyaudio/_native.c +3432 -0
- tachyaudio-0.2.0b1/src/tachyaudio/_native_backend.py +165 -0
- tachyaudio-0.2.0b1/src/tachyaudio/_stream.py +548 -0
- tachyaudio-0.2.0b1/src/tachyaudio/_version.py +1 -0
- tachyaudio-0.2.0b1/src/tachyaudio/py.typed +1 -0
- tachyaudio-0.2.0b1/src/tachyaudio/vendor/README.md +5 -0
- tachyaudio-0.2.0b1/src/tachyaudio/vendor/miniaudio.LICENSE +47 -0
- tachyaudio-0.2.0b1/src/tachyaudio/vendor/miniaudio.h +95864 -0
- tachyaudio-0.2.0b1/src/tachyaudio.egg-info/PKG-INFO +144 -0
- tachyaudio-0.2.0b1/src/tachyaudio.egg-info/SOURCES.txt +33 -0
- tachyaudio-0.2.0b1/src/tachyaudio.egg-info/dependency_links.txt +1 -0
- tachyaudio-0.2.0b1/src/tachyaudio.egg-info/requires.txt +5 -0
- tachyaudio-0.2.0b1/src/tachyaudio.egg-info/top_level.txt +1 -0
- tachyaudio-0.2.0b1/tests/test_public_api.py +633 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
env:
|
|
10
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
linux:
|
|
14
|
+
name: Linux Python ${{ matrix.python-version }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
|
|
28
|
+
- name: Install package
|
|
29
|
+
run: python -m pip install .
|
|
30
|
+
|
|
31
|
+
- name: Run tests
|
|
32
|
+
run: python -m unittest discover -s tests
|
|
33
|
+
|
|
34
|
+
- name: Import smoke test
|
|
35
|
+
run: python -c "import tachyaudio as ta; assert ta.get_backend().name == 'miniaudio'; print(len(ta.list_devices()))"
|
|
36
|
+
|
|
37
|
+
macos:
|
|
38
|
+
name: macOS Python ${{ matrix.python-version }}
|
|
39
|
+
runs-on: macos-14
|
|
40
|
+
strategy:
|
|
41
|
+
fail-fast: false
|
|
42
|
+
matrix:
|
|
43
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
44
|
+
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
|
|
48
|
+
- uses: actions/setup-python@v5
|
|
49
|
+
with:
|
|
50
|
+
python-version: ${{ matrix.python-version }}
|
|
51
|
+
|
|
52
|
+
- name: Install package
|
|
53
|
+
run: python -m pip install .
|
|
54
|
+
|
|
55
|
+
- name: Run tests
|
|
56
|
+
run: python -m unittest discover -s tests
|
|
57
|
+
|
|
58
|
+
- name: Import smoke test
|
|
59
|
+
run: python -c "import tachyaudio as ta; print(ta.get_backend().name); print(len(ta.list_devices()))"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: wheels
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
env:
|
|
9
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
linux:
|
|
13
|
+
name: Build Linux wheels
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
|
|
23
|
+
- name: Install cibuildwheel
|
|
24
|
+
run: python -m pip install cibuildwheel
|
|
25
|
+
|
|
26
|
+
- name: Build wheels
|
|
27
|
+
run: python -m cibuildwheel --output-dir wheelhouse
|
|
28
|
+
|
|
29
|
+
- uses: actions/upload-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: linux-wheels
|
|
32
|
+
path: wheelhouse/*.whl
|
|
33
|
+
|
|
34
|
+
macos:
|
|
35
|
+
name: Build macOS wheels
|
|
36
|
+
runs-on: macos-14
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: "3.12"
|
|
44
|
+
|
|
45
|
+
- name: Install cibuildwheel
|
|
46
|
+
run: python -m pip install cibuildwheel
|
|
47
|
+
|
|
48
|
+
- name: Build wheels
|
|
49
|
+
run: python -m cibuildwheel --output-dir wheelhouse
|
|
50
|
+
|
|
51
|
+
- uses: actions/upload-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: macos-wheels
|
|
54
|
+
path: wheelhouse/*.whl
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `tachyaudio` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The project follows semantic versioning once releases begin. During `0.x`,
|
|
6
|
+
public APIs may still change while the backend design stabilizes.
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.0b1] - 2026-06-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added the public `DuplexStream` API contract for backend-level full-duplex
|
|
15
|
+
capture and playback.
|
|
16
|
+
- Added native macOS `DuplexStream` support backed by Core Audio input and
|
|
17
|
+
output queues owned by one native stream object.
|
|
18
|
+
- Added native Linux `DuplexStream` support backed by one miniaudio duplex
|
|
19
|
+
device callback.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Promoted package metadata from pre-alpha to beta.
|
|
24
|
+
- Kept wheel builds intentionally per-CPython-version rather than stable ABI.
|
|
25
|
+
|
|
26
|
+
## [0.2.0a3] - 2026-06-12
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Refactored native ring-buffer and stream-stat helpers without changing public
|
|
31
|
+
behavior.
|
|
32
|
+
- Added `hardware_latency` to stream statistics. macOS reports Core Audio
|
|
33
|
+
device latency plus safety offset; Linux reports `None` until a reliable
|
|
34
|
+
miniaudio latency value is available.
|
|
35
|
+
- Added blocking convenience helpers `OutputStream.write_all()` and
|
|
36
|
+
`InputStream.read_exactly()`.
|
|
37
|
+
|
|
38
|
+
## [0.2.0a2] - 2026-06-10
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Linux device enumeration now returns no devices instead of raising when
|
|
43
|
+
miniaudio cannot initialize or enumerate an audio backend in headless
|
|
44
|
+
containers.
|
|
45
|
+
|
|
46
|
+
## [0.2.0a1] - 2026-06-10
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- Linux native backend using vendored `miniaudio`.
|
|
51
|
+
- Linux device enumeration through miniaudio.
|
|
52
|
+
- Linux native output playback with a ring-buffer-backed `OutputStream`.
|
|
53
|
+
- Linux native input capture with nonblocking `InputStream.read()`.
|
|
54
|
+
- Linux CI coverage for Python 3.10 through 3.14.
|
|
55
|
+
- Vendored miniaudio license attribution.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- Linux backend initialization prefers PulseAudio, then falls back to ALSA.
|
|
60
|
+
|
|
61
|
+
### Known Limitations
|
|
62
|
+
|
|
63
|
+
- Linux support is new and has only been validated on a small set of Ubuntu
|
|
64
|
+
audio configurations.
|
|
65
|
+
- Windows backend support is not implemented yet.
|
|
66
|
+
- The API is pre-alpha and may change before a stable release.
|
|
67
|
+
|
|
68
|
+
## [0.1.0] - 2026-06-10
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- Initial Python package scaffold.
|
|
73
|
+
- macOS Core Audio device enumeration.
|
|
74
|
+
- macOS native output playback with a ring-buffer-backed `OutputStream`.
|
|
75
|
+
- Finite playback helper via `tachyaudio.play()`.
|
|
76
|
+
- Output stream lifecycle methods: `start()`, `stop()`, `drain()`, `flush()`,
|
|
77
|
+
and `close()`.
|
|
78
|
+
- macOS native input capture with nonblocking `InputStream.read()`.
|
|
79
|
+
- Stream statistics for processed frames, underruns, overruns, queued frames,
|
|
80
|
+
queued latency, and buffer size.
|
|
81
|
+
- Playback and capture examples.
|
|
82
|
+
- macOS CI and wheel workflow scaffolding for Python 3.10 through 3.14.
|
|
83
|
+
|
|
84
|
+
### Known Limitations
|
|
85
|
+
|
|
86
|
+
- Native backend support is currently macOS-only.
|
|
87
|
+
- Windows and Linux backends are not implemented yet.
|
|
88
|
+
- The API is pre-alpha and may change before the first PyPI release.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
`tachyaudio` is pre-alpha. The current implementation is intentionally narrow:
|
|
4
|
+
macOS Core Audio device enumeration, output playback, and input capture.
|
|
5
|
+
|
|
6
|
+
## Development setup
|
|
7
|
+
|
|
8
|
+
Use Python 3.10 through 3.14.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python -m pip install -e .
|
|
12
|
+
python -m unittest discover -s tests
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
On macOS, restricted sandboxes may hide Core Audio devices. Validate audio
|
|
16
|
+
examples from an unsandboxed terminal before treating empty device lists as bugs.
|
|
17
|
+
|
|
18
|
+
## Manual smoke tests
|
|
19
|
+
|
|
20
|
+
Playback:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
PYTHONPATH=src python examples/play_tone.py
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Capture:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
PYTHONPATH=src python examples/capture_level.py
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Contribution rules
|
|
33
|
+
|
|
34
|
+
- Keep the public Python API compatible with Python 3.10.
|
|
35
|
+
- Keep native audio callbacks free of Python calls.
|
|
36
|
+
- Add or update tests for behavior changes.
|
|
37
|
+
- Keep platform-specific backend behavior behind the backend protocol.
|
|
38
|
+
- Avoid new runtime dependencies unless they materially improve correctness or
|
|
39
|
+
packaging.
|
|
40
|
+
|
|
41
|
+
## Pull request checklist
|
|
42
|
+
|
|
43
|
+
- Unit tests pass locally.
|
|
44
|
+
- Relevant examples still run when the change affects audio behavior.
|
|
45
|
+
- README/docs are updated for public API changes.
|
|
46
|
+
- Native code handles allocation and shutdown failures explicitly.
|
|
47
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tachyaudio contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include CONTRIBUTING.md
|
|
5
|
+
include pyproject.toml
|
|
6
|
+
include setup.py
|
|
7
|
+
recursive-include src/tachyaudio *.c *.h *.LICENSE *.md *.py py.typed
|
|
8
|
+
recursive-include docs *.md
|
|
9
|
+
recursive-include examples *.py
|
|
10
|
+
recursive-include tests *.py
|
|
11
|
+
recursive-include .github/workflows *.yml
|
|
12
|
+
global-exclude __pycache__/*
|
|
13
|
+
global-exclude *.py[cod]
|
|
14
|
+
global-exclude .DS_Store
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tachyaudio
|
|
3
|
+
Version: 0.2.0b1
|
|
4
|
+
Summary: Low-level audio primitives for tachypy and Python experiments.
|
|
5
|
+
Author: tachyaudio contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# tachyaudio
|
|
28
|
+
|
|
29
|
+
`tachyaudio` is a low-level audio package intended to replace tachypy’s direct
|
|
30
|
+
dependency on `sounddevice`/PortAudio over time.
|
|
31
|
+
|
|
32
|
+
Status: beta. The native backend currently supports macOS through Core Audio and
|
|
33
|
+
Linux through vendored `miniaudio`. Windows support is planned but intentionally
|
|
34
|
+
deferred until Windows test hardware is available.
|
|
35
|
+
|
|
36
|
+
## Goals
|
|
37
|
+
|
|
38
|
+
- install from `pip` without requiring users to install PortAudio separately
|
|
39
|
+
- keep real-time audio work out of Python callbacks where possible
|
|
40
|
+
- expose explicit latency, underrun, and overrun diagnostics
|
|
41
|
+
- support playback, capture, duplex streams, and device enumeration
|
|
42
|
+
- keep tachypy’s public API stable while the backend evolves
|
|
43
|
+
|
|
44
|
+
## Non-goals
|
|
45
|
+
|
|
46
|
+
- full DAW-style audio graph support in the initial release
|
|
47
|
+
- replacing OS audio stacks such as Core Audio, WASAPI, ALSA, or PulseAudio
|
|
48
|
+
- exposing backend-specific details as the primary user API
|
|
49
|
+
|
|
50
|
+
## Initial API shape
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import tachyaudio as ta
|
|
54
|
+
|
|
55
|
+
devices = ta.list_devices()
|
|
56
|
+
|
|
57
|
+
stream = ta.OutputStream(sample_rate=48_000, channels=2)
|
|
58
|
+
stream.write(samples)
|
|
59
|
+
with stream:
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
stats = ta.play(samples, sample_rate=48_000, channels=2)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The native backend currently supports device enumeration, float32 output
|
|
66
|
+
playback, and nonblocking float32 input capture on macOS and Linux.
|
|
67
|
+
|
|
68
|
+
`OutputStream` is currently a continuous stream. Write audio before or during
|
|
69
|
+
playback; if the stream runs out of queued frames it outputs silence and counts
|
|
70
|
+
an underrun.
|
|
71
|
+
|
|
72
|
+
Use `tachyaudio.play()` for finite stimuli that should start, drain, and close
|
|
73
|
+
as a single operation.
|
|
74
|
+
|
|
75
|
+
See `examples/play_wav.py` for dependency-free playback of WAV files exported
|
|
76
|
+
from an editor such as Audacity. The repository example uses a 48 kHz float WAV
|
|
77
|
+
to avoid runtime resampling. Pass `--eq` to show a lightweight five-band
|
|
78
|
+
terminal meter during playback.
|
|
79
|
+
|
|
80
|
+
Use blocking helpers when callers need complete buffer transfer:
|
|
81
|
+
|
|
82
|
+
- `OutputStream.write_all(frames, timeout=None)`: wait until all frames are
|
|
83
|
+
accepted by the output ring
|
|
84
|
+
- `InputStream.read_exactly(frame_count, timeout=None)`: wait until exactly the
|
|
85
|
+
requested number of frames has been captured
|
|
86
|
+
|
|
87
|
+
Full-duplex capture/playback is exposed as `DuplexStream`. Native macOS and
|
|
88
|
+
Linux support is available.
|
|
89
|
+
|
|
90
|
+
Lifecycle semantics:
|
|
91
|
+
|
|
92
|
+
- `stop()`: stop playback without discarding queued frames
|
|
93
|
+
- `drain()`: wait for queued frames to play
|
|
94
|
+
- `flush()`: discard queued frames without closing the stream
|
|
95
|
+
- `close()`: stop playback and release native resources
|
|
96
|
+
|
|
97
|
+
`StreamStats` reports:
|
|
98
|
+
|
|
99
|
+
- `frames_processed`: frames consumed by the backend
|
|
100
|
+
- `underruns` / `overruns`: buffer starvation or rejected writes
|
|
101
|
+
- `hardware_latency`: backend-reported device latency in seconds when available
|
|
102
|
+
- `queued_frames`: frames currently waiting in the native ring
|
|
103
|
+
- `queued_latency`: queued ring duration in seconds
|
|
104
|
+
- `estimated_latency`: `queued_latency + hardware_latency` when hardware latency
|
|
105
|
+
is available, otherwise queued latency
|
|
106
|
+
- `buffer_size`: native callback buffer size in frames
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
Supported Python versions:
|
|
111
|
+
|
|
112
|
+
- Python 3.10
|
|
113
|
+
- Python 3.11
|
|
114
|
+
- Python 3.12
|
|
115
|
+
- Python 3.13
|
|
116
|
+
- Python 3.14
|
|
117
|
+
|
|
118
|
+
Run the standard-library test suite:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python3 -m unittest discover -s tests
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Play a short test tone:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
PYTHONPATH=src python3 examples/play_tone.py
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Capture a short input buffer and print its RMS level:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
PYTHONPATH=src python3 examples/capture_level.py
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`InputStream.read(frame_count)` is nonblocking and returns currently available
|
|
137
|
+
frames up to `frame_count`.
|
|
138
|
+
|
|
139
|
+
On macOS, a restricted sandbox may hide Core Audio devices. On Linux, sandboxed
|
|
140
|
+
processes may be unable to reach the user PipeWire/PulseAudio server. If
|
|
141
|
+
`tachyaudio.list_devices()` returns an empty tuple or only generic ALSA devices
|
|
142
|
+
in a sandboxed environment, verify from an unsandboxed terminal before debugging
|
|
143
|
+
the backend. Headless Linux containers may return no devices while still being
|
|
144
|
+
able to build and import the native extension.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# tachyaudio
|
|
2
|
+
|
|
3
|
+
`tachyaudio` is a low-level audio package intended to replace tachypy’s direct
|
|
4
|
+
dependency on `sounddevice`/PortAudio over time.
|
|
5
|
+
|
|
6
|
+
Status: beta. The native backend currently supports macOS through Core Audio and
|
|
7
|
+
Linux through vendored `miniaudio`. Windows support is planned but intentionally
|
|
8
|
+
deferred until Windows test hardware is available.
|
|
9
|
+
|
|
10
|
+
## Goals
|
|
11
|
+
|
|
12
|
+
- install from `pip` without requiring users to install PortAudio separately
|
|
13
|
+
- keep real-time audio work out of Python callbacks where possible
|
|
14
|
+
- expose explicit latency, underrun, and overrun diagnostics
|
|
15
|
+
- support playback, capture, duplex streams, and device enumeration
|
|
16
|
+
- keep tachypy’s public API stable while the backend evolves
|
|
17
|
+
|
|
18
|
+
## Non-goals
|
|
19
|
+
|
|
20
|
+
- full DAW-style audio graph support in the initial release
|
|
21
|
+
- replacing OS audio stacks such as Core Audio, WASAPI, ALSA, or PulseAudio
|
|
22
|
+
- exposing backend-specific details as the primary user API
|
|
23
|
+
|
|
24
|
+
## Initial API shape
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import tachyaudio as ta
|
|
28
|
+
|
|
29
|
+
devices = ta.list_devices()
|
|
30
|
+
|
|
31
|
+
stream = ta.OutputStream(sample_rate=48_000, channels=2)
|
|
32
|
+
stream.write(samples)
|
|
33
|
+
with stream:
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
stats = ta.play(samples, sample_rate=48_000, channels=2)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The native backend currently supports device enumeration, float32 output
|
|
40
|
+
playback, and nonblocking float32 input capture on macOS and Linux.
|
|
41
|
+
|
|
42
|
+
`OutputStream` is currently a continuous stream. Write audio before or during
|
|
43
|
+
playback; if the stream runs out of queued frames it outputs silence and counts
|
|
44
|
+
an underrun.
|
|
45
|
+
|
|
46
|
+
Use `tachyaudio.play()` for finite stimuli that should start, drain, and close
|
|
47
|
+
as a single operation.
|
|
48
|
+
|
|
49
|
+
See `examples/play_wav.py` for dependency-free playback of WAV files exported
|
|
50
|
+
from an editor such as Audacity. The repository example uses a 48 kHz float WAV
|
|
51
|
+
to avoid runtime resampling. Pass `--eq` to show a lightweight five-band
|
|
52
|
+
terminal meter during playback.
|
|
53
|
+
|
|
54
|
+
Use blocking helpers when callers need complete buffer transfer:
|
|
55
|
+
|
|
56
|
+
- `OutputStream.write_all(frames, timeout=None)`: wait until all frames are
|
|
57
|
+
accepted by the output ring
|
|
58
|
+
- `InputStream.read_exactly(frame_count, timeout=None)`: wait until exactly the
|
|
59
|
+
requested number of frames has been captured
|
|
60
|
+
|
|
61
|
+
Full-duplex capture/playback is exposed as `DuplexStream`. Native macOS and
|
|
62
|
+
Linux support is available.
|
|
63
|
+
|
|
64
|
+
Lifecycle semantics:
|
|
65
|
+
|
|
66
|
+
- `stop()`: stop playback without discarding queued frames
|
|
67
|
+
- `drain()`: wait for queued frames to play
|
|
68
|
+
- `flush()`: discard queued frames without closing the stream
|
|
69
|
+
- `close()`: stop playback and release native resources
|
|
70
|
+
|
|
71
|
+
`StreamStats` reports:
|
|
72
|
+
|
|
73
|
+
- `frames_processed`: frames consumed by the backend
|
|
74
|
+
- `underruns` / `overruns`: buffer starvation or rejected writes
|
|
75
|
+
- `hardware_latency`: backend-reported device latency in seconds when available
|
|
76
|
+
- `queued_frames`: frames currently waiting in the native ring
|
|
77
|
+
- `queued_latency`: queued ring duration in seconds
|
|
78
|
+
- `estimated_latency`: `queued_latency + hardware_latency` when hardware latency
|
|
79
|
+
is available, otherwise queued latency
|
|
80
|
+
- `buffer_size`: native callback buffer size in frames
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
Supported Python versions:
|
|
85
|
+
|
|
86
|
+
- Python 3.10
|
|
87
|
+
- Python 3.11
|
|
88
|
+
- Python 3.12
|
|
89
|
+
- Python 3.13
|
|
90
|
+
- Python 3.14
|
|
91
|
+
|
|
92
|
+
Run the standard-library test suite:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python3 -m unittest discover -s tests
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Play a short test tone:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
PYTHONPATH=src python3 examples/play_tone.py
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Capture a short input buffer and print its RMS level:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
PYTHONPATH=src python3 examples/capture_level.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`InputStream.read(frame_count)` is nonblocking and returns currently available
|
|
111
|
+
frames up to `frame_count`.
|
|
112
|
+
|
|
113
|
+
On macOS, a restricted sandbox may hide Core Audio devices. On Linux, sandboxed
|
|
114
|
+
processes may be unable to reach the user PipeWire/PulseAudio server. If
|
|
115
|
+
`tachyaudio.list_devices()` returns an empty tuple or only generic ALSA devices
|
|
116
|
+
in a sandboxed environment, verify from an unsandboxed terminal before debugging
|
|
117
|
+
the backend. Headless Linux containers may return no devices while still being
|
|
118
|
+
able to build and import the native extension.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# tachyaudio architecture
|
|
2
|
+
|
|
3
|
+
## Package boundary
|
|
4
|
+
|
|
5
|
+
`tachyaudio` owns low-level audio primitives. `tachypy` should not depend on
|
|
6
|
+
backend-specific concepts directly. Instead, tachypy should wrap `tachyaudio`
|
|
7
|
+
from `tachypy.audio` and provide experiment-oriented scheduling, stimulus
|
|
8
|
+
construction, and compatibility behavior.
|
|
9
|
+
|
|
10
|
+
## Backend boundary
|
|
11
|
+
|
|
12
|
+
The public Python API delegates to an internal backend protocol:
|
|
13
|
+
|
|
14
|
+
- `list_devices()`
|
|
15
|
+
- `open_output_stream(config)`
|
|
16
|
+
- `open_input_stream(config)`
|
|
17
|
+
|
|
18
|
+
The first production backend should be native. The current first slices expose
|
|
19
|
+
macOS Core Audio device enumeration and output playback directly so the build
|
|
20
|
+
and Python boundary can be validated. Playback uses preallocated Core Audio
|
|
21
|
+
queue buffers fed by a native ring buffer. A future cross-platform
|
|
22
|
+
implementation can move to vendored `miniaudio` once the desired callback,
|
|
23
|
+
buffering, and drain semantics are clearer.
|
|
24
|
+
|
|
25
|
+
Finite playback uses an explicit drain mode: partial final buffers are padded
|
|
26
|
+
with silence without counting as underruns, and callbacks stop refilling once
|
|
27
|
+
queued audio has drained.
|
|
28
|
+
|
|
29
|
+
Stream lifecycle methods have distinct responsibilities: `stop()` halts playback,
|
|
30
|
+
`drain()` waits for queued audio to finish, `flush()` discards queued ring-buffer
|
|
31
|
+
audio, and `close()` releases native resources.
|
|
32
|
+
|
|
33
|
+
Input capture mirrors output buffering in reverse: Core Audio callbacks write
|
|
34
|
+
captured float32 frames into a native ring buffer, and Python reads available
|
|
35
|
+
frames without invoking Python from the audio callback.
|
|
36
|
+
|
|
37
|
+
The core stream methods remain nonblocking: `write()` accepts as many frames as
|
|
38
|
+
the output ring can hold, and `read()` returns currently available captured
|
|
39
|
+
frames. Blocking helpers (`write_all()` and `read_exactly()`) are Python-level
|
|
40
|
+
conveniences layered on top of those primitives.
|
|
41
|
+
|
|
42
|
+
Full-duplex support is modeled as a backend-level `DuplexStream`, not as a
|
|
43
|
+
Python wrapper around one `OutputStream` and one `InputStream`. Backends should
|
|
44
|
+
use a single native duplex callback where available so capture and playback share
|
|
45
|
+
one scheduling clock. The current macOS implementation owns both native Core
|
|
46
|
+
Audio queues inside one native stream object. The Linux miniaudio implementation
|
|
47
|
+
uses one native duplex device callback.
|
|
48
|
+
|
|
49
|
+
Stream statistics distinguish queue state from hardware behavior. `queued_frames`
|
|
50
|
+
and `queued_latency` describe the native ring buffer. `hardware_latency`
|
|
51
|
+
describes backend-reported device latency when available. `buffer_size`
|
|
52
|
+
describes the callback buffer size in frames. `estimated_latency` reports
|
|
53
|
+
`queued_latency + hardware_latency` when hardware latency is available, otherwise
|
|
54
|
+
queued latency.
|
|
55
|
+
|
|
56
|
+
## Real-time constraints
|
|
57
|
+
|
|
58
|
+
Audio callbacks must not call Python. Native backend code should own:
|
|
59
|
+
|
|
60
|
+
- device callback execution
|
|
61
|
+
- ring-buffer reads and writes
|
|
62
|
+
- underrun and overrun counters
|
|
63
|
+
- format conversion when needed
|
|
64
|
+
- stream start/stop synchronization
|
|
65
|
+
|
|
66
|
+
Python should submit buffers, inspect diagnostics, and control stream lifecycle.
|
|
67
|
+
|
|
68
|
+
## Packaging strategy
|
|
69
|
+
|
|
70
|
+
Support CPython 3.10 through 3.14. Keep the Python layer syntax-compatible with
|
|
71
|
+
3.10, and build wheels for each supported CPython minor version because native
|
|
72
|
+
extension modules are ABI-specific unless we intentionally move to the stable
|
|
73
|
+
ABI.
|
|
74
|
+
|
|
75
|
+
Ship wheels for supported platforms:
|
|
76
|
+
|
|
77
|
+
- macOS arm64/x86_64 using Core Audio
|
|
78
|
+
- Windows x86_64/arm64 using WASAPI
|
|
79
|
+
- Linux x86_64/aarch64 using the best available runtime backend
|
|
80
|
+
|
|
81
|
+
Source builds should remain possible, but routine users should not need to
|
|
82
|
+
install PortAudio or a compiler.
|
|
83
|
+
|
|
84
|
+
## Native backend milestone
|
|
85
|
+
|
|
86
|
+
The minimum useful native backend is:
|
|
87
|
+
|
|
88
|
+
1. device enumeration
|
|
89
|
+
2. output stream
|
|
90
|
+
3. input stream
|
|
91
|
+
4. frame counters
|
|
92
|
+
5. underrun/overrun counters
|
|
93
|
+
6. explicit latency configuration
|
|
94
|
+
7. wheel builds in CI
|
|
95
|
+
|
|
96
|
+
## macOS sandbox note
|
|
97
|
+
|
|
98
|
+
Core Audio device enumeration can return zero devices when run from a restricted
|
|
99
|
+
sandbox, even when the host Mac has audio hardware. Validate native enumeration
|
|
100
|
+
from an unsandboxed process before treating an empty list as a backend defect.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python support policy
|
|
2
|
+
|
|
3
|
+
`tachyaudio` targets CPython 3.10 through 3.14.
|
|
4
|
+
|
|
5
|
+
## Compatibility rules
|
|
6
|
+
|
|
7
|
+
- Keep public Python code syntax-compatible with Python 3.10.
|
|
8
|
+
- Avoid runtime dependencies unless they materially improve packaging or audio
|
|
9
|
+
correctness.
|
|
10
|
+
- Build and test native wheels for every supported CPython minor version.
|
|
11
|
+
- Treat Python 3.10 as the lower-bound type-checking target.
|
|
12
|
+
- Add version-specific code only behind narrow compatibility helpers.
|
|
13
|
+
|
|
14
|
+
## Wheel targets and ABI decision
|
|
15
|
+
|
|
16
|
+
Initial wheel scope:
|
|
17
|
+
|
|
18
|
+
- `cp310`
|
|
19
|
+
- `cp311`
|
|
20
|
+
- `cp312`
|
|
21
|
+
- `cp313`
|
|
22
|
+
- `cp314`
|
|
23
|
+
|
|
24
|
+
The native extension currently uses the regular CPython C API. That means wheel
|
|
25
|
+
builds are per-CPython-version. Moving to Python’s stable ABI can be evaluated
|
|
26
|
+
later, but only after the native API settles.
|
|
27
|
+
|
|
28
|
+
For the public beta, `tachyaudio` intentionally ships per-version CPython
|
|
29
|
+
wheels (`cp310` through `cp314`) rather than `abi3` wheels. This keeps the
|
|
30
|
+
native backend implementation unconstrained while macOS and Linux playback,
|
|
31
|
+
capture, duplex streams, and diagnostics are still evolving. The additional
|
|
32
|
+
wheel matrix is acceptable because cibuildwheel already builds the supported
|
|
33
|
+
Python minors explicitly.
|
|
34
|
+
|
|
35
|
+
CI should run unit tests on every supported Python minor version before a wheel
|
|
36
|
+
is published.
|