feat(sounds): crash-isolated sound server and dual-channel output modes - #73
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
SoundServerClientis a drop-in forStereoSoundthat 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:
spawnstart (Windows-safe), a blockingQueue.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-effortniceon Unix, pre-registered cues so a trigger is a fire-and-forget integerput.Channel modes on
StereoSoundchannel_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 onttl_channel, a hardware onset marker for a BNC/DAQ input.Measurements
Added trigger latency (client
put→ serverget, the isolation overhead), Linux:End-to-end on real Bpod hardware via a BNC loopback (softcode → handler → audio → BNC1High, timed on Bpod's own clock):
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:
both50/50 hits;ttlfires ~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.tests/test_sounds.py).Follow-ups (not in this PR)
SoundServerClient(this PR only adds the capability).