Skip to content

Feat/spatcore io stage1 - #4

Merged
pob31 merged 12 commits into
mainfrom
feat/spatcore-io-stage1
Aug 2, 2026
Merged

Feat/spatcore io stage1#4
pob31 merged 12 commits into
mainfrom
feat/spatcore-io-stage1

Conversation

@pob31

@pob31 pob31 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

No description provided.

pob31 and others added 12 commits August 2, 2026 16:35
The shared audio device layer landed on spatcore main (8e0d7e6): DeviceHost,
DeviceIoCallback, HardwareIndexMap and a converged TestSignalGenerator. XOA can
adopt it now; the Audio Interface window and patch matrix it belongs under are
still app-side in WFS-DIY and arrive later.

This documents both stages, and in particular the things that would silently
corrupt audio if a migration session did not know them:

- Under DeviceIoCallback a single buffer row is BOTH hardware input h and
  hardware output h. XOA's stem gather happens to run before the decoder
  writes, so it is correct today by accident of ordering; the ordering becomes
  load-bearing.
- The buffer width is max(highest active input, highest active output)+1
  clamped to the ceiling, so it is not the speaker count. Taking the speaker
  count from the buffer would run the decoder over input-only rows.
- countNumberOfSetBits() is a count, not an index bound, and XOA indexes its
  meters and prepare() by that ordinal.

It also records what XOA is and is not exposed to: it is NOT affected by the
128-channel AudioSourcePlayer cap (it was never an AudioAppComponent), but it
does have the channel-mask defect, since it never clears
useDefaultInput/OutputChannels and so can neither honour nor persist a mask.

CLAUDE.md registers the document and corrects two claims: the pinned spatcore
commit does not contain the io layer, and the DEVPLAN's decision records run
past D31.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Updates the handoff now that spatcore/ui/patch/PatchMatrixComponent exists and
the boundary has moved. Section 7 was "the window XOA inherits"; it is now a
build specification, because the tabs and window are staying in WFS-DIY.

What changed and why: the matrix is 2,179 lines of scroll, hit-testing,
drag-patching, 1:1 constraint enforcement, keyboard navigation and
signal-presence tinting, none of it app-specific once the seams are cut — worth
sharing. The shell around it is ~700 lines of layout and widget glue, and
sharing it would have pulled the app's long-press button, its slider base
(carrying TTSManager and OSC origin-tagging) and 3,199 generated lines of
signal-flow artwork into spatcore.

So section 7 now carries what a session here actually needs: how to construct
the matrix and what every provider is for, the AudioPatch ValueTree section XOA
must add, and a behaviour spec for the shell — including the three
non-obvious bits (stop the tone on every exit path, the non-linear level and
frequency mappings, the 1-based combo vs 0-based enum), a warning not to port
WFS-DIY's dead TestSignalControlPanel, and the instruction to make the window
stopped-only because patching a live rig destroys speakers.

The load-bearing detail is unchanged from stage 1 and now has a second home:
feed activeHardwareChannels from DeviceHost::getNumActiveOutputs(), not from
the device's channel-name count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… matrix)

Link spatcore-io into the app and move the pin prose in CLAUDE.md and
XOA-PLAN.md $4. Build-only change: nothing consumes the io layer yet
(that lands with the AudioEngine migration). Release build and both
ctest lanes verified green on this commit alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AudioEngine now opens/restores its device through spatcore::io::DeviceHost
and is driven as a juce::AudioSource by spatcore::io::DeviceIoCallback,
replacing the bare AudioDeviceManager::initialise and the raw
AudioIODeviceCallback:

- DeviceHost writes explicit channel masks with JUCE's useDefault*Channels
  flags cleared, so the masks the app asks for are the masks that open, the
  saved device state can round-trip a channel selection, and rigs are no
  longer silently capped at 256 outputs (handoff $2.2). The addressing
  ceiling is the new xoa::kMaxHardwareChannels = 512 (D37).
- DeviceIoCallback hands getNextAudioBlock a hardware-indexed buffer. The
  stem gather now runs off that buffer BEFORE anything writes an output --
  a row that is an active output aliases the device's own storage with the
  matching input pre-copied in, so this ordering is load-bearing ($5.1).
- Speaker/stem counts come from the device masks in prepareToPlay, never
  from the buffer width ($5.2), and stay ordinal-indexed under an asserted
  identity mapping (D36 option A; option B when the stage-2 patch window
  lands). Downstream (decode, comp, test signal, meters) sees a
  speaker-width view, unchanged in shape from before.
- A device change made through the stock selector is re-asserted to the
  enable-all policy from the change listener (no-op when already right),
  and open/policy errors are captured in getLastDeviceError() -- a failing
  driver now reports a non-empty reason ($5.5).

Verified: Release + Debug builds, both ctest lanes, and --gui-smoke in
both configs against real hardware (Debug = asserts active). The $6
multi-channel hardware acceptance pass remains a manual step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
spatcore pin bump 00ea5d8 -> ac45f0f (PR #8: SpeakerId in the shared
generator). XOA's forked TestSignalGenerator becomes a using-alias shim
in the shared-EQ style; SpeakerId and getCurrentSpeakerIndex() now come
from spatcore with the ordinals unchanged, so the SpeakersDecoderTab
combo mapping and persisted values hold.

The deterministic seed is opt-in upstream (the shared prepare() seeds
from the wall clock by default), so AudioEngine pins kTestSignalSeed at
construction and the determinism test seeds explicitly. The shared
prepare() also fixes the silent-Tone defect the fork never had (phase
increment recomputed on sample-rate change).

Verified: Release build, both ctest lanes, --gui-smoke.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…signal type)

The 500 ms hearing-protection ramp now applies to Sweep, DiracPulse and
SpeakerId as well as the continuous types (spatcore PR #9) - no signal
type may bypass it, so the operator always gets time to stop a too-loud
test before it hurts. No XOA code changes: none of the test-signal tests
assumed an unramped transient start. Release build, both ctest lanes and
--gui-smoke green on this pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…D45)

Stage-2 groundwork: an input stem is no longer implicitly mono.

- inputFormat (0 = mono point source, 1..10 = AmbiX group of that order,
  (N+1)^2 stem channels) on each input's Channel node, with span helpers
  (getInputChannelCount / getStemChannelOffset / getTotalStemChannels)
  and a kMaxStemChannels = 128 total ceiling enforced by stepping formats
  down, last HOA input first (D45).
- AudioPatch section: InputPatch (one flattened row per stem CHANNEL,
  D43) and OutputPatch (one row per speaker), property names shared with
  the spatcore patch matrix (patchData/rows/cols/activeHardware*).
  Identity-diagonal defaults; reconcileAudioPatch() remaps input rows by
  per-input block across format/count changes and pads new rows with the
  identity continuation, so a project that never opens the patch window
  keeps behaving exactly identity-mapped. recomputePatchCols applies the
  WFS-DIY column policy; updateHardwareChannelCount feeds the truthful
  DeviceHost counts. Patch edits are non-undoable, as in WFS-DIY.
- Persistence: audiopatch.xml joins the section files (save/load/backup/
  merge), with a post-load reconcile.
- InputsTab: Format combo; position/conditioning/spread/NFC grey out for
  HOA groups (inert there, D44). Descriptor + registry entries keep the
  XoaUiDescriptorTests coverage gate green.

Engine consumption of formats and patches lands in the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The audio path now runs in three explicit domains joined by the patch
maps (RtSnapshot'd PatchRtState, composed from the AudioPatch trees on
the message thread, identity by default):

- GATHER: flattened stem channel k reads hardware input
  hwForStemChannel[k]; per-input meters take the max across the input's
  span; new per-hardware-input peaks feed the patch matrix tinting.
- BUS: applyEncoder handles both formats per input. Mono keeps the
  point-source path bit-exactly (SH row broadcast + NFC lanes). An HOA
  group contributes its own channel c to bus channel c at its liveMatrix
  factor - now order-adapt x gain composed by AmbiCalculationEngine
  (zero-padded upmix via weights::orderAdaptGains), with NFC forced off
  and the same per-coefficient one-block ramps. EncoderRtParams carries
  the per-input stem offsets/orders.
- SCATTER: decode+comp run on a speaker-domain scratch sized by the
  STORE's speaker count (no longer the device outs), then speaker s is
  copied to hardware row hwForSpeaker[s]; unfed active outputs are
  cleared. The test signal is injected post-scatter in the hardware
  domain (D46) so testing-mode reaches every open output. Per-speaker
  meters read the speaker's patched hardware row - what actually leaves
  the device.

The $5.1 gather-before-write invariant is now structural (decode never
targets io rows), and the identity jassert stays: it guards the
DeviceHost contiguous-mask guarantee the hardware indexing rests on.

Verified: Release + Debug, ctest (incl. offline-render bit-exactness
smoke), --gui-smoke both configs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
XoaPatchMatrix derives spatcore::ui::patch::PatchMatrixComponent and
binds the app seams: AudioPatch trees + shared property names, flattened
stem-channel rows labelled/coloured by their owning input (D43), palette
from ColorScheme, LOC strings, XoaLookAndFeel::uiScale, TTSManager
announcements. Distinct basename per the handoff's object-file trap
note. The Audio Interface window that hosts it lands next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
XOA's own shell around the shared matrix (the handoff draws the sharing
line here deliberately): a device info bar, and Device Settings / Input
Patch / Output Patch tabs.

- DeviceSettingsPanel routes every mutation through DeviceHost
  (openNamedDevice / setDeviceAllChannels / enableAllChannels); the
  sample-rate and buffer-size path re-asserts the mask policy after
  setAudioDeviceSetup, so the D39 spatcore setters stay optional.
- One XoaPatchTab serves both directions: Scroll/Patch modes, Test on
  the output tab with the non-linear level and log-frequency mappings
  plus Hold, long-press Unpatch All, and the 20 Hz header repaint that
  animates the input signal-presence tint.
- Transport gate (D41): while the file transport plays, tones stop,
  modes drop to Scrolling and the tabs are disabled behind a banner.
  A tone is also stopped on tab change and window close.
- SystemConfigTab drops juce::AudioDeviceSelectorComponent for an
  "Audio Interface…" button plus a live device summary and the last
  DeviceHost error.
- openAudioDevice now seeds the patch trees' active-channel counts.
  They were only written by the device-change listener, which does not
  fire at startup, so the matrices' overflow gating was disabled and
  channels the device never opened looked patchable. Found by opening
  the window: outputs 7-24 were drawn live on a 6-output device.

GUI-verified on Windows: all three tabs render, the matrices show the
identity default, gating dims the unopened columns, and switching an
input to HOA 1 expands it to four labelled ACN rows in the input
matrix, greying its point-source controls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tests (tests/XoaPatchTests.cpp): stem spans and the kMaxStemChannels
ceiling stepping formats down; the identity default patch; a patchData
round-trip through the routing POD including a NON-CONTIGUOUS speaker
assignment and an unpatched row; a format change preserving the other
inputs' hardware channels while re-spanning the rows; speaker growth
keeping the identity continuation; and the HOA group merge verified
against a double reference decode (component c -> bus channel c at the
order-adapt gain, silent above the group's order).

Also adds rt::makeMonoEncoderParams: EncoderRtParams gained per-input
stem offsets, and brace-initialising it left them all zero, which points
every input at stem row 0. The factory states the all-mono contract
explicitly; the existing encoder tests now use it.

Docs: D41-D47 in XOA-DEVPLAN.md (transport gate, both matrices,
flattened rows, inputFormat, the stem ceiling, the hardware-domain test
signal, clusters deferred), D24 marked superseded, and the handoff's
status table updated to ADOPTED.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Architectural correction: program material is played by external apps
(Reaper, QLab, ...) and arrives through the device inputs as stems and
AmbiX HOA groups; there is no cueing mechanism in this app. A WFS-DIY
style interactive sampler may come later - a plain file player will not.

- FilePlayer and the header transport (Browse/Play/Stop/Loop, position
  bar, file label) are gone; the header keeps a Test-scene latch, the
  rotation dials, master gain and the status line. InputSource is now
  { none, testScene } with none the default: the no-source publish is a
  zero-channel BusRtParams whose gather clears busA every block while
  the encoder stage accumulates the device stems on top. makeBusParams,
  BusRtParams and the gather loop are byte-identical - the offline
  bit-exactness smoke passes unchanged.
- The patch window's transport gate is dropped (D49, supersedes D41's
  mechanism): an external transport cannot be sensed, so a local gate
  would be theatre. Tones still stop on every exit path; the universal
  500 ms ramp remains the safety property.
- playbackFilePath/Loop/ContentOrder/Convention leave the schema,
  descriptors, registry, constraints, OSC bindings and locale in one
  commit (the descriptor-parity gate enforces the lockstep). OSC map
  bumped to v1.1 with a documented removal of the three
  /xoa/config/playback* leaves (D50); live HOA groups stay AmbiX-only,
  per-input convention/order override deferred to the clusters era.
- Tests: XoaFilePlayerTests becomes XoaFileFormatTests, keeping the
  WP6 C1 large-channel WAV/FLAC spike (any future file import builds on
  it) and dropping the player half. The persistence test now proves a
  pre-D48 project file carrying the removed ids loads harmlessly.
- Docs: PRD FR-8 rewritten (external program input), WP6 records
  annotated as historical, D48-D50 recorded, kMaxFileChannels
  re-documented (value baked into offline baselines, name kept).

Verified: Release + Debug builds, ctest green (offline-render smoke
UNCHANGED), --gui-smoke both configs, app launches and responds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pob31
pob31 merged commit 0ab95d3 into main Aug 2, 2026
3 checks passed
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