theallelectricsmartgrid

SmartGridOne UI/audio state sharing audit — 2026-09-10

Read-only source audit of /Users/joyo/.codex/worktrees/e18e/theallelectricsmartgrid. No source edits, builds, tests, device operations, networking, or recording control were performed. The only created file is this report. JUCE internals are being audited separately.

Result

The inspected active visualizer has no app-level lock that paint holds while audio waits, and no display queue whose producer blocks when UI stops consuming. SetDisplayMode changes JUCE component visibility and repaint requests, not synthesizer state. UI-off removes appreciable main-thread calculation and memory reads in addition to rasterization, but the audio thread continues writing and publishing all display data and sending controller MIDI. This is a plausible workload/scheduling variable, not an identified cause of USB IN 0x82 transaction failures.

There are real weaknesses in display-data ownership. Their immediate consequence is an inconsistent UI read or a C++ data race under the conditions described below; no traced path converts them into an audio-thread wait. Some are inactive in the actual screenshot. Direct synchronous MIDI output calls do exist inside normal audio processing, but remain enabled in both UI modes.

The parent supplied old UI-on 17 restart gaps / 382.55 s versus UI-off 1 / 960 s, and JUCE 8.0.15 UI-on 5 / 168.192 s versus an ongoing UI-off run with 1 long gap at about 8 minutes. Both UI modes retain periodic damage. Those sequential, unequal observations motivate further investigation; this source audit does not establish a UI-dependent causal rate or explain the separate periodic damage. The supplied driver chronology is transaction error, driver restart, then callback gap, with no measured app DSP overrun.

What actually changes

The observed page and its workload

I viewed the existing UI-on screenshot. Its three large waveform panels, lower-right spectrum plus filter response, meters, four random-modulation panels and four envelope/LFO panels match the Source/DualWaveShapingVCO layout. The saved patch backup contains stateSaver.voiceSourceMachine_0 through _8, each [0,0,0,0]; VoiceMachineEnums.hpp:9 maps zero to DualWaveShapingVCO. This is positive evidence for the shown page and stored machine configuration, rather than an inference from constructor defaults alone. It is not a time-resolved record of every later frame.

Source is also the constructor/setup default (SmartGridOneEncoders.hpp:248, 376). Visualizer dispatch reads the selected bank and machine flags (SmartGridOneVisualizerMain.hpp:118, 224–280). The active Source entries are VCO1, VCO2, PostFilter scope, and SourceAnalyzer (ForEachSmartGridOneVisualizer.hpp:11, 22, 51, 62). The Sample and PhysicalModeling entries have different machine flags and are inactive for the stored VCO machines.

The UI-on workload includes:

Work done in paint Evidence Active in the screenshot?
Up to three 1024-sample FFTs, Hann windows, magnitude smoothing, 512-bin spectrum paths, and three 1024-point filter-response curves per paint. FFT/filter objects are component-owned, not DSP-owned. ScopeComponent.hpp:329, especially 348–388; ScopeWriter.hpp:264; BasicWaveTable.hpp:155 and AdaptiveWaveTable.hpp:237 select 1024-point types. Yes. Muted voices or a UI voice selection can reduce the count.
Three audio-scope widgets plus four control-scope widgets, each drawing up to three voices with 1024 path points each: up to 21,504 scope points per paint, before spectrum/response/modulation paths. ScopeComponent.hpp:47, 67–104; PathDrawer.hpp:190; ForEachModulationVisualizer.hpp:47. Yes. Count is the code maximum for all three voices, not a measured profiler total.
PathDrawer construction recomputes a 1024-entry power-based logarithmic lookup even for scope drawing, which uses linear sample positions. Additional PathDrawers are constructed for each spectrum’s filter response. PathDrawer.hpp:32, 43–49; ScopeComponent.hpp:59,382. Yes.
Four ganged-random panels reconstruct and draw past/future modulation trajectories, including up to 32 dashed future segments per voice. GangedRandomLFOComponent.hpp:182, 236–275. Yes.
Meters, encoder arcs, gradients, cached badge images, and vector paths. MeterComponent.hpp:222; EncoderComponent.hpp:205. Yes.

For the active three-voice maximum, scope-path reads are 21 * 1024 * 2 floats, and the three FFT input windows add 3 * 1024 floats: about 180 KiB of logical float loads from scope histories per paint, plus small metadata/marker reads. This is not measured memory-bus traffic: histories are strided, some reads repeat cached data, empty/muted scopes reduce it, and other UI state/graphics work is excluded. At the requested 62.5 Hz this would be about 11 MiB/s of those logical loads. Each FFT uses a local 1024-float wave table and a 1024-complex-float transform workspace (roughly 12 KiB of stack scratch), rather than allocating a scope-sized copy. See ScopeWriter.hpp:264–285 and AdaptiveWaveTable.hpp:48–60. At least the active paths are reconstructed as local juce::Path objects without app-side preallocation: up to 21 scope, 3 spectrum, 3 filter-response, and 12 random-modulation past-trajectory paths per paint, plus encoder paths. Whether/how these allocate internally is a JUCE implementation question, not directly measured by this source audit.

Thus this experiment changes CPU computation, memory reads, vector-path construction, and rendering. It cannot distinguish those costs from each other. Power, cache/memory traffic, shared CPU/GPU resources, and scheduling pressure are plausible intermediates; their size and relationship to USB errors were not measured here. The screenshot’s CPU label is derived from AudioDeviceManager.getCpuUsage and a rolling maximum, not a measure of total app/UI/GPU load.

An apparent additional repaint source is inactive: EncoderComponent’s segment-display setters would repaint during parent paint, but the whole call site is behind literal if (false) at EncoderComponent.hpp:402. It should not be blamed for a live repaint loop in this build.

Display production, ownership, and waiting

Audio-side production is unconditional in normal processing: NonagonWrapper.hpp:799 runs ProcessFrame and ProcessSample; TheNonagonSquiggleBoy.hpp:399 advances scope indexes each sample and populates UI state at 424–435. SquiggleBoy.hpp:1800 publishes all eight scope writers and updates meters regardless of which page is visible. This work is not removed by UI-off.

Shared object Ownership/concurrency assessment Relevance to shown VCO page
ScopeWriter float history Each object has 32 * 1024 * 1024 floats = 128 MiB of addressable storage (ScopeWriter.hpp:9). Eight embedded instances imply 1 GiB of float storage, not necessarily 1 GiB physically resident immediately. Audio writes ordinary floats and publishes an atomic index (71–101). Readers load ordinary floats (56–68). There is no reader acknowledgment or wait. Atomic publication orders completed writes, but does not pin history against later wraparound. A reader that is overtaken can race with reuse. The 9-voice/4-scope audio history spans about 19.4 s at 48 kHz, so routine short FFT reads are far from wrap; this is a conditional ownership defect, not evidence every FFT races. Audio and control histories are actively read. UI-off stops those reads but retains storage and audio writes.
Scope trigger/end history 256 ordinary start/end entries per scope/voice are reused; only the cursor is atomic (19–24,110–127). ScopeWriter.hpp:409 takes unpinned references/values from those arrays. Reuse after 256 triggers or modification of the current published cycle’s end can overlap a read. ScopeReader has no consistent-snapshot retry. This exposes display metadata to conditional races, without backpressure into audio. Active scopes/envelopes use these histories. Trigger rate, gate edges, and reader timing determine overlap.
SnapshotUIState Ten rotating plain snapshots plus an atomic published slot; GetCurrentSnapshot returns a reference, with no reader release (SnapshotUIState.hpp:10,21–40). Release/acquire publication protects a completed snapshot initially, but the producer can reuse that slot while a sufficiently delayed UI is reading it. For publication every 512 samples at 48 kHz, ten slots provide roughly 107 ms, not an ownership guarantee. The sample-waveform consumer uses it at SampleTrioWaveformVisualizerComponent.hpp:75, but that widget is inactive here. Delay/partial/mastering consumers are also different banks.
Meter and ganged-random state MeterReader publishes atomic scalars and UI getters load them (Metering.hpp:112). GangedRandomLFOUIState uses atomic fields plus a revision check, tries at most four times, then declines to draw (GangedRandomLFO.hpp:168). Neither requires an audio-side reader acknowledgment or paint-held lock. Active. No direct audio wait found.
Selected encoder bank NonagonWrapper.hpp:734 reads the live plain m_selectedBank, which SelectBank writes (SmartGridOneEncoders.hpp:381). Concurrent bank changes are a data race; a fixed page does not provide evidence of one. The voice-offset selection is UI-local. Read during paint, but no bank-changing interaction is evidenced by the supplied screenshot.
Melody-roll event data Its UI has an unbounded consistency retry, then copies plain events while audio may update note-end fields (ScopeComponent.hpp:240–262; ScopeWriter.hpp:606–612). The retry is on the UI thread, not the audio thread. Inactive: PanningAndSequencing bank, not Source.

The screenshot shows VCO machines, so a sample-buffer lifetime explanation is especially weak for these trials. The sample visualizer reads copied 1024-bin minima/maxima, not the live sample vector (SampleTrioWaveformVisualizerComponent.hpp:74,90–110; AudioBuffer.hpp:439). Its snapshot ring has the retention problem above, but it does not hold the underlying sample bank while drawing or lock the DSP.

MIDI and logger boundaries

Normal audio processing calls both controller SendMidiOutput methods at NonagonWrapper.hpp:596. Launchpad SysEx calls sendMessageNow at 62–71, encoder output at 96–104, and WrldBldr color SysEx at 281–295. These are direct synchronous API calls from the audio callback whenever the corresponding outputs exist. Their native implementation can have its own timing cost; this audit does not measure it. They do not depend on paint or the UI-render flag. A reduced graphics workload might indirectly affect their timing, but that is unproven.

Clock/transport and WrldBldr indicator messages use MidiSender.hpp:73, which pushes to a 16,384-message CircularQueue. CircularQueue.hpp:16 returns false immediately if full; MidiSender ignores that result. That can lose MIDI, not block the audio producer. A timestamped head can delay later queued messages on the dedicated sender thread. MidiOutputHandler’s spin lock is taken in Open/SendMessage, including sender-thread calls (MidiHandlers.hpp:126,181); the direct audio Process overrides bypass that wrapper lock. None of the inspected paint methods takes it.

UI interactions and MIDI input can both push into MessageInBus. Its queue algorithm is a single-producer/single-consumer pattern, so concurrent producers would need separate examination; paint does not push into it. ProcessMessages drains eligible events on audio (MessageInBus.hpp:70), meaning a genuine input burst can increase callback work in either UI mode.

Logging is also nonblocking with respect to queue fullness: AsyncLogger.hpp:95 increments a missed counter and returns if no slot exists. The message thread drains queues and flushes file output (110–150,247–264), in both modes. Delayed UI consumption can change logging bursts or lose records, but it does not make Log wait for space. The separate CircularByteQueue does have a sleep/retry Flush, but no visualizer path to it was found.

A measurement limit matters: MainComponent.h:107 ends dsp_us timing before getCurrentAudioDevice/getXRunCount at 109–110 and log formatting/enqueue at 111–117. The existing no-overrun evidence excludes that callback tail and JUCE’s surrounding callback machinery. Direct normal-frame MIDI sends occur inside the measured region.

Evidence that would discriminate

  1. On a later authorized diagnostic run, record full callback entry/exit and the existing DSP subsection separately, alongside substage timing for direct MIDI sends and paint/FFT. This would distinguish a long callback, a late callback after the driver stopped invoking it, and display work coincident with the first USB error. Use bounded in-memory records to keep the instrumentation’s effect small.
  2. Compare repeated, interleaved runs with the same patch, page, transport and MIDI devices, while counting driver transaction errors/restarts separately from periodic audio damage. Longer UI-off exposure alone does not determine the causal mechanism.
  3. Split the UI experiment into a static visible page, active drawing using frozen/copied display data, and live FFT/scope computation with drawing suppressed. That separates compositor/vector work, shared-memory consumption, and visualization arithmetic; the current flag combines all three.
  4. If ownership is suspected, collect publication generation and reader-age/retention evidence, or run an isolated later concurrency check on the display-state exchange. The shown VCO page prioritizes scope/end history and selected-bank access; sample and melody ownership problems are not active candidates for this screenshot.
  5. Relate any UI/callback timing outlier to the first USB IN transaction error, rather than only the restart gap that follows it. Continued short full callbacks up to the USB error would weaken a direct audio-thread-blocking explanation and favor the driver/device path or indirect system scheduling effects.

No source fix is justified as a USB-dropout remedy by this audit alone. The concrete next hypothesis is that the active Source visualizer adds material CPU/memory/rendering work; the missing evidence is whether that changes scheduling at the USB failure boundary.