feat(gfx): audio capture builtins — audio_capture_open/read/close (#579)#588
Merged
Conversation
The gfx extension's audio surface was output-only; DeslanStudio's transport recording (F-DS-17) could never record a real microphone. Pull-model capture via the already-dlopen'd SDL2: SDL_OpenAudioDevice(iscapture=1) in queue mode + SDL_DequeueAudio — no callback, no C->eigs re-entry, no new threading surface. - audio_capture_open of [freq, channels] -> dev id or 0 (defaults 44100/1; allowed_changes=0 so SDL converts to the requested format; re-open closes the previous device; graceful 0 without SDL/capture) - audio_capture_read of null -> VAL_BUFFER (the #578 fast type) of new samples in [-1, 1], at most 2048 per call — loop until empty. Empty buffer = nothing yet; null = no device open. - audio_capture_close of null — double-close safe; gfx_close also reaps the capture device before dlclosing SDL. Tape-first: open and read are TRACE_NONDET_TAKE/RECORD builtins — under EIGS_REPLAY the tape is taken before any SDL call, so replay never opens or reads a real microphone; the b[...] buffer encoding rides N records natively. The 2048-sample per-read cap exists so the worst-case record (~51 KB) always fits the 64 KiB N-record budget — a truncated record would silently fall back to the live mic on replay. audio_open (output) stays deliberately untraced (playback is a re-performed side effect, like print); the residual gap of its env-dependent return is documented in docs/TRACE.md. Tests: gfx-suite section [62] +13 checks (buffer shape, per-call cap, [-1,1] range, monotonic accumulation, drain-until-empty, close/read- after-close/double-close, re-open; device-gated with constant counts — SDL_AUDIODRIVER=dummy provides a silent capture device), and a test_replay.sh case that records a capture session then replays it with the SDL driver deliberately broken. Fixed the [62] label drift (said 24, file ran 25; now 38). Closes #579 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Closes #579
What
Live recording input for the gfx extension — the missing device source for DeslanStudio's transport recording (F-DS-17). Pull-model capture through the already-dlopen'd SDL2:
SDL_OpenAudioDevice(iscapture=1)in queue mode +SDL_DequeueAudio. No callback, no C→eigs re-entry, no new threading surface — fits the existing polling clients (gfx_pollloops,transport_tick_recording).audio_capture_open of [freq, channels]→ device id or0(defaults[44100, 1];allowed_changes=0so SDL converts to exactly the requested format — no rate-negotiation surface needed; re-open closes the previous device; graceful0without SDL or a capture device)audio_capture_read of null→ buffer (VAL_BUFFER, the gfx audio: audio_play / audio_play_loop reject VAL_BUFFER samples (list-only) #578 fast type) of new samples since the last read, floats in[-1, 1](interleaved forchannels > 1); at most 2048 samples per call — loop until the returned buffer is empty to drain fully. Empty buffer = nothing new yet;null= no device open.audio_capture_close of null— double-close safe;gfx_closealso reaps the capture device before dlclosing SDL.vs the issue's proposed shape
Matches the issue's poll/drain model exactly (queue-mode capture, no callback, CI-testable under
SDL_AUDIODRIVER=dummy— verified empirically: dummy capture opens, accumulates silence monotonically, partial dequeue and double-close both behave). Three deliberate deviations:audio_capture_open/read/closeinstead ofaudio_open_capture/audio_read/audio_close_capture— one greppable family prefix, and bareaudio_readcollides with theread_*file-builtin namespace.VAL_BUFFERthe bulk-samples type on the output side; capture returns the same type, so a record→overdub round trip (audio_capture_read→transport_feed_recording→audio_play) never converts.Tape design (the load-bearing part)
Captured audio is the textbook nondeterministic input, so
audio_capture_openandaudio_capture_readareTRACE_NONDET_TAKE/RECORDbuiltins — underEIGS_REPLAYthe tape is taken before any SDL call: replay never opens or reads a real microphone; the recorded device id and sample buffers are served off the tape.b[…]N-record encoding (write_value_ptr_full / parse_value_p) already round-tripsVAL_BUFFER; no format change needed.%.17gover all 65536 possible int16-derived samples is 23 chars → a full read records at ~51 KB < 65536. An over-budget record is written…<truncated>and replay silently falls back to the live source — for capture that would mean a live mic read mid-replay, the exact divergence the tape exists to prevent. The cap makes truncation impossible by construction. (Documented in docs/TRACE.md and at the definition site.)audio_openinconsistency found and documented, not changed: the output-sideaudio_openis untraced today — its env-dependent return (0on a machine with no audio) can steer a branch differently on replay, andg_audio_freq(which the untraced generators read) is hidden nondet state. Tracing it now would change how existing tapes replay and silence playback under replay; capture (an input) must be traced regardless. The asymmetry + residual gap is spelled out in docs/TRACE.md so it's a deliberate line, not an accident.audio_capture_closeis deterministic (alwaysnull) and untraced; under replay it's a no-op because no device was ever opened.Tests
null; buffer type; per-call cap on first and full chunks;[-1, 1]range scan; monotonic accumulation (bounded poll); drain-until-empty terminates; read-after-close →null; double-close; re-open with default args. Runs against dummy and real drivers (content assertions are range-based, not silence-based). Fixed the [62] label drift while there: it said 24 but the file already ran 25; now 38 and enforced.SDL_AUDIODRIVER=dummy+EIGS_TRACE, then replays withSDL_AUDIODRIVER=doesnotexist— a live open would fail, so byte-identical output proves the id and samples came off the tape. Gated (skips on non-gfx builds and where no capture device opens, e.g. the CI dev image, which has no libSDL2).Gates (all local, X540NA)
make test(release, default build): 2862/2862make asan+ASAN_OPTIONS=detect_leaks=1full suite: 2866/2866, leak tally 0, [87] cleanmake asanexcludes ext_gfx.c): test_audio 38/38 + test_replay 20/20, leak-cleanmake jit-smoke,make freestanding-check: pass (freestanding surface untouched)make gfx+ full suite: 2901/2901 (= 2862 + 38 audio + 1 replay)🤖 Generated with Claude Code