Skip to content

feat(sounds): crash-isolated sound server and dual-channel output modes - #73

Merged
larsrollik merged 1 commit into
mainfrom
feat/sound-server-isolation
Aug 13, 2026
Merged

feat(sounds): crash-isolated sound server and dual-channel output modes#73
larsrollik merged 1 commit into
mainfrom
feat/sound-server-isolation

Conversation

@larsrollik

Copy link
Copy Markdown
Member

What

Make sound output robust against a native sound-device fault, and fix the stereo output so playback doesn't depend on which conductor is wired.

Crash-isolated sound server

SoundServerClient is a drop-in for StereoSound that runs PortAudio in a spawned subprocess. A native segfault or hang in the sound device now takes down only the child; the client detects a dead worker (proc.is_alive()) and degrades to no-sound instead of propagating the fault. (A native segfault can't be caught in-process, so isolation is the only real defense.)

Latency design: spawn start (Windows-safe), a blocking Queue.get() in the server (event-driven, avoids the ~15.6 ms Windows timer granularity a poll loop would hit), timeBeginPeriod(1) + elevated priority on Windows, best-effort nice on Unix, pre-registered cues so a trigger is a fire-and-forget integer put.

Channel modes on StereoSound

  • channel_mode="both" (default) — the tone on both output channels, so playback no longer depends on which L/R conductor is wired.
  • channel_mode="ttl" — the tone on one channel plus a full-scale sync level on ttl_channel, a hardware onset marker for a BNC/DAQ input.

Measurements

Added trigger latency (client put → server get, the isolation overhead), Linux:

condition p50 p99 max
idle 0.03 ms 0.05 ms 0.18 ms
all cores at 100% 0.03 ms 1.43 ms 3.66 ms

End-to-end on real Bpod hardware via a BNC loopback (softcode → handler → audio → BNC1High, timed on Bpod's own clock):

backend p50 p99
in-process (today) 19.75 ms 21.55 ms
server (isolated) 20.70 ms 21.35 ms

The isolated server is within run-to-run noise of the in-process path. The ~20 ms absolute is audio output buffering (pipewire quantum + DAC + softcode USB round-trip), shared by both paths, not introduced by the subprocess.

Channel-mode check on hardware: both 50/50 hits; ttl fires ~7 ms earlier (marks true buffer onset rather than waiting for the sine to climb its fade-in) and is 30/30 at realistic trial spacing.

Verification

  • scripts/bench_sound_server.py — IPC latency benchmark (run on Windows to confirm parity: uv run python scripts/bench_sound_server.py -n 5000).
  • scripts/measure_sound_latency_bpod.py — Bpod BNC-loopback end-to-end harness.
  • Unit tests for both channel modes (tests/test_sounds.py).

Follow-ups (not in this PR)

  • Windows IPC p99 run to confirm cross-platform parity.
  • Switching the tasks over to SoundServerClient (this PR only adds the capability).

Run PortAudio in a spawned subprocess (SoundServerClient) so a native
segfault or hang in the sound device takes down only the child, not the
session; the client detects a dead worker and degrades to no-sound. Added
trigger latency is negligible (put->get p99 ~0.05 ms idle, ~1.4 ms under
full CPU load), confirmed on-rig via a Bpod BNC loopback: end-to-end p99
matches the in-process path within noise.

Add StereoSound channel_mode: 'both' (default) duplicates the tone onto
both output channels so playback no longer depends on which conductor is
wired; 'ttl' emits a full-scale sync level on ttl_channel for a hardware
onset marker into a BNC/DAQ input.

Includes an IPC latency benchmark and a Bpod-loopback measurement harness.
@larsrollik
larsrollik requested a review from a team as a code owner August 13, 2026 11:50
@larsrollik
larsrollik merged commit 60b1d93 into main Aug 13, 2026
7 checks passed
@larsrollik
larsrollik deleted the feat/sound-server-isolation branch August 13, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant