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
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ delivery, and one recording file per stem.

```text
desktop application ─┐
microphone ──────────┼─ native Session ─┬─ Python model code
generated PCM ─────── ├─ Relay and a browser
└─ separate recording stems
system audio ────────┼─ native Session ─┬─ Python model code
microphone ──────────┤ ├─ Relay and a browser
generated PCM ───────┘ └─ separate recording stems
```

PocketStation runs capture, frame timing, routing, recording, and Relay
Expand Down Expand Up @@ -44,6 +44,26 @@ match.
The context manager starts one native Session and joins it when the block
exits. No microphone opens and no file is written unless you request them.

## Capture the complete output mix

Use a Session directly when the workflow intentionally needs every sound
playing through the computer:

```python
import pocketstation as pks

session = pks.Session()
system_audio = session.capture(pks.Source.system_audio())
system_audio.send(session.polled_audio())

with session.start() as running:
for frame in running.audio:
print(frame.source_id, frame.stem_id)
```

Use `Source.application(...)` instead when unrelated desktop audio must remain
outside the Session.

## Add a microphone or recording

```python
Expand Down Expand Up @@ -307,7 +327,7 @@ Rust code.
| Windows x86-64 and ARM64 | published wheels; Core selection and 10 ms capture in a Windows 11 ARM64 VM; physical-device and latency qualification remain separate |
| WAN and TURN | not yet qualified |

Version 0.1.3 uses PocketStation Core 1.1.7 and the shared Relay Connector
Version 0.1.4 uses PocketStation Core 1.1.9 and the shared Relay Connector
0.1.5.

Reading native audio into Python copies samples into Python-owned bytes before
Expand Down
22 changes: 22 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

## Unreleased

## 0.1.4 — 2026-09-04

Capture the complete desktop output mix with the same Python Session used for
selected applications and microphones:

```python
session = pocketstation.Session()
system_audio = session.capture(pocketstation.Source.system_audio())
system_audio.send(session.polled_audio())
```

Use `Source.application(...)` when the user selected one application. Use
`Source.system_audio()` when the workflow intentionally needs every sound
playing through the computer. Both preserve source and stem identity for
iteration, Connectors, Relay, and recording.

This release uses PocketStation Core 1.1.9 and Relay Connector 0.1.5.

```console
python -m pip install --upgrade pocketstation==0.1.4
```

## 0.1.3 — 2026-09-03

Configure route delivery with one consistent set of names across Python and
Expand Down
22 changes: 22 additions & 0 deletions docs/getting-started/capture.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ exits. The iterator receives audio through a native queue that holds 32 frames
by default. If Python stops reading and the queue fills, PocketStation drops
new frames and reports the loss.

## Capture all desktop output

Use `Source.system_audio()` when the workflow needs every sound playing
through the computer. This is an explicit choice because notifications, music,
and unrelated applications can be included.

```python
import pocketstation as pks

session = pks.Session()
desktop = session.capture(pks.Source.system_audio())
desktop.send(session.polled_audio())

with session.start() as running:
for frame in running.audio:
print(frame.source_id, frame.stem_id)
```

Use `Source.application("Zoom")` when the user selected one application and
other desktop audio must remain private. Do not open both sources unless the
application needs both; their audio can overlap.

## Add a microphone or recording

```python
Expand Down
16 changes: 10 additions & 6 deletions docs/operations/platform-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ follow the host operating system.

## Supported Python

PocketStation 0.1.2 supports CPython 3.11 and newer through one ABI3 extension
PocketStation 0.1.4 supports CPython 3.11 and newer through one ABI3 extension
per operating system and architecture. Install the wheel that matches the host;
do not rely on a sibling Rust checkout.

Expand Down Expand Up @@ -96,6 +96,8 @@ Choose fallback behavior explicitly:
## macOS

Application capture needs screen and system-audio recording permission.
System-audio capture uses the same operating-system recording permission and
includes the complete output mix.
Microphone capture needs microphone permission. Restart the application after
changing consent when macOS does not update the running process.

Expand All @@ -105,15 +107,17 @@ microphone, the 10 ms voice profile, Relay, Chromium, and three recordings.
## Windows

The release workflow builds Windows x64 and ARM64 wheels. Core selector and
10 ms correctness have been exercised in Windows 11 ARM64. VM scheduling is
not a physical-device latency result.
system-audio correctness at 10 ms and 20 ms have been exercised in an
interactive Windows 11 ARM64 VM. VM scheduling is not a physical-device
latency result.

## Linux

The release workflow builds manylinux x86_64 and ARM64 wheels. Application
capture requires access to the logged-in PipeWire session. Microphone capture
uses ALSA. A service or container must receive those devices and session
permissions explicitly.
and system-audio capture require access to the logged-in PipeWire session.
System audio follows the default output monitor. Microphone capture uses ALSA.
A service or container must receive those devices and session permissions
explicitly.

## Separate correctness from performance

Expand Down
1 change: 1 addition & 0 deletions docs/reference/api-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ provider, Relay, and diagnostic APIs from the module that implements them.
| Task | API |
|---|---|
| Capture one application | `pocketstation.capture` |
| Capture all desktop output | `Session.capture(Source.system_audio())` |
| Declare a synchronous Session | `pocketstation.Session` |
| Declare an asyncio Session | `pocketstation.aio.Session` |
| Select a source | `pocketstation.Source` |
Expand Down
14 changes: 7 additions & 7 deletions native/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions native/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pocketstation-python"
version = "0.1.3"
version = "0.1.4"
edition = "2021"
publish = false
description = "Native PocketStation runtime bindings for the Python SDK"
Expand All @@ -16,10 +16,10 @@ default = []
conformance-fixtures = ["pocketstation/conformance-fixtures"]

[dependencies]
pocketstation = "=1.1.7"
pocketstation = "=1.1.9"
pocketstation-relay = "=0.1.5"
pyo3 = { version = "0.27", features = ["abi3-py311"] }

[dev-dependencies]
pocketstation = { version = "=1.1.7", features = ["conformance-fixtures"] }
pocketstation = { version = "=1.1.9", features = ["conformance-fixtures"] }
tempfile = "3"
9 changes: 9 additions & 0 deletions native/src/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub(crate) enum SourceDeclaration {
platform: Platform,
stable_key: String,
},
SystemAudio,
MicrophoneDefault,
MicrophoneId(String),
}
Expand Down Expand Up @@ -57,6 +58,7 @@ impl SourceDeclaration {
ProcessId::new(*process_id),
StableSourceId::new(*platform, SourceKind::Application, stable_key.clone()),
)),
Self::SystemAudio => Source::system_audio(),
Self::MicrophoneDefault => Source::microphone_default(),
Self::MicrophoneId(device_id) => {
Source::microphone(DeviceSelector::id(DeviceId::new(device_id.clone())))
Expand Down Expand Up @@ -290,6 +292,13 @@ impl PythonSource {
})
}

#[staticmethod]
const fn system_audio() -> Self {
Self {
declaration: SourceDeclaration::SystemAudio,
}
}

#[staticmethod]
const fn microphone_default() -> Self {
Self {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "pocketstation"
version = "0.1.3"
version = "0.1.4"
description = "Source-aware live audio capture, processing, and routing for Python"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion python/pocketstation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from .session import RecordingOutcome, RunningSession, Session, StopResult
from .sources import Source, discover_sources

__version__ = "0.1.3"
__version__ = "0.1.4"

__all__ = [
"RUNTIME_COMPATIBILITY",
Expand Down
2 changes: 1 addition & 1 deletion python/pocketstation/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@
SignalStream,
)

__version__ = "0.1.3"
__version__ = "0.1.4"
__all__ = [
"RUNTIME_COMPATIBILITY",
"STREAM_EOF",
Expand Down
2 changes: 2 additions & 0 deletions python/pocketstation/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ class Source:
stable_key: str,
) -> Source: ...
@staticmethod
def system_audio() -> Source: ...
@staticmethod
def microphone_default() -> Source: ...
@staticmethod
def microphone_id(device_id: str) -> Source: ...
Expand Down
4 changes: 2 additions & 2 deletions python/pocketstation/compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class RuntimeCompatibility:


RUNTIME_COMPATIBILITY = RuntimeCompatibility(
sdk_version="0.1.3",
core_version="1.1.7",
sdk_version="0.1.4",
core_version="1.1.9",
relay_connector_version="0.1.5",
python_requires=">=3.11",
python_abi="abi3-py311",
Expand Down
21 changes: 16 additions & 5 deletions python/pocketstation/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,16 @@ def application_process_instance(
ProcessInstanceSelector(process_id, stable_id),
)

@classmethod
def system_audio(cls) -> Source:
"""Capture the audio playing through the host output devices."""
native = _native_call(_NativeSource.system_audio)
return cls(
native,
SourceKind.SYSTEM_MIX,
SourceSelectorKind.SYSTEM_MIX,
)

@classmethod
def microphone_default(cls) -> Source:
"""Select the host default microphone for this Session open."""
Expand All @@ -478,8 +488,7 @@ def microphone_id(cls, device_id: str) -> Source:
def from_discovered(cls, source: DiscoveredSource) -> Source:
"""Build the strongest supported Session declaration from discovery.

Output devices and system mix remain discovery-only in the stable 1.1
Session declaration requirements.
Output devices remain discovery-only.
"""
stable_id = source.stable_id
if stable_id.kind is SourceKind.APPLICATION:
Expand All @@ -495,6 +504,8 @@ def from_discovered(cls, source: DiscoveredSource) -> Source:
)
if stable_id.kind is SourceKind.INPUT_DEVICE:
return cls.microphone_id(source.device_uid or stable_id.stable_key)
if stable_id.kind is SourceKind.SYSTEM_MIX:
return cls.system_audio()
raise PocketStationError(
"discovered "
f"{stable_id.kind.value!r} is not a frozen built-in Session Source",
Expand Down Expand Up @@ -586,9 +597,9 @@ def application_capture_available() -> bool:
def microphone_permission_observation() -> PermissionObservation:
"""Read microphone authorization without prompting.

Linux, Python hosts on Windows with Core 1.1.4, and any backend without an
authoritative query return :attr:`PermissionObservation.NOT_OBSERVABLE`;
callers must not reinterpret it as allowed or denied.
Linux, Python hosts on Windows, and any backend without an authoritative
query return :attr:`PermissionObservation.NOT_OBSERVABLE`; callers must
not reinterpret it as allowed or denied.
"""
observation = _native_call(_native_microphone_permission_observation)
return PermissionObservation(observation)
Expand Down
3 changes: 0 additions & 3 deletions tests/test_aio_streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import asyncio
import threading
from time import monotonic

import pocketstation._native as _native
import pytest
Expand Down Expand Up @@ -162,14 +161,12 @@ def operation() -> str:

task = asyncio.create_task(_native_async(operation))
assert await asyncio.to_thread(entered.wait, 1.0)
started = monotonic()
task.cancel()
asyncio.get_running_loop().call_later(0.02, release.set)

with pytest.raises(asyncio.CancelledError):
await task

assert monotonic() - started >= 0.015
assert finished.is_set()


Expand Down
1 change: 1 addition & 0 deletions tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def test_given_selector_family_when_declared_then_each_shape_is_available():
"macos",
"bundle:com.spotify.client",
)
assert Source.system_audio()
assert Source.microphone_id("device-42")


Expand Down
Loading
Loading