SSPACEAGEDOCUMENTATION/
DSP and Instruments

SpaceAge Convolution Architecture

Updated Aug 29, 2026   |   52.3 KB   |   docs/Convolution_Architecture.md

SpaceAge Convolution Architecture

Status

SpaceAge's first convolution product is the Mixer-owned shared Reverb return. The production processor owns ConvolutionReverb directly; the former juce::Reverb return has been removed. It is built into Standalone and VST3 and covered by focused DSP, processor-routing, host, state, editor, release-hygiene, and audition proofs.

The implementation intentionally separates a neutral convolution primitive from reverb policy so later convolution synthesis work can reuse the engine without inheriting room generation, dry/wet mixing, damping, or stereo-width behavior.

Source boundaries

  • Source/SpaceAgeConvolution.h/.cpp
    • PreparedConvolutionKernel carries samples, source rate, stable identifier, semantic kind, and normalization policy.
    • ConvolutionEngine is a wet-only stereo convolution primitive.
    • ConvolutionReverb owns SpaceAge's room responses and reverb controls.
  • Source/PluginProcessor.cpp
    • Owns sends, return gain, enable state, runtime tail scheduling, and mixing.
  • Tests/SpaceAgeConvolutionTest.cpp
    • Focused primitive, wrapper, performance, host-configuration, and audition contracts.
  • Tests/AudioSelfTest.cpp
    • Product-level shared-effects routing, control, silence, and tail proof.

Neutral engine contract

ConvolutionEngine accepts prepared kernels rather than files. Kernel acquisition, licensing, excitation, and interpretation remain outside the engine.

The engine:

  • resamples a valid kernel to the prepared host rate;
  • expands mono kernels to stereo without cross-talk;
  • uses only the first two channels of wider kernels before normalization;
  • contains non-finite kernel and input samples;
  • supports same-channel, cross-channel, full, and partial buffer aliasing;
  • clears unsupported channels and oversized remainders predictably;
  • allocates scratch during prepare(), not process();
  • uses a direct FIR for native-rate kernels shorter than 64 samples;
  • otherwise uses JUCE 8.0.8 convolution, including non-uniform partitioning for long responses;
  • fails silent when unprepared or called outside its prepared contract;
  • exposes latency, kernel length, and stable kernel identity.

PreparedConvolutionKernel::Kind already distinguishes room, cabinet, resonant-body, tonal, and procedural material. Those categories are metadata; the neutral engine does not change its mathematics based on kind.

Reverb policy

The first-party responses are generated deterministically during preparation:

  • spaceage.room.compact.v2: 0.48 seconds;
  • spaceage.room.deep.v2: 2.75 seconds.

They use fixed reflection times, fixed seeds, a deterministic LCG noise source, colouring, a controlled shared stereo component, exponential envelopes, and no recorded impulse-response assets. Version 2 was calibrated against external reference-room measurements without copying or redistributing those recordings.

Size continuously morphs between both immutable, continuously running room histories with a 20 ms equal-power interpolation. Damping, stereo width, and the 0-250 ms Pre-delay use the same automation-safe smoothing duration. Pre-delay uses a preallocated fractional delay line before both convolution engines. The first valid process call adopts restored host values immediately; only later changes are smoothed.

Damping is a post-convolution low-pass. Values below 1e-20 in its recursive state snap to exact zero to avoid denormals. Width uses a mid/side matrix; zero produces exact mono and one preserves the generated stereo return.

The wrapper remains wet-only. Mixer code owns return level and dry signal.

The shared return also has 12 dB/octave Input Low Cut and Input High Cut controls before convolution. This removes low-frequency mud or harsh excitation before it enters the room rather than EQing an already established tail. The 20 Hz and 20 kHz endpoints are true bypasses, preserving existing projects and avoiding filter cost at neutral settings. Active filters use fixed stereo state, calculate coefficients once per audio block, and allocate nothing on the audio thread. Reverb presets save both controls; presets created before their addition recall with neutral values.

Reverb Ducking uses the stereo send as its detector and attenuates only the wet return, with an 8 ms attack, 350 ms release, and up to 18 dB of reduction. The single amount control keeps the first workflow immediate while allowing the room to rise between phrases and hits. At zero, detector processing is skipped. Its gain trace is prepared with the audio block, performs no allocation in process, and is included in project and reverb-preset recall. Older presets load it at zero.

Bass Mono progressively centers only the wet return below approximately 180 Hz. It preserves the upper stereo field while reducing low-frequency side energy that can blur bass and kick placement or disappear unpredictably in mono playback. The crossover is a fixed, allocation-free one-pole pair with a smoothed amount; at zero its processing is bypassed and older presets recall it at zero.

Return Tilt rebalances the completed wet signal around approximately 1 kHz over a +/-6 dB range. Negative values emphasize warmth while reducing upper energy; positive values emphasize air while reducing low-mid weight. This is deliberately separate from Damping, which changes the tail's post-convolution low-pass. Gain targets are recalculated only when the control changes, smoothed for 20 ms, and the split filter is bypassed at zero. Projects and reverb presets retain the value; older presets recall it flat.

User impulse responses

The Reverb detail panel can load mono or stereo WAV/AIFF impulse responses and return to the calibrated built-in rooms. Loading validates the file, channel count, sample rate, a deliberate 30-second maximum duration, finite sample data, and non-silent content before touching the active engine. Wider files deliberately use their first two channels. User responses are normalized by the convolution backend.

Interactive decoding and custom-engine preparation run on one editor-owned background-priority worker, keeping long-response loading from freezing the interface and serializing repeated requests. The worker is joined during editor teardown. FFT preparation, resampling, normalization, and allocation finish before the audio callback is guarded; the guarded section only validates the unchanged host configuration and publishes the prepared engine. If the device changes during preparation, publication is refused and the user is asked to retry. Project-state restoration uses the same validated processor loader before the editor is interactive. A 50 ms crossfade moves from the continuously running built-in room pair to the custom response. The source kernel is retained so an audio-device sample-rate or block-size change rebuilds the custom engine instead of silently reverting to built-in rooms.

Each interactive request carries a monotonically increasing generation token. The token is checked before and inside the guarded publication section, so a superseded load cannot briefly replace a newer selection. Choosing built-in rooms invalidates pending loads and folder searches; stale completion messages are ignored.

Projects store the external IR path and its SHA-256 fingerprint, not the copyrighted audio. A valid file is restored and activated when the project opens; a missing or invalid path remains recorded for later repair while the wet engine fails safely to the built-in/cleared state. Relink mode verifies the selected file against the saved fingerprint and rejects a different response. The user can also select a folder tree to search recursively for the exact response; hashing runs on a background-priority worker and only an exact fingerprint match is accepted. Legacy projects without a digest remain loadable. No user IR is copied into release assets or project state.

The seven supplied Sony DRE777 hall responses are external calibration fixtures only. Each passes production decoding, 48-to-44.1 kHz project restoration, built-in fallback, and deliberate missing-path recovery; none is embedded or redistributed.

Factory room library

The Reverb detail panel includes 12 embedded rooms from Conner's IR Library, distributed under the MIT license. The browser groups them as Small, Medium, Large, and Historic. Previous and next controls move through the catalog without reopening the menu. Selecting a factory room loads the embedded audio directly, so it does not depend on a user file path and remains available after a project or reverb preset is moved to another machine.

Factory-room identity is stored separately from custom-file identity in project state and .ssreverb presets. Factory-to-factory changes retain the outgoing convolution history and use a 50 ms equal-power crossfade into the prepared new kernel. This avoids a discontinuity while preserving the old room's immediate tail during auditioning. Embedded responses use the same normalization and finite-output safeguards as custom responses.

Timing contract

The convolution backend reports zero processing latency, so the shared return does not delay the dry Mixer path and the processor must not declare plug-in latency for this effect. Both procedural rooms share the same first-reflection time (sample 341 at 48 kHz, approximately 7.1 ms), which keeps Size morphing phase-aligned at the onset. Pre-delay is additional acoustic delay: a requested 180 ms moves the first wet reflection by exactly 8,640 samples at 48 kHz in both 64- and 1,024-sample host blocks. The focused test locks these guarantees down.

The damping knob is converted to its filter coefficient once per process call; the coefficient itself is then smoothed in the audio loop. This preserves the settled response while removing per-sample power and exponential calculations. The exact equal-power Size curve is prepared as a 2,049-point table and linearly interpolated during processing instead of evaluating sine and cosine per sample. On the focused 96 kHz stress render, these changes reduced median load from a 0.497x baseline to 0.440x best observed and 0.453-0.455x clean verification runs, approximately 8.5-11.5 percent lower depending on machine contention.

The partitioned backend is treated as an untrusted numerical boundary. Input samples outside +/-8 are removed before entering its history, and an output block containing NaN, infinity, or a wet sample outside +/-8 is discarded before it can reach the Mixer. The focused suite deliberately feeds an excessive response, and the product shared-effects gate rejects Reverb disable-fade RMS at or above unity.

The original intermittent runaway was traced to the fractional Pre-delay ring, not JUCE convolution. Float rounding could turn a wrapped negative read position into exactly the buffer capacity, producing a one-past-end read. Depending on the adjacent memory, 20 ms Pre-delay could become almost silent or produce enormous finite values. Read-position arithmetic now uses double precision and normalises both the lower and upper bounds before indexing. At 48 kHz, 20 ms now moves the first reflection from sample 341 to 1301 with identical 0.644371 impulse energy; twelve rapid fresh-instance trials retain substantial zero- and 20-ms energy with zero rejected blocks. Each engine retains JUCE's owned queue so VST3 module unload does not depend on static shared-queue destruction order.

Tail behavior

The runtime scheduler services the input callback plus the active maximum tail. For built-in rooms this is 3.0 seconds: 2.75 seconds of response plus up to 250 ms of Pre-delay. A custom response expands that budget to its decoded source duration plus 250 ms, up to the loader's 30-second response limit, so imported halls are not retired at the built-in-room boundary. That expanded budget survives host sample-rate or block-size re-preparation and returns to 3.0 seconds when the custom response is cleared. The scheduler uses the processor's validated internal sample rate, so direct self-tests and host/device transitions cannot accidentally reduce the countdown to one block. Across 8–384 kHz and 16–4096-sample blocks, the worst measured backend residue at retirement is approximately 1.8e-12 (about -235 dBFS), below the -200 dBFS operational-silence contract. JUCE reaches bit-exact zero within 40 ms in every tested configuration.

If processing pauses at retirement and the same instance is later reused without reset, stale pre-onset output measures about -172 dBFS, below both the -160 dBFS reuse ceiling and the 24-bit quantization floor. A real-time convolution reset is therefore not required merely to erase numerical residue.

Reusable reverb presets

The Reverb editor can save and load .ssreverb presets independently of a project. A preset stores Active, Size, Damping, Stereo Width, Pre-delay, Input Low Cut, Input High Cut, Ducking, Bass Mono, Return Tilt, Return, and the selected room source.

Built-in-room presets are self-contained. Custom-room presets store the external IR path and SHA-256 fingerprint, but never the audio itself. Recall verifies the fingerprint before publishing the prepared convolution engine. If the IR has moved or is offline, the musical controls are restored and the exact missing reference is retained so relink and folder search can recover it without silently substituting another file.

The external-IR regression serializes and parses the production preset schema, requires all eleven current musical parameters, rejects missing or out-of-range values and malformed custom fingerprints, proves an exact fingerprint can be recalled independently of the previously selected IR, and refuses a mismatch without disturbing the active room. Version 3 is the current schema. Versions 1 and 2 remain accepted with their six original required controls; later controls receive documented neutral defaults.

Saved and successfully recalled .ssreverb files are also recorded in a persistent 16-entry Recent preset list. The Reverb panel's PRESET LIBRARY... menu exposes Recent and Favorite presets without reopening the operating-system chooser, and the active preset can be added to or removed from Favorites. Library selection uses the same validated asynchronous recall path as direct file loading, including factory-room preparation, custom-IR fingerprint checks, missing-IR retention, and newest-request-wins cancellation.

When two or more stored preset files remain available, < PRESET and PRESET > step through Favorites first and Recent second. Duplicate and missing paths are excluded. The effect-detail action area wraps onto additional rows when these preset controls coexist with custom-IR and factory-room controls, preserving usable button sizes at the compact supported viewport.

RESET CONTROLS restores all eleven Reverb parameters to their declared defaults without replacing or reloading the selected factory, built-in, or custom room. The operation is undoable and clears the active preset association because the edited controls no longer represent that saved preset.

Custom IR library

Successful custom-file loads and custom IR recalls from .ssreverb presets are recorded in a persistent 16-entry Recent list. The Reverb panel can mark the current custom response as a Favorite and can reload either Recent or Favorite entries without reopening the operating-system file chooser. Entries show both the file and parent-folder name. A moved or offline file remains visible but is disabled instead of being silently removed or substituted. The library stores paths only and never copies or embeds impulse-response audio.

The IR Library menu also provides explicit housekeeping. CLEAR RECENT HISTORY empties only the Recent list and preserves Favorites. REMOVE MISSING ENTRIES removes dead path references from both lists while retaining every file that still exists. Neither operation deletes, moves, or edits impulse-response audio.

When the active custom response belongs to a library containing at least two available files, < IR and IR > step through Favorites first and then Recent. The browse list is deduplicated and skips missing files. These changes reuse the background preparation generation guard, so rapid browsing cannot allow an older load to replace the user's newest selection when it finishes late.

The library state is written atomically beside the user's Reverb presets. Its round-trip schema and the expanded Reverb-panel layout are covered by the Effects detail regression at both supported viewport sizes.

Validation

External reference calibration and its non-redistribution boundary are recorded in docs/Convolution_Reference_Calibration.md.

Fast focused entry point:

powershell -NoProfile -ExecutionPolicy Bypass -File '.\tools\test-convolution.ps1'

CTest entry point:

ctest --test-dir '<build-directory>' -C Release -L convolution --output-on-failure

CTest registers the audio and convolution labels, runs serially to isolate the wall-clock watchdog, and has a 180-second timeout.

The focused suite covers exact FIR output, reset, normalization, resampling, kernel sanitation, channel canonicalization, aliasing, fixed and variable block segmentation, sample-aligned control automation, control startup, rapid Size automation, natural-tail settlement and paused reuse, failure states, sample-rate and block-size extremes, deterministic re-prepare, active 96 kHz performance, and an isolated dual-kernel reference budget. The latter retains separate Compact and Deep output-energy checks and prints the current two-engine realtime load, giving the planned shared-transform prototype a stable baseline and correctness target.

Product integration:

powershell -NoProfile -ExecutionPolicy Bypass -File '.\tools\run_tests.ps1' -Gate SHARED_EFFECTS_SIGNAL

That gate proves real sends/returns, Size/Damping/Width distinction, finite output, silence isolation, and runtime tail retirement in the complete processor.

Audition evidence

The focused executable can render deterministic 48 kHz/24-bit references:

powershell -NoProfile -ExecutionPolicy Bypass -File '.\tools\test-convolution.ps1' `
  -NoBuild `
  -RenderAuditionDirectory '.\outputs\convolution-audition' `
  -VerifyAuditionDeterminism

It writes a dry reference, five production-mix comparisons, and wet-only Compact/Medium/Deep files. Determinism mode renders twice and requires identical names, lengths, and SHA-256 hashes.

Licensing and release posture

  • Only explicitly reviewed, redistributable recorded IRs may be embedded. The 12 factory rooms from Conner's IR Library are MIT-licensed and recorded in the release asset manifest, third-party notices, bundled license, and source references.
  • Proprietary and calibration-only IRs are never embedded or redistributed.
  • Release hygiene rejects IR-like WAV, WAVE, AIFF, or FLAC asset paths unless the asset is deliberately reviewed and the guard is updated.
  • The procedural room source and stable identifiers are recorded in docs/First_Party_Asset_Provenance.md.
  • JUCE is the only convolution/FFT dependency and is pinned to the repository's approved 8.0.8 source fingerprint.
  • Third-party notices, source fingerprint, clean packaging, archive integrity, and provenance binding are enforced by release-hygiene scripts.
  • Outstanding release asset blockers are unrelated artwork/TG55 evidence, not convolution licensing.

Measured performance direction

The Basic Groove 5.sskit project profile at 48 kHz / 512 samples measured 12.16% with shared effects disabled and 18.32% with Reverb alone enabled. Delay alone measured 12.66%. Reverb is therefore a verified project-level CPU priority rather than a speculative one.

The existing mixer already retires the shared room after its bounded natural tail; the persistent active cost comes from Compact and Deep running as independent JUCE convolutions so Size can morph continuously between their outputs. Small buffer clear and shared-input-validation experiments did not produce repeatable gains and were rejected.

The next substantial optimization should evaluate a purpose-built dual-kernel partitioned convolver that computes the input FFT once per partition and applies both room spectra to that shared transform. Compact and Deep outputs must remain separate until the existing sample-aligned equal-power Size morph, preserving the current sound, automation, tails, and segmentation contract. This is an isolated DSP replacement candidate, not permission to change the room responses or quantize Size. It requires reference-render equality, hostile-input coverage, alternating compiled benchmarks, and whole-project confirmation before adoption. The first dedicated 48 kHz / 512-sample reference run measured 5.54% realtime load for the two synthetic long-room engines, with both output-energy assertions passing. That number is a fixture baseline, not a product CPU claim.

A test-only uniform-partition prototype now shares each channel's input transform between the Compact and Deep spectral accumulations. At full 512-sample blocks it matched the two JUCE reference engines within 1.21e-08 maximum sample error. Two compiled runs measured shared/reference ratios of 0.779 and 0.784, a repeatable isolated reduction of about 22%. The prototype performs no processing allocation.

It is deliberately not connected to the product yet. The prototype now recomputes only the current partial partition while retaining older spectral sums, allowing it to publish every requested sample immediately as host callbacks change size. A 90-callback irregular pattern matched the JUCE references within 5.32e-08, with no added latency; the same run retained a 0.787 shared/reference CPU ratio.

The shared-transform engine is now the production backend for the built-in Compact/Deep pair. It validates and sanitizes the common input once, validates both outputs, retains rejection counters, performs no processing allocation, and preserves reset, tail, Size-automation, and irregular-segmentation behavior. Custom and retiring custom rooms remain on the existing JUCE engine.

The production reference gate measured full-block and irregular maximum errors of 6.52e-09 and 4.66e-09, with a shared/reference CPU ratio of 0.761. The full focused suite passed. Two Basic Groove 5.sskit project runs measured Reverb-only at 16.19% and 15.95%, while the complete project measured 16.71% and 16.57%. The pre-change profile was 18.32% Reverb-only and 19.52% complete-project load.

The gate also exercises the production primitive directly with NaN and infinity input, requiring sanitized silence and one rejected-input diagnostic. An exercised engine reset must then match a fresh engine sample-for-sample; the current reset difference is exactly 0. The obsolete test-only prototype implementation was removed so benchmarks and safety checks now exercise production code exclusively.

Adaptive callback policy

The shared engine uses a fixed 512-sample partition. A production host matrix found shared/reference ratios of roughly 1.78-1.84 at 64 samples, 1.04-1.08 at 128, 0.86-0.96 at 256, 0.71-0.77 at 512, 0.72-0.73 at 1024, and 0.70-0.71 at 4096. Recomputing a 1024-point partial transform dominates at very small callbacks.

ConvolutionReverb therefore retains the independent JUCE Compact/Deep engines below a 256-sample prepared callback and selects the shared-transform engine at 256 samples and above. A focused assertion verifies both sides of this boundary. This preserves low-latency performance while retaining the measured 512-sample project improvement. A future shared non-uniform head/tail design may revisit the small-buffer path, but the current adaptive policy is the production contract.

The current 48 kHz stereo Compact/Deep fixture reports 9,289,728 bytes of prepared shared-engine storage, about 8.86 MiB. This includes transformed kernels, input-spectrum history, overlap buffers, working spectra, and input scratch. The focused gate requires nonzero storage below 16 MiB so future room-length or partition changes cannot silently cause unbounded per-instance growth.

Compact and Deep spectral tail traversal was also tested as one fused loop. It preserved reference output and passed the complete convolution suite, but five-run candidate and reverse-compiled baseline ranges overlapped, with only about a 1.1% median difference. The simpler separate accumulation loops remain. The focused test executable now accepts --benchmark-dual-kernel for future exact structural experiments.

Shared non-uniform prototype boundary

Prepared partition diagnostics report 45 Compact and 258 Deep partitions at 48 kHz with the current 512-sample uniform partition. Deep therefore owns about 85.1% of partition accumulation. A bounded model with eight 512-sample head partitions per room and 2048-sample tails reduces the modeled accumulation count from 303 to 89 equivalent partitions, about 70.6%.

That number is an upper-bound cost-model signal, not a measured CPU claim. Larger tail FFTs add their own forward/inverse transform work. The prototype must:

  • keep the first 4096 response samples on the existing zero-latency head;
  • process each remaining response at 2048-sample tail partitions;
  • schedule tail work using the head interval so output timing and reported latency remain unchanged;
  • preserve separate Compact and Deep outputs through the Size morph;
  • allocate and transform all kernels before audio processing;
  • match the existing engine under fixed and irregular callback segmentation;
  • retain hostile-input containment, reset equality, bounded memory, and tail reuse;
  • beat the uniform engine by a repeatable margin at 256, 512, and 1024 samples before production integration.

The existing adaptive JUCE fallback below 256 samples remains the contract while this prototype is evaluated.

The first preparatory attempt made partition size runtime-configurable in the current shared engine. Although all reference and lifecycle contracts passed, the fixed-512 benchmark regressed repeatably from roughly 0.77-0.79 of the JUCE pair to 0.865-0.876. Restoring compile-time constants measured 0.760. Therefore the prototype must use separate compile-time-specialized 512 and 2048 stages; it must not generalize the production hot loop merely to support the experiment.

The shared implementation is now factored as an internal compile-time template. Production instantiates only the original 512-sample specialization. The focused convolution target additionally compiles a 2048-sample tail specialization behind SPACEAGE_CONVOLUTION_PROTOTYPE; normal application targets do not expose or instantiate it.

An aligned long-kernel reference fixture compared the 2048 specialization directly with the production 512 engine at 2048-sample callbacks. Maximum output difference was 1.07e-08. Five repeated prototype/reference CPU ratios were 0.507, 0.527, 0.519, 0.518, and 0.528, showing a stable 47-49% reduction for this tail-only upper-bound workload. Prototype storage was about 9.25 MiB and remained bounded.

This does not yet prove the complete non-uniform engine. The next milestone is a zero-latency scheduler that combines the 512 head with delayed 2048 tail output, then proves fixed and irregular callback alignment before CPU comparison.

That scheduler now exists in the focused test target. It keeps the first 4096 kernel samples in the 512 specialization, accumulates arbitrary host callbacks into 2048-sample tail blocks, and publishes tail output through a preallocated future-output ring at the original 4096-sample kernel offset. No tail work is run until a complete 2048-sample input block is available.

Aligned output differs from the uniform engine by at most 9.31e-09. A repeating irregular callback pattern from 11 through 401 samples differs by at most 5.12e-09. Five complete candidate/reference ratios were 0.822, 0.815, 0.823, 0.792, and 0.826, a repeatable 17-21% reduction including head, tail, delay-ring, and summation work. A complete focused-suite run measured 0.756 and passed every existing convolution contract.

The prototype remains test-only. Before promotion it still needs explicit reset and hostile-input tests at the combined scheduler boundary, host-size timing at 256/512/1024, and a whole-project comparison.

The combined scheduler boundary now sanitizes hostile input into preallocated scratch, tracks rejected blocks, and resets both engines, a partially accumulated tail block, the absolute timeline, and every queued future sample. A hostile-input fixture passes with silent finite output, and an exercised reset matches a fresh scheduler exactly across fourteen subsequent 512-sample blocks.

The dedicated host matrix also passes with maximum output difference 7.92e-09. Candidate/uniform ratios are 0.846 at 256 samples, 0.816 at 512, and 0.832 at 1024. Safety and host-size fixtures are mandatory in the complete focused suite. The scheduler is now implemented as the production DSP class DualKernelNonUniformConvolutionEngine. Its timeline uses a 64-bit sample counter, and its head, tail, scratch, and future-output storage are all prepared before audio processing. ConvolutionReverb can select it once during preparation through a diagnostics-only preference; only one built-in backend is retained in memory.

The mandatory scheduler fixtures now exercise that production class rather than a private test copy. The complete focused suite passes with 9.31e-09 aligned and 5.12e-09 irregular-callback differences, exact reset equality, hostile-input containment, and a measured host matrix of 0.780, 0.797, and 0.805 at 256, 512, and 1024 samples. The normal shared-effects signal regression also passes.

Basic Groove 5.sskit initially measured 18.32% for the current Reverb backend and 16.07% for the non-uniform backend, a 12.3% reduction. A reverse-order repeat measured 16.50% and 16.32%, only a 1.1% difference. Because the end-to-end margin was not repeatable under the current machine load, the existing shared engine remains the default. The non-uniform implementation and profiler switch are retained for a cleaner paired promotion run; no user-visible behavior changed.

The project profiler now supports an explicit 1-60 second measurement window and an in-process paired Reverb mode. Paired mode alternates fresh current/candidate processor instances in both orders, reducing executable-startup drift. A 20-second pair measured current/candidate at 16.29%/14.64% in the first order, then candidate/current at 16.57%/15.93% in reverse. The candidate average was about 3.1% lower, but it lost the reverse pair and raised callback P95/P99 pressure. This confirms that the 2048 tail saves aggregate work while concentrating some of that work into periodic callbacks; it is not yet a safer realtime default.

A 1024-sample tail compromise was also evaluated. Correctness remained within 1.63e-08, but candidate/uniform host ratios were 0.982, 1.000, and 0.997 at 256, 512, and 1024 samples. It failed the mandatory performance gate and was fully reverted. The restored 2048 implementation passes the complete suite with host ratios of 0.829, 0.808, and 0.821.

The focused host matrix now records per-callback timing distributions as well as aggregate work. With the restored 2048 tail, 256- and 512-sample candidate P95 costs were about 1.6-1.9 times the uniform engine despite aggregate ratios near 0.83. At 1024 samples the candidate was approximately 17-20% cheaper overall, while P95 ranged from 0.97 to 1.12 of the reference across runs.

A narrow automatic-selection trial therefore enabled the non-uniform engine only at 1024 samples or larger. The mandatory suite passed and explicitly proved the 128/256/512/1024 ownership boundary. In a 20-second paired Basic Groove 5.sskit profile, however, one order improved from 16.36% to 15.37%, while the reverse order regressed from 17.12% to 21.34% and produced a larger callback maximum. Automatic selection was fully reverted. The threshold and backend remain available only when the diagnostics preference explicitly enables them.

The 2048 tail now has a staged execution mode for host blocks up to 1024 samples. It uses the existing 4096-sample response offset as scheduling lead: the forward transform, Compact accumulation/inverse transform, and Deep accumulation/inverse transform execute on separate callbacks. The tail history advances only after all three stages finish, and accumulating and active tail blocks retain independent 64-bit timestamps. Larger host callbacks retain the immediate proven path.

This removes the concentrated-work problem without duplicating FFT history. In the focused matrix, staged candidate/uniform aggregate ratios measured approximately 0.80, 0.79, and 0.79 at 256, 512, and 1024 samples. P99 and maximum callback ratios were below the uniform reference at every size; P95 ranged from 0.83 to 1.09 across repeated runs rather than the former 1.6-1.9 range.

The production staged class matches uniform output within 7.92e-09 for fixed callbacks and 8.85e-09 for an irregular 11-401-sample callback sequence. Hostile input containment and exact reset equality remain intact. The mandatory ownership test proves the default remains uniform while explicit opt-in can select staged non-uniform processing at 256 samples.

A corrected 20-second Basic Groove 5.sskit pair at 512 samples produced mixed orders: candidate lost 16.08% versus 15.31%, then won 14.19% versus 16.78%. Its average was lower, but the ordering disagreement still blocks default promotion. The staged backend is retained, validated, and opt-in while the normal application continues to choose the steady shared-uniform backend.

That final default decision was superseded after adding a true lockstep project profiler. Lockstep owns current and staged processors simultaneously, restores the same project into both, isolates Reverb identically, starts both transports from the same point, and alternates which processor runs first on every callback. This removes the sequential-profile drift that had reversed earlier comparisons.

Three 20-second lockstep runs of Basic Groove 5.sskit at 512 samples measured candidate/current ratios of 0.888, 0.894, and 0.886, a repeatable 10.6-11.4% reduction in whole-project Reverb workload. Candidate P95 and P99 were lower in all three runs. Maximum callback load was lower twice and only slightly higher in the third run.

The staged non-uniform engine is therefore the production default for supported host blocks of 256 samples or larger. Blocks below 256 retain the JUCE fallback. The uniform shared engine remains available through the preparation preference for diagnostics and rollback. Mandatory ownership tests prove default staged selection at 256/512/1024 and explicit uniform opt-out at 256.

The complete focused convolution suite passes after promotion. The full shared- effects signal regression also passes with the production default, including Size, damping, width, pre-delay, input cuts, ducking, bass mono, tilt, tails, bypass fades, silence, custom rooms, and finite output.

Post-promotion profiling confirms Reverb remains the largest isolated shared-effect cost in Basic Groove 5.sskit: 15.04% Reverb-only versus 10.33% with shared effects off. EchoRay-only measured 10.58%. Pre-delay and output-stage microbenchmarks are negligible, so the remaining cost is spectral convolution accumulation.

A trial adopted JUCE's packed real/imaginary spectrum and four portable vector operations inside the existing engine. It was faster, but incompatible with the current history and overlap representation: reference differences reached roughly 0.02-0.04, and multiple audible-contract gates failed. The trial was fully reverted. Any future packed-SIMD design must convert kernel storage, history, inverse reconstruction, and overlap handling as one coherent representation.

That coherent conversion is now complete. A mandatory primitive compares scalar complex multiplication with the packed representation and reports an exact maximumDifference=0. Kernels and input history are packed immediately after their forward transforms, all partition accumulation stays packed, and each sum is restored to a complete conjugate-symmetric spectrum immediately before its inverse transform. Overlap buffers remain ordinary time-domain samples.

The complete focused suite passes with this representation. Fixed-engine output differs from the independent reference by about 8.15e-09, irregular staged output by about 8.85e-09, reset remains exact, and hostile-input containment and all lifecycle tests remain intact. At 512/1024/4096-sample host blocks, the shared fixed engine measured approximately 0.63/0.60/0.60 of the reference engine's time.

A fresh same-run Basic Groove 5.sskit profile measured 12.606% with shared effects off and 15.794% with Reverb only. The resulting 3.188 percentage-point Reverb increment is about 32% below the preceding 4.71-point increment. Absolute loads from separately timed runs are not treated as directly comparable because machine load moved between sessions.

Packed persistent storage now matches that representation. Kernel partitions, input-spectrum history, and cached tail sums retain only FFT size + 1 values; temporary forward/inverse work areas remain at 2 x FFT size. Estimated shared- engine storage fell from 9.29 MB to 7.16 MB, about 23%, without changing output. Three focused runs produced shared/reference ratios of 0.658, 0.645, and 0.679; the final full-suite pass measured 0.655 with the same 8.15e-09 reference difference.

A nested-validation bypass was also evaluated because the non-uniform owner and its internal fixed engines inspect some of the same blocks. Repeated host-matrix runs did not show a dependable improvement, so the bypass was removed. Public and internal validation remain explicit.

The packed layout also prompted a fresh production-default audit because it accelerates both uniform and non-uniform engines. Three new 20-second lockstep Basic Groove 5.sskit runs at 512 samples measured staged/uniform ratios of 0.924, 0.914, and 0.919. Staged P95 and P99 were lower in all three runs. The non-uniform engine therefore remains the supported-block production default; its advantage has narrowed from the pre-packed 10.6-11.4% range to approximately 7.6-8.6%, but remains repeatable at whole-project scale.

A packed-history Compact/Deep fusion traversed shared history once and applied both room kernels while each input spectrum was cache-hot. It remained exact, but five focused ratios centered near 0.663, effectively the same as the separate- loop baseline near 0.658. The fusion was removed and the simpler loops remain.

A diagnostic-only --benchmark-packed-stages fixture now separates one representative 2048-sample tail workload into JUCE forward FFT, spectrum packing, 89 packed partition accumulations, inverse restoration, JUCE inverse FFT, and overlap-add. Five corrected runs placed packed accumulation at 35.3-36.7% of the measured work. Forward and inverse FFT calls together occupied about 62-63%; pack plus restore were roughly 1.4%, and overlap was negligible. The next substantial engine experiment should therefore compare FFT backends, not further hand-tune the conversion or overlap loops.

PFFFT commit 8c6b35e77624ddef0b3362c546a50b101c379bef is now vendored under Tests/ThirdParty/PFFFT for ConvTest only. Its UCAR/FFTPACK-style permissive terms permit source and binary redistribution with notices and non-endorsement conditions, and its documented SIMD paths include x86 and ARM/NEON with scalar fallback. It is not linked into Standalone, VST3, SampleSquadAudioTest, or release packages. The third-party register and retained license identify this test-only boundary.

The pinned source builds with the same MSVC 19.44 Release toolchain used by SpaceAge, and its own real-transform validator passes against FFTPACK. More importantly, --benchmark-pffft-comparison now compares JUCE and PFFFT using the same deterministic buffers, transform sizes, copy costs, normalization, and 400-iteration hot loop. Across five runs, PFFFT/JUCE round-trip timing ratios were 0.033-0.039 at 1,024 points and 0.033-0.040 at 4,096 points. Backend output difference remained at or below 4.10e-08, with both round trips near 3.35e-08 to 4.47e-08 error. This is compelling transform-level evidence, not an end-to-end Reverb gain. The next gate is a complete test-only partitioned convolution adapter using PFFFT's native unordered spectra and accumulation.

The first convolution-level gate is now mandatory. It zero-pads a deterministic 512-sample input and 257-sample kernel to 1,024 points, performs unordered forward transforms, calls pffft_zconvolve_accumulate, applies inverse scaling, and compares every output sample with direct linear convolution. Maximum difference is 1.40e-09. This proves the native layout, multiplication, and scaling contract; it does not yet prove partition history, overlap scheduling, or dual-room output. The complete existing convolution suite passes with this gate enabled.

A second mandatory test-only gate now mirrors the production 512-sample uniform partition scheduler. It retains kernel spectra, circular input-spectrum history, cached historical tails, partial-partition recomputation, overlap-add state, and reset behavior in PFFFT's native unordered representation. Both procedural room kernels and both stereo channels match the current shared engine within 1.86e-08 for complete blocks, 1.68e-08 for an irregular 7-512-sample callback pattern, and 1.86e-08 after reset replay.

Five focused complete-block runs measured PFFFT/current-engine ratios of 0.100, 0.118, 0.100, 0.103, and 0.106; the full-suite run measured 0.110. The fixture performs both rooms and both channels on each backend. The PFFFT candidate currently pays for four separate input transforms, so the result does not depend on a shared-transform optimization. This is strong end-to-end uniform partition evidence, but not a whole-Reverb claim. The staged 2048 tail and saved-project profile remain required before production integration.

The staged-tail and combined-scheduler gates are now implemented in test code. A 2,048-sample PFFFT tail matched the current tail prototype within 1.68e-08 for fixed and reset rendering and 1.49e-08 for irregular callbacks. Five focused timing ratios ranged from 0.178 to 0.208; the complete-suite run was 0.274. The complete test-only non-uniform scheduler combines a 512-sample head, a 2,048-sample tail beginning at sample 4,096, and both stereo room kernels. It matched the production scheduler within 8.90e-08 under irregular callbacks. Across five focused runs its total-time ratio was 0.153-0.166, its P95 ratio was 0.281-0.309, and its maximum-callback ratio was 0.506-0.806. The complete suite measured 0.182, 0.327, and 0.550, respectively, and passed.

These results completed the original test-only scheduling evidence. Subsequent work added production ownership, hostile-input containment, release-license packaging, and inactive shipping-target builds. Runtime activation still requires the production combined scheduler, bounded diagnostics, and a paired saved-project profile.

Production backend selection now uses one explicit BuiltInRoomBackend state instead of two coupled booleans. The current values identify separate JUCE, shared-uniform JUCE, and shared-non-uniform JUCE processing. Existing convenience queries remain available, but reset, processing, rejection counters, and latency all route through the single state. The host matrix verifies separate JUCE at a 128-sample preparation size, shared non-uniform JUCE at 256 samples and above, and shared-uniform JUCE when non-uniform processing is explicitly declined. This is the production adapter seam for PFFFT. Shipping targets now compile and link the inactive implementation, but do not select it at runtime.

The pinned PFFFT source and license are now also present under Source/ThirdParty/PFFFT. All six files have exact SHA-256 receipts in the dependency manifest, and the retained license is registered as both source inventory and notice evidence. This is intentionally a source-readiness step: Standalone, VST3, and the monolithic audio test now compile and link these files. Explicit JUCE fallback and shipping-target validation pass; runtime selection remains disabled pending the combined production scheduler.

Convolution-synth continuation seam

Production PFFFT transform adapter

SpaceagePffftBackend now owns prepared real forward and inverse transforms plus native unordered-spectrum convolution accumulation. Preparation owns setup and scratch storage; DSP operations allocate nothing. ConvTest compiles this adapter and the production vendored source. Its mandatory direct-convolution comparison passes within 1.40e-09. Standalone and VST3 compile the inactive backend but do not select it yet.

The test-only partition scheduler no longer owns raw PFFFT setups or calls. Its 512-sample head and 2,048-sample tail now use SpaceagePffftBackend for every kernel transform, input-history transform, spectral accumulation, and inverse. Uniform, tail, and combined non-uniform gates retain maximum differences of 1.86e-08, 1.68e-08, and 8.90e-08. Invalid transform sizes and null DSP pointers are contained. The complete convolution suite passes.

The production selector now reserves sharedNonUniformPffft and exposes an explicit PFFFT preference plus fallback receipt. Until the production scheduler declares itself ready, a request deterministically selects shared non-uniform JUCE instead. A paired 24-block impulse render with Size, damping, width, and pre-delay active produced exactly zero difference between normal JUCE selection and PFFFT-requested fallback. This prevents partial integration from producing silence or a different room response.

Future convolution synthesis should build beside ConvolutionReverb, not inside it. Reuse PreparedConvolutionKernel and ConvolutionEngine; introduce a separate owner for excitation and kernel generation.

Likely synthesis directions that preserve this boundary:

  • convolving an oscillator, noise burst, sample, or live input with a tonal or resonant-body kernel;
  • deterministic procedural kernels derived from harmonic partials, modal bodies, filtered noise, or user-authored envelopes;
  • offline kernel preparation and immutable audio-thread publication;
  • click-free kernel changes using two prepared engines and a bounded crossfade;
  • explicit gain policy outside the neutral engine so normalization does not erase intentional resonant energy;
  • stable kernel identifiers and provenance records compatible with presets, project restore, reproducible audition renders, and release manifests.

Do not make kernel synthesis, FFT allocation, file decoding, network access, or license decisions on the audio thread. Do not overload Reverb Size or its room responses with synthesis semantics.

Remaining product decisions

Automated work cannot choose the preferred room character. Listening should decide:

  • whether Compact and Deep decay shapes fit SpaceAge's source material;
  • whether the 50% morph has the desired apparent room volume;
  • whether early reflections mask transients at normal send levels;
  • whether maximum damping is dark enough without sounding closed;
  • whether full width remains convincing on headphones and mono playback;
  • whether control labels, defaults, and return gain need product tuning.

Those are listening and product decisions, not unresolved correctness defects.

Production PFFFT partition engine (2026-08-29)

SpaceagePffftBackend now also owns the reusable partition-history convolution engine used by the PFFFT validation scheduler. It supports the measured 512-sample head and 2,048-sample tail sizes, prepares all transform, history, overlap, and accumulation storage before processing, and performs no allocation inside process().

The former duplicate test-only partition class was removed. The uniform, long-tail, and combined non-uniform gates now instantiate the production class directly. The complete suite measured maximum differences of 1.86e-08, 1.68e-08, and 8.90e-08, respectively. In the same run, their timing ratios against the JUCE references were 0.116, 0.191, and 0.166.

A dedicated safety gate verifies rejection of empty kernels and unsupported partition sizes, containment of null DSP pointers and zero-length calls, channel clamping, reset safety, and nonzero prepared-storage reporting. The production runtime selector still falls back to JUCE because the combined head/tail scheduler owner, bounded scheduling diagnostics, and production integration are not yet complete. This change therefore does not alter shipped sound or project state.

Production PFFFT combined scheduler (2026-08-29)

PffftNonUniformConvolution now production-owns the stereo dual-room scheduler: a 512-sample head covering the first 4,096 kernel samples, a 2,048-sample tail, preallocated delayed-output rings, bounded callback handling, input sanitization, output containment, reset, rejection counters, and storage reporting.

The mandatory combined gate now instantiates this production class directly. It matched the current JUCE non-uniform engine within 8.90e-08; the latest complete suite measured a total-time ratio of 0.176, P95 ratio of 0.316, and maximum callback ratio of 0.492. Windows Release audio-test, Standalone, and VST3 targets compile and link it successfully. Runtime selection remains on JUCE until direct ConvolutionReverb integration and fallback/profile gates are completed.

Controlled PFFFT runtime integration (2026-08-29)

ConvolutionReverb can now select the production PFFFT scheduler when explicitly preferred and prepared with a supported shared-room block size. The ordinary default remains JUCE. Unsupported low-latency preparation and any PFFFT preparation failure retain the existing JUCE path and record a fallback receipt.

Reset, processing, zero-latency reporting, input/output rejection counters, room morphing, damping, width, pre-delay, and custom-room transitions now recognize the PFFFT backend state. A 24-block controlled reverb render differed from JUCE by 4.47e-08. The selector gate proves successful PFFFT activation and deterministic JUCE fallback when PFFFT preparation is unavailable. The complete convolution suite and Windows Release audio-test, Standalone, and VST3 builds pass.

The default must not change until the paired saved-project CPU profile confirms a meaningful whole-project benefit and host smoke is repeated with PFFFT selected.

PFFFT supported-size default (2026-08-29)

Four lockstep Basic Groove 5.sskit comparisons at 48 kHz/512 samples measured PFFFT/JUCE whole-project load ratios of 0.825, 0.826, 0.824, and 0.832. The corrected post-default run measured 15.68% JUCE versus 13.04% PFFFT, with P95 19.48% versus 16.77%, P99 20.92% versus 18.46%, and maximum callback 36.59% versus 22.66%.

PFFFT is therefore the default built-in-room backend for supported shared-room block sizes. JUCE remains the deterministic fallback for preparation failure and remains explicitly selectable for diagnostics. Project and preset formats are unchanged. The complete convolution suite and Windows Release Audio Test, Standalone, and VST3 builds pass after the default change.

The obsolete test-local combined scheduler has now been removed. All active PFFFT partition and non-uniform tests execute the production classes directly, leaving one implementation of scheduling behavior. The post-cleanup complete suite passes; the focused active-reverb load measured 2.14%. Full release hygiene also passes with 12 dependencies, 32 dependency receipts, 12 notices, 629 factory presets, and the expected preset fingerprint.

64-sample PFFFT promotion (2026-08-29)

The production PFFFT scheduler's host-facing block contract permits any positive size through 4,096 samples; its transform work remains staged internally at 512 and 2,048 samples. The prior 128-sample selector floor was therefore conservative, not a scheduler requirement. Supported shared-room selection now begins at 64 samples.

On Basic Groove 5.sskit, five 64-sample PFFFT runs produced a 15.034% median full-project load versus the preceding three-run 22.887% separate-JUCE median. Reverb-only moved from 21.844% to 14.258%. The complete convolution suite, shared-effects signal, focused-effects performance, and audio-safety continuity pass with the promoted boundary.

A paired 64/128-sample project split showed that most remaining low-latency overhead is processor-wide callback frequency. After dry-project cost is removed, Reverb contributes approximately 0.53 percentage points to the observed gap. This does not justify returning to the separate JUCE room backend, whose measured 64-sample cost was substantially higher.