SpaceAge Convolution Architecture
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/.cppPreparedConvolutionKernelcarries samples, source rate, stable identifier, semantic kind, and normalization policy.ConvolutionEngineis a wet-only stereo convolution primitive.ConvolutionReverbowns 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(), notprocess(); - 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.