Skip to content

Select the host audio device by name, and read back audioctl caps - #554

Open
Ticed wants to merge 6 commits into
infernode-os:masterfrom
Ticed:fix/host-audio-device-selection
Open

Select the host audio device by name, and read back audioctl caps#554
Ticed wants to merge 6 commits into
infernode-os:masterfrom
Ticed:fix/host-audio-device-selection

Conversation

@Ticed

@Ticed Ticed commented Aug 25, 2026

Copy link
Copy Markdown

What this changes

The emulator opened whatever the operating system called the default audio
device. On a developer machine that default is often a virtual device belonging
to an application, which may not even be running. Capture opens, every sample is
zero, and no layer reports a fault. Diagnosing it meant leaving InferNode
entirely.

#A/audiodev makes the choice and the verdict visible. Read it to enumerate
what the host offers and what is selected; write a name to select one:

; bind -a '#A' /dev
; cat /dev/audiodev
in selected default
in device 'MacBook Pro Microphone'
in device 'Loopback Audio'

A line from the read can be written straight back — audiodevwrite skips a
leading device or selected word, so in device 'X' selects X.

SDL audio is re-initialised when it comes up with no devices. The subsystem
can start before the host has enumerated anything, in which case it reports zero
devices permanently rather than recovering. The probe runs once per host rather
than on every open, and SDL_QuitSubSystem is guarded against live streams so a
device disappearing mid-capture cannot leave a dangling stream.

audioctl reads back the buffer capabilities. They were write-only, so a
caller could set play_buffer_ms and had no way to confirm it took — and a
failed echo > to a ctl file does not set $status. Reading now returns the
live values. audio_parse_buffer_ms also now accepts single-digit values, so
play_buffer_ms 0 parses instead of falling through.

emu/port/audio-tbls.c gains 16000 Hz, which speech capture needs and the table
did not offer.

Not agent-reachable. #A does not appear in appl/veltro/nsconstruct.b, so
a confined agent cannot enumerate or select host audio devices.

Tests

tests/audioctl_readback_test.b exercises the capability round trip over 9P and
is wired into tests/mkfile TARG and tools/dis-manifest.txt.
tests/host/audio_device_select_test.sh covers device selection, the read/write
round trip, and the capture verdict — the capture writes under the emu root, so
capture active / capture silent are asserted rather than skipped.
tests/host/audio_macos_test.sh fails when an open fails while devices are
present, rather than passing quietly.

On macOS ARM64 (SDL3 build), and red against an emulator built from the merge
base:

tests/host/audio_device_select_test.sh  rc=0 PASS   (merge-base: FAIL)
CapsRoundTripOver9P                     PASS       (merge-base: SKIP)

The readback test skips rather than fails on a backend with no accessor, such as
Linux, so it discriminates by moving from skipped to passing.

Not included

The Alt+V keyboard chord in emu/port/draw-sdl3.c belongs to the desktop voice
mode, not to audio device handling, and follows separately.

@pdfinn

pdfinn commented Aug 31, 2026

Copy link
Copy Markdown
Member

Heads-up on merging this one, and it is my doing rather than yours.

This branch predates #560, which stopped tracking compiled bytecode. It has a
.dis file in its diff, so merging hits:

CONFLICT (modify/delete): <path>.dis deleted in HEAD and modified in <branch>.
Version <branch> of <path>.dis left in tree.

Git leaves the file sitting in the working tree, so resolving with git add .
would quietly re-commit bytecode that master deliberately removed. The
resolution is to delete it:

git rm <path>.dis

Or just rebase onto current master, where the file no longer exists and the
conflict does not arise.

Nothing else in the PR is affected — dis/ is a build product now, rebuilt
with:

for d in appl appl/mpeg appl/veltro tests; do (cd $d && mk install); done

hooks/post-merge does that automatically after a pull if you have run
./hooks/install.sh.

Sorry for the friction — five open PRs are in this position because of the
timing.

@pdfinn pdfinn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot exercise CoreAudio from here, so this covers the parts that are
testable on Linux plus a read of the C. Someone on macOS should confirm the
backend itself and the SDL re-init behaviour — I am not signing off on those.

What I did check

The shared files do not break other platforms. emu/port/devaudio.c,
audio.h and audio-tbls.c are built everywhere, so I applied the PR onto
current master and built both Linux targets:

headless build exit=0
sdl3 build     exit=0

audiodevwrite is careful C. Bounded at 256 with Ebadarg above it,
smalloc(count + 1) with an explicit NUL, memmove rather than a string
copy, and waserror()/poperror() around the allocation so the buffer is
freed on the error path. The quote handling matches the read format, so a line
from cat can be written straight back, which is a nice property and the
comment says so.

It is not agent-reachable. #A does not appear in
appl/veltro/nsconstruct.b, so a confined agent cannot enumerate or select
host audio devices. Worth stating explicitly in the PR, because "select the
microphone" is exactly the capability someone will ask about, and the answer
is good.

The new test skips honestly. On Linux:

this audio backend registers no buffer-cap accessor
PASS  0 passed, 1 skipped

That is the right shape — a clean SKIP naming the reason, rather than passing
while testing nothing. It is the pattern #563 is trying to establish and the
opposite of what #565's test currently does.

One thing to add

dis/tests/audioctl_readback_test.dis is in tests/mkfile TARG but not in
tools/dis-manifest.txt. Since #560 the manifest is the tracked record of
what the build must produce, so a new module needs both, in the same commit.
One line.

Not reviewed

  • emu/MacOSX/audio.c, the CoreAudio backend — needs a macOS reader
  • the SDL "came up with zero devices, re-initialise" path — I cannot reproduce
    the condition
  • whether 16000 Hz in audio-tbls.c is right for the speech capture path;
    the addition is obviously harmless, the claim that speech needs it I am
    taking on trust

Note on scope

This is three changes in one PR — device selection, SDL re-init, and ctl
readback — plus a new CoreAudio backend. They are related by subject but
independent in risk: the readback is trivial, the device file is a new
namespace surface, and the backend is 1000+ lines of platform code. If the
macOS review stalls on the backend, the first two are worth landing without
it. Not a blocker, just an option if it sits.

Add the manifest line, and resolve the dis/tests/audioctl_readback_test.dis
conflict by deletion — see my other comment.

@Ticed
Ticed force-pushed the fix/host-audio-device-selection branch from fd1e519 to 0af4d14 Compare August 31, 2026 11:17
@Ticed

Ticed commented Aug 31, 2026

Copy link
Copy Markdown
Author

Manifest line added and the .dis conflict resolved by deletion via the rebase.

16000 Hz is right, and you were right to only take it on trust. Verified:
docs/SPEECH-ARCHITECTURE.md:391 shows the voice stack writing rate 16000 to
audioctl, and appl/veltro/speech9p.b converts everything to 16 kHz mono s16.

I had the macOS parts reviewed here — the ones you said you were not signing off
on — and it found three things you should know about, two of which contradict
what the PR told you.

emu/MacOSX/audio.c is gone. You could not review it; it turns out nothing
built it either. emu/MacOSX/emu says audio audio-sdl3, emu-g has audio
commented out, and both builds link audio-sdl3.o and never audio.o. It
registered no Audiodevops, so device selection — this PR's feature — would not
have worked through it even if wired in. The giveaway was
audio_macos_test.sh grepping for a -66680 string only that file could emit.
531 lines with no build coverage is not something to land unreviewed, so I
dropped it and the -framework CoreAudio -framework AudioToolbox flags that
were linking an object nothing produced. That removes your "needs a macOS
reader" item rather than answering it.

The round-trip property you praised did not work. The comment and the doc
both claimed a read line could be written straight back. It errored:

; echo in device 'MacBook Pro Microphone' > /dev/audiodev
sh: write error: audiodev: no device of that name

The parser stripped the direction word but not device or selected, and no
read line has the bare form. Fixed in the parser rather than by softening the
comment, and pinned by a test step.

The device-select test never exercised its own verdict. Step 5 captured to
/tmp, which does not exist in the emu namespace, so cap_bytes stayed 0 and
the read always ended capture idle → SKIP. The capture paths this feature
exists to surface were never asserted. It now writes under the emu root, and
capture-idle-after-capture is a failure.

Also bounded the ensure_sdl_audio probe to once per host — on a machine with
no devices it re-ran twenty init cycles on every open — and guarded both
SDL_QuitSubSystem sites against live streams, which was the unplug race you
flagged as unreviewed.

On scope: you offered landing the first two without the backend. With the
backend gone that is close to what this is now, so I have left it as one PR. Say
if you would still rather have the ctl readback separately and I will split it.

No macOS configuration compiles emu/MacOSX/audio.c: the emu config
lists 'audio audio-sdl3', emu-g has audio commented out, and both
builds link audio-sdl3.o. It registers no Audiodevops, so it could not
perform device selection even if it were wired in. Remove it and the
-framework CoreAudio/-framework AudioToolbox flags added for it — link
flags for an object that is never linked.

audio_macos_test.sh grepped for coreerror() text only that file could
emit; that branch was dead too and is gone. The empty-capture skip now
exits 77 like every other skip, and the capture asserts a size floor
instead of passing on a handful of bytes.
The comment in audiodevwrite and docs/SPEECH-REMOTE-AUDIO.md claimed a
line from a read of #A/audiodev can be written straight back. It could
not: the parser stripped the direction word but not the device/selected
keyword, so every form the read actually emits was refused with 'no
device of that name'. Teach the parser to skip that keyword (only when
a name follows it, so a device literally called device stays
reachable) and restate the accepted forms in the doc.

Also document INFERNODE_AUDIO_IN / INFERNODE_AUDIO_OUT there, which
were shipped undocumented.
ensure_sdl_audio re-ran its 20x50 ms SDL_Init/Quit probe on every call
when the host enumerates no audio devices, so each /dev/audio open and
audioctl read blocked a second or more, forever. Probe once and
remember the result: sdl_quiet_host short-circuits the churn while the
subsystem stays up, so SDL still sees devices that appear later.

SDL_QuitSubSystem(SDL_INIT_AUDIO) was called without checking for live
streams, in both the re-probe path and the 'not initialized' recovery
in open_stream. Quitting under an open stream leaves in_stream /
out_stream dangling and the next SDL_DestroyAudioStream is a
use-after-free — the unplug-mid-capture race. Both sites now check
sdl_streams_live() and leave the subsystem up instead.
Step 5 captured to /tmp/audiodev-cap.pcm, but /tmp does not exist
inside the emulator namespace: dd failed, cap_bytes stayed 0, and the
read always ended 'capture idle', so the active/silent verdicts this
feature exists to surface were never verified. Capture to a path under
the emu root instead and assert the verdict — 'idle' after a capture
to a writable file is now a failure, not a skip.

Also add the round-trip property the parser fix enables: the exact
line a read of #A/audiodev produced ('in device <name>') is written
back unchanged and must select.
@Ticed
Ticed force-pushed the fix/host-audio-device-selection branch from 0af4d14 to fd51797 Compare September 2, 2026 03:14
@Ticed

Ticed commented Sep 2, 2026

Copy link
Copy Markdown
Author

Rebased onto current master (df34b02). Force-pushed 0af4d14 -> fd51797.

git range-diff reports every commit unchanged — same content, same messages,
only the base moved. The five commits that landed underneath are #583, #582,
#566, #578 and #580; none of them touches this branch's files.

Re-verified on macOS after the rebase.

Device enumeration and select-by-name verified in both directions against a
loopback audio device. audio_device_select_test.sh exits 0, with the capture
case reporting SKIP rather than claiming a pass. Rebuilt through
build-macos-sdl3.sh, since the emulator was stale against #582.

#578 fixed the ClusterFuzzLite link break, so Fuzz should now be green here
rather than red for a reason that was never this branch's.

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.

2 participants