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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ jobs:

- name: Build wheel
working-directory: sdk-python
env:
CARGO_TARGET_DIR: native/target-release
run: maturin build --release --locked --out dist

- name: Test isolated release wheel
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ Add `microphone=True` when you need the default microphone as a second
independent stem. Add `record_to="recordings"` when you want each selected stem
recorded. Both behaviors are off by default.

## Handle permissions and source changes

PocketStation does not prompt during import or discovery. Check microphone
permission without prompting, then let Source opening report the authoritative
result. Application capture and microphone capture use separate permissions.

When an application or device disappears, PocketStation reports the change and
does not switch to another Source. Stop the current Session, discover again,
confirm any changed selection, and create a new Session. Store a discovered
identity only for its reported persistence scope. Keep fallback and provider
retry policy explicit and finite.

See [Prepare and qualify each Python platform](docs/operations/platform-support.md)
for the permission states, persistence scopes, and recovery sequence.

## Debug a voice interruption

[`examples/debug_voice_ai.py`](examples/debug_voice_ai.py) sends a physical
Expand Down Expand Up @@ -135,7 +150,7 @@ with session.start():
The input uses finite preallocated Core buffers. Writes report full, closed,
cancelled, and invalid-buffer outcomes explicitly.

## Build an integration
## Create an integration

PocketStation uses four open boundaries:

Expand Down Expand Up @@ -188,7 +203,7 @@ microphone.send_to(destination)

PocketStation calls `start()` once, interleaves both source-aware stems through
`send()`, and calls `stop()` once. A second Connector object creates a separate
destination. See [Build an integration](docs/guides/integrations.md) for
destination. See [Create an integration](docs/guides/integrations.md) for
deadlines, failures, and the advanced SPI.

Python provider callbacks execute on bounded off-realtime workers. They cannot
Expand Down Expand Up @@ -253,6 +268,8 @@ uv run mypy python tests/qualification/typing_contract.py examples
PCM input and selective output cancellation.
- [Record and observe a Session](docs/guides/record-and-observe.md) — multistem
outcomes, route metrics, and lifecycle events.
- [Prepare each platform](docs/operations/platform-support.md) — permissions,
source persistence, explicit rediscovery, and fallback policy.
- [`examples/README.md`](examples/README.md) — runnable examples and
prerequisites.
- `pocketstation.capture` — concise application and microphone capture.
Expand Down
74 changes: 31 additions & 43 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# PocketStation for Python release notes

## 0.1.1 — 2026-09-01
## 0.1.2 — 2026-09-01

Send audio to an external system with one function or one focused Python class.
Capture code can now send source-aware audio to an external system through one
function or one focused Python class.

### Added
This is the first PocketStation release published to PyPI.

Use `Connector(send=...)` when a destination is already open:
### Create a Connector

Use `Connector(send=...)` when the destination is already open:

```python
async def send_audio(frame):
Expand All @@ -16,11 +19,10 @@ destination = pocketstation.aio.Connector(send=send_audio)
application.send_to(destination)
```

Subclass `pocketstation.Connector` for finite synchronous integrations or
`pocketstation.aio.Connector` for network providers. Implement `start()`,
`send(frame)`, and `stop()`; PocketStation supplies the bounded Core routes,
source and stem identity, delivery observations, drain, abort, and joined
shutdown.
Subclass `pocketstation.Connector` for a synchronous destination or
`pocketstation.aio.Connector` for an asynchronous provider. The class owns its
provider connection. PocketStation owns the bounded routes, source and stem
identity, delivery observations, drain, abort, and joined shutdown.

```python
class WebSocketConnector(pocketstation.aio.Connector):
Expand All @@ -40,42 +42,31 @@ class WebSocketConnector(pocketstation.aio.Connector):
await self.socket.close()
```

One configured object can receive several stems through one provider
lifecycle:

```python
destination = WebSocketConnector(url, token)
application.send_to(destination)
microphone.send_to(destination)
```

Two Connector objects remain separate destinations. Async provider calls use
finite startup, delivery, and shutdown deadlines. Provider failures retain the
lifecycle stage, stable error code, and retryability when supplied.
One Connector object can receive several stems through one provider lifecycle.
Create another object when a backup or second destination needs its own queue,
failure, and shutdown outcome.

### Changed
### Operate capture safely

`Connector`, `AudioFrame`, and their asyncio Connector entry points are now
available from the concise package namespaces. The manifest, driver, worker,
configuration-schema, and explicit Endpoint contracts remain in
`pocketstation.connector` for integrations that need the advanced SPI.

The callback form also accepts `start=` and `stop=` when a small integration
needs lifecycle operations without a class. The existing
`Connector.with_driver()`, `Connector.with_worker()`,
`Connector.from_handler()`, and `Connector.from_audio_handler()` APIs remain
available without migration.
The guides now show how to inspect microphone permission without prompting,
persist a discovered source only for its reported scope, recover after a
process or device disappears, reject ambiguous application matches, and make
fallback and provider retry policy explicit.

### Upgrade

```console
python -m pip install --upgrade pocketstation==0.1.1
python -m pip install --upgrade pocketstation==0.1.2
```

Python Connectors run outside native realtime partitions, but each Python frame
delivery crosses the interpreter boundary. Use the shared native Relay
Connector or a native extension when provider-side Python execution is not
appropriate for the workload.
Python Connectors run outside native realtime partitions, but each frame still
crosses the Python interpreter boundary. Use a native Connector when that cost
is not appropriate for the destination.

## 0.1.1 — 2026-09-01

This version was tagged on GitHub but was not published to PyPI. Its changes are
included in 0.1.2.

## 0.1.0 — 2026-08-31

Expand Down Expand Up @@ -129,10 +120,7 @@ provider-history truncation as unavailable instead of inferring them.
startup outcome. This avoids an unsafe repeated WinRT query in Core 1.1.4.
- WAN and TURN behavior are not yet qualified.

### Compatibility and upgrade
### Distribution

This is the first public Python release. There is no earlier package migration.

```console
python -m pip install pocketstation==0.1.0
```
This version was tagged on GitHub but was not published to PyPI. Its package
features are included in 0.1.2.
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ need the lower-level API.
- [Stream any application audio to a browser](../README.md#stream-any-application-audio-to-a-browser)
- [Browse the runnable examples](../examples/README.md)

## Build an integration
## Create an integration

- [Write application-owned audio into a Session](guides/application-audio.md)
- [Record stems and inspect Session delivery](guides/record-and-observe.md)
- [Compose a bounded voice workflow](guides/voice.md)
- [Publish a named AudioBus through Relay](guides/relay.md)
- [Build a Source, Operator, Connector, or Endpoint](guides/integrations.md)
- [Create a Source, Operator, Connector, or Endpoint](guides/integrations.md)

## Understand the system

Expand Down
5 changes: 5 additions & 0 deletions docs/getting-started/capture.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ Replace `Spotify` with a display name or application identifier. Pass a
positive integer, such as `application=1234`, when you already have a process
ID. Selection must resolve one running application before the Session starts.

A process ID lasts only for that process instance. If the application restarts,
discover it again. For a saved selection, use
`pocketstation.discover_sources()` and `Source.from_discovered()` as described
in [Persist a source at its supported scope](../operations/platform-support.md#persist-a-source-at-its-supported-scope).

The context manager starts one native Session and joins it when the block
exits. The iterator reads a finite native Endpoint; it does not create an
unbounded Python audio queue.
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/integrations.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Build a Source, Operator, Connector, or Endpoint
# Create a Source, Operator, Connector, or Endpoint

Choose the boundary by the direction and ownership of the work. All four use
the same Session compiler, finite queues, lifecycle, observations, and joined
Expand All @@ -20,7 +20,7 @@ owns Session lifecycle, route bounds, lineage, observations, and shutdown.
Python integrations run off realtime. They must not capture audio again, create
another Session, or hide an unbounded queue behind a provider callback.

## What a Connector solves
## Choose a Connector for outbound delivery

A Connector is the outbound boundary between one PocketStation Session and an
external system. Use it to publish source-aware audio to a WebSocket, call
Expand Down
84 changes: 83 additions & 1 deletion docs/operations/platform-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,92 @@ follow the host operating system.

## Supported Python

PocketStation 0.1.1 supports CPython 3.11 and newer through one ABI3 extension
PocketStation 0.1.2 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.

## Check permission without prompting

`pocketstation.microphone_permission_observation()` reads the current state
without showing consent UI:

```python
import pocketstation as pks

permission = pks.microphone_permission_observation()
print(permission.value)
```

`DENIED`, `RESTRICTED`, and `REVOKED` require a host or user action.
`NOT_DETERMINED` means the host has not decided. `NOT_OBSERVABLE` means the
platform cannot provide an authoritative preflight result; it must not be
treated as approval.

PocketStation does not prompt on import or discovery. Start capture from a
clear user action so the operating system can present consent when supported.
Opening the Source is authoritative and returns a typed failure when capture
cannot start. Application capture and microphone capture use separate
permissions, so enable the microphone only when the workflow needs it.

## Persist a source at its supported scope

Use discovery when the application needs to remember a selection or reopen it
after a restart:

```python
import pocketstation as pks

matches = pks.discover_sources(pks.SourceQuery.application("Zoom"))
if len(matches) != 1:
raise RuntimeError("Select one running Zoom source")

discovered = matches[0]
source = pks.Source.from_discovered(discovered)
print(discovered.selector_persistence_scope)
```

`Source.from_discovered()` chooses the strongest supported declaration: an
exact process instance plus stable application identity when both exist, a
stable application identity when no process is available, or a native
microphone device ID.

Persist the platform, source kind, stable key, and persistence scope only when
the scope matches the workflow:

| Persistence scope | Safe reuse |
|---|---|
| `APPLICATION_IDENTITY` | Reopen the application identity after a normal restart, then verify discovery still resolves it. |
| `DEVICE_IDENTITY` | Reopen the same installed and permitted device. |
| `PLATFORM_IDENTITY` | Reuse the platform-owned identity on that platform. |
| `PROCESS_LIFETIME` | Reuse only while the exact process is alive. Rediscover after exit. |
| `SESSION_DEFAULT_DEVICE` | Follow the host default on each new Session; do not treat it as a pinned device. |
| `None` | Do not persist the selector. Rediscover before the next Session. |

`source_id` belongs to media lineage. It is not a portable account identifier
or a substitute for the discovered stable key.

## Recover without selecting the wrong source

PocketStation does not switch Sources silently. A source disappearance or
backend failure appears in the Session event stream. When
`recovery_requirement` is
`EXPLICIT_REDISCOVERY_AND_NEW_SESSION`, stop or cancel the current Session,
discover again, let the user confirm any changed selection, and create a new
Session.

Choose fallback behavior explicitly:

- prefer a stored application identity, then require user confirmation before
falling back to a unique display-name result;
- use `Source.microphone_id()` to pin one device or
`Source.microphone_default()` to follow the host default on each Session;
- reject zero or multiple discovery matches;
- use separate Connector objects for primary and backup destinations so their
queues, failures, and shutdown outcomes remain independent; and
- keep provider retries and reconnect attempts finite. The advanced Connector
API reports readiness, health, and recovery but does not invent a provider
retry policy.

## macOS

Application capture needs screen and system-audio recording permission.
Expand Down
18 changes: 18 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ recording, and receiver playout as separate observations.

Do not treat an empty iterator as proof of silence when source opening failed.

If the application restarted, do not reuse its old process ID. Stop the current
Session, discover the source again, and create a new Session with
`Source.from_discovered()`. PocketStation reports
`EXPLICIT_REDISCOVERY_AND_NEW_SESSION` instead of silently selecting another
process or device.

## Permission changes are not reflected

`microphone_permission_observation()` never prompts. Treat `NOT_OBSERVABLE` as
unknown and use the typed Source open result. On macOS, restart the Python host
after changing screen-recording or microphone consent. On Windows and Linux,
verify that the current desktop user or service can access the selected audio
session and device.

Do not fall back from denied application capture to system mix or the default
microphone without a visible user choice. The fallback changes what audio the
application receives.

## Python misses frames

Inspect route capacity, queue depth, delivered frames, drops, and
Expand Down
2 changes: 1 addition & 1 deletion native/Cargo.lock

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

2 changes: 1 addition & 1 deletion native/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pocketstation-python"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
publish = false
description = "Native PocketStation runtime bindings for the Python SDK"
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.1"
version = "0.1.2"
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.1"
__version__ = "0.1.2"

__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 @@ -321,7 +321,7 @@
SignalStream,
)

__version__ = "0.1.1"
__version__ = "0.1.2"
__all__ = [
"RUNTIME_COMPATIBILITY",
"STREAM_EOF",
Expand Down
2 changes: 1 addition & 1 deletion python/pocketstation/compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class RuntimeCompatibility:


RUNTIME_COMPATIBILITY = RuntimeCompatibility(
sdk_version="0.1.1",
sdk_version="0.1.2",
core_version="1.1.4",
relay_connector_version="0.1.2",
python_requires=">=3.11",
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

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

Loading