particle_voice/nn/kernels/articulatory/tract_waveguide

Summary: tract_waveguide.js — Phase 2 ParticleNN articulatory kernel. Production Kelly-Lochbaum single-delay-per-section digital waveguide vocal tract, generalizing `risk/KellyLochbaumWaveguide.js` (R6)'s proven `createVocalTract().step()` recurrence to run entirely on GPU across a whole chunk of samples and N simultaneous voice/capsule instances. Junction reflection coefficients (`k[i] = (A_i - A_{i+1}) / (A_i + A_{i+1})`) are computed CPU-side by the caller via `risk/KellyLochbaumWaveguide.js`'s `reflectionCoefficients` (a fixed-shape, typically-small computation — same rationale `mel_filterbank.js` gives for precomputing its filter matrix CPU-side rather than reproducing it in WGSL) and passed in as `junction_k`, not recomputed here. Constriction noise injection (`constriction_index` + `constriction_noise`) and the nasal TAP signal (`nasal_tap_index`) ARE implemented, as faithful ports of R6's proven `step()` options — they were deferred when this kernel was first written, but `ParticleTract.js` cannot produce real fricatives or real nasal coupling without them: - `constriction_index >= 0` adds that sample's `constriction_noise` value to the RIGHT-going wave entering junction `constriction_index`, exactly as R6's `if (i === constrictionIndex) incomingRight += constrictionNoiseSample`. Injecting at the junction (rather than mixing noise into `source_flow` at the glottis) is what makes a fricative a fricative: only the cavity IN FRONT of the constriction filters the turbulence. - `nasal_tap_index >= 0` writes `right[i] + left[i]` for that section into the packed output's `nasal_tap` block every sample, giving `nasal_junction.js` an anatomically-meaningful `input_flow` to be driven by. Matching R6 exactly, the tap is a READ-ONLY observation: the oral tract is not itself perturbed by it. That remains a simplification versus a rigorous 3-port scattering junction (R6's own docstring says the same); it is enough for controllable, stable nasal coupling, and the honest limitation is recorded here rather than overstated. Both are opt-in: a negative index disables that feature entirely, so callers that want neither pass `-1` and get bit-identical behavior to this kernel before they existed. ## `radiation_difference` must match what the SOURCE already is Lip radiation is approximately a differentiator, so R6's `step()` ends with `radiated = outputRaw - prevOutputRaw`. That is correct when the excitation is glottal FLOW, which is what R6's `GlottalSource.js` raised-cosine pulse produced. It is WRONG when the excitation is already the flow derivative. `glottal_lf.js` is explicitly the LF glottal flow DERIVATIVE model, so feeding it in and then differencing again yields the SECOND derivative: +6 dB/octave of spurious tilt (audibly reedy/buzzy) and a far spikier waveform, whose large crest factor then drives any downstream limiter into distorting every single pitch pulse. Injecting a derivative source and skipping the difference is what gives the correct radiated pressure, and is why formant synthesizers use the LF derivative directly. `radiation_difference` therefore has to agree with the caller's source: 1.0 for a flow source (R6 parity), 0.0 for `glottal_lf.js`. There is no safe default that suits both, so it is an explicit parameter rather than a built-in assumption. ## `wall_loss`: the tube was perfectly lossless, and that is audible Scattering at a Kelly-Lochbaum junction conserves energy exactly, so with only the glottis and lips absorbing anything, the formants of this tube had very high Q. Real vocal tracts lose energy continuously to yielding walls plus viscous and thermal effects, which is what gives formants their bandwidth (roughly 50-100 Hz for F1, more higher up). A resonator with too little damping rings after every excitation, and a whole tube of them reads as a metallic, synthetic buzz laid over the speech no matter how accurate the formant FREQUENCIES are — accurate centres with no bandwidth still does not sound human. `wall_loss` multiplies each traveling wave once per section per sample, so a wave crossing the whole tract is attenuated `wall_loss^num_sections`. Because it is applied per section rather than once at a boundary, the loss accumulates with distance travelled, which is the correct behaviour for a distributed effect. It is a frequency-INDEPENDENT approximation of a loss that really does grow with frequency; the honest limitation is recorded here rather than overstated, and a one-pole filter per junction would be the refinement. 1.0 restores the lossless tube exactly, which is what the R6 parity test uses. One digital-waveguide step is inherently sequential in time (sample n depends on sample n-1's right/left traveling-wave state), but the N-1 junction updates *within* one sample step are independent of each other (each reads only the previous sample's `right`/`left`, matching R6's `step()` computing a full `nextRight`/`nextLeft` array before committing it) — so this kernel assigns one thread per tube section (`local_invocation_id.x`), keeps the whole tract's traveling-wave state in workgroup memory (double-buffered, since every junction read is the *previous* sample's state), and walks the chunk's samples sequentially with a `workgroupBarrier()` between samples. `workgroup_id.x` selects which of up to `num_instances` simultaneous tracts to process, matching every other Phase 2 kernel's per-instance batching convention (`breath_reservoir.js`, `lip_radiation.js`). `MAX_SECTIONS = 64` is a compile-time workgroup-memory cap, comfortably above R6's proven `NUM_SECTIONS = 32` oral / `NASAL_SECTIONS = 16` nasal tract discretizations (`risk/VoiceBoxWaveguideProbe.js`) — `num_sections` over this cap is a caller error, not a silently-truncated tract. To keep this kernel within the project's "N read-only storage inputs then one read_write storage output" binding convention (`KernelRegistry.bindGroupLayoutEntriesFor`), all four pieces of kernel-produced data (`radiated_out`, the two persisted traveling-wave state arrays, and the persisted previous-lip-output scalar needed for the next chunk's radiation first-difference) are packed into ONE flat output buffer at fixed uniform-computed offsets, documented in the struct comments below, rather than needing four separate output bindings.


tractWaveguideOutputLayout

Summary: Compute the flat-output-buffer layout `tract_waveguide.js` writes to and `TractWaveguideModel.js` (Phase 3+ consumer) must read from, given `numSections`/`numSamples`/`numInstances` — kept as one shared function so the WGSL offsets below and any future JS-side reader can never drift apart.


particle_voice/nn/kernels/articulatory/visual_state_decimate

Summary: visual_state_decimate.js — Phase 2 ParticleNN articulatory kernel. Decimates a high-rate, multi-CHANNEL tract field (e.g. a per-section pressure-like signal, one channel per tube section) down to one box-averaged value per channel per fixed-size frame — the `VisualAirflowState` `ARCHITECTURE.md` §4 names as "a decimated field for ribbon/particle/heatmap visualization". Deliberately NOT a reuse of `physiology_feedback_reduce.js` despite the similar per-thread "small sequential average over a window" shape: that kernel reduces a single FLAT per-instance signal to (mean, RMS) pairs for model feedback (a statistics reduction); this kernel decimates a multi-CHANNEL per-instance FIELD (extra channel/section axis) down to mean-only per channel per frame, because a rendered airflow ribbon/ heatmap needs a representative value per tube section over time, not an energy statistic, and box-averaging over the decimation window (rather than picking every Nth raw sample) avoids visual aliasing/flicker in the downsampled field — ordinary decimation-filter practice, and consistent with how `overlap_add.js`/`stft.js` already treat windowing as the caller's/kernel's job rather than raw strided subsampling. `ARCHITECTURE.md` §4 also requires that visualization "must be bit- identically disable-able without changing rendered PCM" — this kernel reads its input field only (never writes back into it), so skipping this kernel's dispatch entirely when visualization is off cannot affect the `tract_waveguide.js`/`nasal_junction.js` PCM path in any way; that invariant is structural (this kernel has no read_write access to anything the acoustic path also reads), not merely a runtime convention.


particle_voice/nn/kernels/chunks/complex_math

Summary: chunks/complex_math.js — Phase 1 ParticleNN kernel chunk. Generic complex-arithmetic WGSL fragment (mul/add/sub/twiddle), split out as reusable infrastructure ahead of Phase 2's `fft/fft1024_f32.js` / `fft/complex_mul.js` kernels — R3's `risk/Fft1024Kernel.js` inlined its own twiddle multiply directly in the butterfly loop (fine for a disposable risk spike); Phase 2's production FFT kernel should compose this chunk instead of re-deriving the same four multiplies/two adds. Complex numbers are represented as `vec2<f32>` (`.x` = real, `.y` = imag) throughout, not a named struct, to keep it directly usable as a return type/local without an extra struct declaration at every call site.


particle_voice/nn/kernels/chunks/f16_utils

Summary: chunks/f16_utils.js — Phase 1 ParticleNN kernel chunk. On the WGSL side, `f16` kernels use WGSL's native `f16` type directly (`enable f16;` + `f16`/`vec2h` etc.) when `shader-f16` is available — no bit-manipulation WGSL fragment is needed there, unlike `int8_pack.js`'s manual unpack (WGSL has no native `i8` storage type, but it DOES have a native `f16` type). This file's job is the *host-side* JS<->f16 bit conversion needed to build/verify f16 test vectors and read `ParticleTensor`'s raw `Uint16Array` f16 readback (`ParticleTensor.js`'s `wrapReadbackBuffer` deliberately leaves `f16` tensors as raw bit patterns — this is where they get decoded).


Summary: IEEE 754 binary16 -> JS float64.


Summary: JS float64 -> IEEE 754 binary16 bit pattern (round-to-nearest-even is not implemented precisely — round-half-away-from-zero on the mantissa, adequate for test-vector construction, not for production-quality quantization-aware training).


Summary: Decode a raw f16-bits `Uint16Array` (e.g. from `ParticleTensor.getData()`) into a `Float64Array`.


Summary: Encode a `Float32Array`/`Float64Array`/plain array into an f16-bits `Uint16Array`, suitable for `ParticleTensor` `data` with `dtype: 'f16'`.


particle_voice/nn/kernels/chunks/int8_pack

Summary: chunks/int8_pack.js — Phase 1 ParticleNN kernel chunk. WGSL side: the `unpack_int8` fragment, extracted from R2's `risk/MatmulW8A16Kernel.js` (proven correct there — R2's parity test passed with max abs/rel error ~3.4e-7 against a JS scalar reference) into a shared chunk so `matmul_w8a16.js`/`conv1d_w8a16.js`/ `dequant_int8.js` all use the exact same unpack logic rather than each re-deriving it. JS side: the host-side symmetric block quantizer that produces the `packed`/`scales` pair this WGSL fragment expects, matching `ParticleTensor.js`'s documented `quant` scheme: `value = i8Value * scale` (zero-point-free, one scale per `blockSize`-sized block).


Summary: Symmetric per-block INT8 quantization: for each contiguous `blockSize`-sized block of `values`, scale = max(abs(block)) / 127, quantized = round(value / scale). Returns `{ packed: Uint32Array, scales: Float32Array, blockSize }`. `values.length` must be a multiple of 4 (four int8 values pack into one u32, matching R2's little-end-is-lowest-index layout).


Summary: Dequantize `quantizeInt8Blockwise`'s output back to a `Float32Array`, for host-side parity checking against a kernel's GPU output.


particle_voice/nn/kernels/chunks/math_common

Summary: chunks/math_common.js — Phase 1 ParticleNN kernel chunk. Small reusable WGSL scalar-math fragments shared by multiple kernel generators, so e.g. `silu_f16.js` and any future fused-activation kernel define `siluScalar()`/`sigmoidScalar()` exactly once rather than each inlining its own copy (the kind of duplication the R2 spike's inline `unpack_int8` was an acceptable one-off for, but Phase 1 kernels should not repeat).


Summary: convolution/conv1d_w8a16.js — Phase 1 ParticleNN kernel. 1D convolution, INT8 weights (same 4-packed-per-u32 layout and `unpack_int8` as `matmul_w8a16.js`) x FP16/FP32 activations, FP32 accumulation. `[inChannels, kernelWidth]` per output channel, `'same'` padding (input implicitly zero-padded so output length equals input length) — stride 1 only; dilation/stride are Phase 2+ follow-ups if a consumer needs them (R7's toy Conv1D model used stride 1 only, and no Phase -1/0 document commits to anything else yet). Weight layout: `packedWeights[outCh * ceil(inChannels*kernelWidth/4) + k4]`, a u32 whose 4 bytes are int8 weights for the flattened `(inCh, tap)` index `k = k4*4 + 0..3`, where `inCh = k / kernelWidth` and `tap = k % kernelWidth`. `scales[outCh]` is the per-output-channel scale. `inChannels * kernelWidth` must be a multiple of 4.


conv1dW8a16Shader({ useShaderF16 = false } = {})


particle_voice/nn/kernels/core/transpose_2d

Summary: core/transpose_2d.js — Phase 3 ParticleNN kernel. 2D transpose: `out[c, r] = in[r, c]`, i.e. `[rows, cols]` → `[cols, rows]`, both row-major. ## Why this exists The Phase 1 kernels disagree about which axis is major, and building `PhonemeEncoder.js` is the first time anything composes them: - `speech/embedding_f16.js` writes `[numIds, embedDim]` — **sequence-major** (one row per token), the natural shape for a gather. - `convolution/conv1d_w8a16.js` reads `[inChannels, seqLen]` — **channel-major**, the natural shape for sliding a kernel along time. - `normalization/rmsnorm_f16.js` normalizes each ROW independently, so per-token normalization over the feature axis needs sequence-major again. Neither layout is wrong for its own kernel, but the encoder has to cross between them, and nothing in the registry could. Rather than duplicating a transposed variant of the convolution (or silently re-interpreting a buffer and producing scrambled activations), the axis swap is its own explicit, testable op. Deliberately naive: one thread per element, no tiling and no shared-memory staging. The encoder's tensors are small (tens of channels x hundreds of frames), so the uncoalesced write is not worth the complexity — and a tiled version can replace this later without changing a single caller, since the contract is just "swap the axes".


transpose2dShader({ useShaderF16 = false } = {})


particle_voice/nn/kernels/fft/complex_mul

Summary: complex_mul.js — Phase 2 ParticleNN DSP kernel. Elementwise complex multiplication: `(a+bi)(c+di) = (ac-bd) + (ad+bc)i`, over interleaved `[re, im, re, im, ...]` buffers, matching `fft1024_f32.js`'s interleaved-complex layout so FFT output can feed this directly (e.g. frequency-domain filtering: `FFT(x) * H` then `IFFT`).


particle_voice/nn/kernels/fft/fft1024_f32

Summary: fft1024_f32.js — Phase 2 ParticleNN DSP kernel. Single-workgroup, in-workgroup-memory, iterative radix-2 Cooley-Tukey forward FFT — generalizes `risk/Fft1024Kernel.js` (R3)'s proven butterfly network to run with **256 threads/workgroup** (each thread handles 2 butterfly operations per stage / 4 elements for load-scatter) instead of R3's 512, so it needs **no elevated `maxComputeWorkgroupSizeX` request** — every conformant WebGPU adapter guarantees 256 by default. This resolves the exact gap `spec/DeviceContract-v0.md` §4 flagged as an unresolved "Phase 1/2 follow-up": rather than requesting a higher limit with a fallback path for adapters that refuse it, this kernel simply never needs the higher limit. One workgroup computes exactly one 1024-point complex FFT; `workgroup_id.x` selects which frame of a batch to process (`dispatchWorkgroups(numFrames)`), matching R3's batching design. Complex numbers are interleaved `[re0, im0, re1, im1, ...]` in a single flat `array<f32>` per frame (`frame_base = wid.x * N * 2`) — one input buffer, one output buffer, matching every other Phase 1/2 kernel's single-input/single-output convention (unlike R3's separate real/imag arrays). No uniform buffer is needed (frame selection comes entirely from `workgroup_id`), so this kernel registers with `hasUniform: false` in `KernelRegistry.js`.


Summary: Shared by `fft1024_f32.js` and `ifft1024_f32.js` — `direction` (+1 forward, -1 inverse) is baked in at generation time, not read from a uniform, since each op is a distinct, fixed-behavior kernel.


particle_voice/nn/kernels/fft/ifft1024_f32

Summary: ifft1024_f32.js — Phase 2 ParticleNN DSP kernel. Inverse of `fft1024_f32.js`: same butterfly network (see that file's docstring for the full algorithm/limit-generalization rationale), with `direction = -1` baked in — conjugate the input, run the same forward butterfly network, conjugate + scale by `1/N` (`IFFT(X) = (1/N) * conj(FFT(conj(X)))`), matching R3's proven identity.


Summary: matmul/matmul_w8a16.js — Phase 1 ParticleNN kernel. INT8 weights (4-packed per u32, per-output-channel scale, `chunks/int8_pack.js`'s `unpack_int8`) x FP16 activations, FP32 accumulation, FP16 output — the real target this plan names, superseding R2's FP32-activation stand-in (`risk/MatmulW8A16Kernel.js`) now that R1 has proven `shader-f16` is available on at least one reference device. Falls back to FP32 activations/output when `useShaderF16` is false (DeviceContract-v0's `no-f16` tier) — same kernel body, different element type, not two separately-maintained kernels. Weight layout (unchanged from R2, already proven): logical weights are [K, N]; `packedWeights[n * (K/4) + k4]` is a u32 whose 4 bytes are the int8 weights for k = k4*4 + 0..3 (little end = lowest k); `scales[n]` is the per-output-channel dequantization scale. K must be a multiple of 4.


matmulW8a16Shader({ useShaderF16 = false } = {})


particle_voice/nn/kernels/normalization/rmsnorm_f16

Summary: normalization/rmsnorm_f16.js — Phase 1 ParticleNN kernel. Row-wise RMSNorm: `out[i] = x[i] / sqrt(mean(x^2) + eps) * weight[i]`. One workgroup per row; workgroup-shared reduction for the sum-of-squares (standard tree reduction, `WORKGROUP_SIZE` must be a power of two). Accumulates in f32 regardless of `useShaderF16`, matching `matmul_w8a16.js`'s "FP32 accumulation" convention — only the input/output storage element type changes with the feature flag.


rmsnormF16Shader({ useShaderF16 = false, eps = 1e-6 } = {})


particle_voice/nn/kernels/quantization/dequant_int8

Summary: quantization/dequant_int8.js — Phase 1 ParticleNN kernel. Standalone unpack+dequantize kernel: `matmul_w8a16.js`/`conv1d_w8a16.js` fuse unpack+dequant+multiply-accumulate inline for performance, but `WeightPageUploader.js`/tooling sometimes need a plain materialized dequantized tensor (e.g. for a CPU-side parity check, or a debug visualization) without running a full matmul — this is that kernel. Uses the same `chunks/int8_pack.js` `unpack_int8` and per-block scale scheme as `ParticleTensor.js`'s documented `quant` metadata.


dequantInt8Shader({ useShaderF16 = false } = {})


particle_voice/nn/kernels/quantization/pack_unpack

Summary: quantization/pack_unpack.js — Phase 1 ParticleNN kernel. The pack direction complementing `dequant_int8.js`'s unpack direction: a GPU kernel that bit-packs already-quantized int8 values (as `i32` inputs already rounded/clamped to [-127, 127] — computing the *scale* is a separate reduction pass, not this kernel's job) into 4-packed u32 words, matching `chunks/int8_pack.js`'s host-side `quantizeInt8Blockwise`'s packed-word layout exactly (so a GPU-side requantization pipeline and the JS host-side one are interchangeable).


Summary: Packs `array<i32>` (one already-quantized value per element, [-127,127]) into `array<u32>` (4 values per word). `count` must be a multiple of 4.


Summary: JS-side reference pack, for parity-testing `packInt8Shader` against a CPU implementation. Mirrors `chunks/int8_pack.js`'s packing loop exactly.


particle_voice/nn/kernels/speech/crossfade

Summary: crossfade.js — Phase 2 ParticleNN DSP kernel. Equal-power crossfade between two equal-length signals over their full length: `t = i/(length-1)`, `out = a*cos(t*pi/2) + b*sin(t*pi/2)` — equal-power (not linear) so the perceived loudness stays constant through the fade instead of dipping at the midpoint (linear crossfade's well-known flaw), matching the constant-power crossfade convention `ARCHITECTURE.md`'s reaction-system voice transitions call for.


particle_voice/nn/kernels/speech/embedding_f16

Summary: speech/embedding_f16.js — Phase 1 ParticleNN kernel. Row-gather embedding lookup: `output[i, :] = table[ids[i], :]`. Named under `speech/` (not `matmul/`) because its first production consumer is `PhonemeEncoder.js`'s phoneme-ID embedding (Phase 3), looking up rows out of `PhonemeSet-v0`'s fixed 42-entry table — but the kernel itself is generic over table/embedding-dim size. (That count read "51" until Phase 3: the spec's own summary line miscounted its vowel tables. `frontend/PhonemeSet.js` is the executable source of truth and exports `PHONEME_COUNT = 42`, which is what a caller should size `tableRows` from rather than any hard-coded literal.) Output layout is `[numIds, embedDim]` — SEQUENCE-major, one row per token. Note that `convolution/conv1d_w8a16.js` expects CHANNEL-major `[inChannels, seqLen]`, so composing the two requires `core/transpose_2d.js` in between; `PhonemeEncoder.js` does exactly that.


embeddingF16Shader({ useShaderF16 = false } = {})


particle_voice/nn/kernels/speech/harmonic_source

Summary: harmonic_source.js — Phase 2 ParticleNN DSP kernel. Voiced glottal pulse train — the "LF-lite" raised-cosine shape proven in `risk/GlottalSource.js` (R6): flow(n) = 0.5*(1-cos(2*pi*cyclePos(n)/openSamples))  for cyclePos(n) < openSamples flow(n) = 0                                           otherwise Unlike R6's per-sample sequential CPU generator (which tracks `cyclePos` as running state, updated one sample at a time), this kernel computes every sample **in closed form and fully in parallel**: `cyclePos(n) = (start_phase + n) mod period_samples`, using an **absolute sample counter** (`start_phase`) the caller persists and advances across chunk boundaries — exactly the "LF glottal source with absolute counter" the plan's checklist calls for, and mathematically identical to R6's per-sample recurrence for as long as `period_samples`/`open_samples` hold constant across the dispatched chunk (the same constant-until-the- next-period-boundary assumption R6's own generator already makes). Aspiration noise is NOT mixed in here (unlike R6's spike, which combined both for simplicity) — that is `noise_source.js`'s job, mixed downstream, since the operator allowlist treats voiced and unvoiced excitation as independent ops that a later stage combines.


particle_voice/nn/kernels/speech/mel_filterbank

Summary: mel_filterbank.js — Phase 2 ParticleNN DSP kernel. Applies a precomputed triangular mel filterbank matrix to a power/ magnitude spectrogram: `mel[f, m] = sum_k filterbank[m, k] * spectrum[f, k]` — a small matmul-shaped reduction, not a general matmul (the filterbank is fixed-shape and typically far smaller than `matmul_w8a16.js`'s tile sizes, so a dedicated one-thread-per-output-element kernel is simpler and avoids forcing an INT8-quantized weight path onto filter coefficients that are naturally small dense f32 matrices, not learned weights). The filterbank matrix itself (triangular mel-scale filters for a given sample rate/`n_mels`/`fmin`/`fmax`) is computed once on the CPU at model-load time — this kernel only applies it.


particle_voice/nn/kernels/speech/noise_source

Summary: noise_source.js — Phase 2 ParticleNN DSP kernel. Deterministic, chunk-boundary-independent aspiration/turbulence noise — the exact integer hash proven in `risk/GlottalSource.js` (R6)'s `noiseAt(absoluteIndex, seed)` (itself the same technique as `StreamingContinuitySource.js`'s counter-based noise): a two-round Murmur-style avalanche hash of `absoluteIndex ^ seed`, mapped to `[-1, 1)`. Counter-based (not a stateful PRNG stream) so any two chunks covering the same absolute sample index always produce the same noise value, regardless of dispatch order or chunk boundaries — required for glitch-free streaming synthesis.


particle_voice/nn/kernels/speech/overlap_add

Summary: overlap_add.js — Phase 2 ParticleNN DSP kernel. Synthesis overlap-add: reconstructs a continuous signal from overlapping `frame_size`-length windowed frames at `hop_size` stride, matching `risk/WolaSignals.js`'s proven `overlapAdd` reference. Implemented as a **gather**, not a scatter: each output thread owns exactly one output sample and sums every frame that overlaps it (`f*hop <= t < f*hop + frame_size`) — WGSL's core spec has no `atomic<f32>` add, so a scatter (`output[t] += ...` from many threads across overlapping frames) would race; gathering avoids that entirely with no atomics and no serialized per-frame dispatches needed. `component_stride` is the number of consecutive `f32` components per logical sample in `frames_in`, so this kernel can consume both layouts the pipeline actually produces: - `component_stride = 1` — flat real frames. - `component_stride = 2` — INTERLEAVED COMPLEX frames, which is exactly what `ifft1024_f32.js` emits. Only the REAL component is summed (the imaginary residue of an inverse-transformed real signal is numerical noise), making this the ISTFT synthesis stage directly, with no separate deinterleave kernel needed in between. Without this stride, `frames_in[f*frame_size + n]` reads the interleaved-complex buffer as if it were flat real — silently mixing real and imaginary components of the wrong samples together. That gap was found by the `tests/particle-voice/stft-istft-identity.html` integration test.


particle_voice/nn/kernels/speech/stft

Summary: stft.js — Phase 2 ParticleNN DSP kernel. The framing half of STFT: slices overlapping `frame_size`-length windows from a flat mono signal at `hop_size` stride (zero-padded past the signal's end), matching `risk/WolaSignals.js`'s proven `sliceFrames` reference exactly. Writes each frame's samples into the real channel of an interleaved-complex output (`fft1024_f32.js`'s layout) with the imaginary channel zeroed, so the output can be windowed (`window.js`) then fed directly to `fft1024_f32.js` — the actual FFT transform is a separate op (already in the operator allowlist), not duplicated here.


particle_voice/nn/kernels/speech/window

Summary: window.js — Phase 2 ParticleNN DSP kernel. Applies a Hann window in-place across framed audio: `out[f,n] = in[f,n] * 0.5*(1-cos(2*pi*n/(N-1)))`, matching `risk/WolaSignals.js`'s proven `hannWindow`/`applyWindow` CPU reference exactly (same coefficient formula) so R3's WOLA identity tests remain the ground truth this kernel is checked against. The coefficient is computed in-shader from the frame-relative index rather than read from a precomputed buffer — one fewer buffer to manage, and Hann's formula is cheap per-element. `stride` is the number of consecutive `f32` components per logical sample, so the same kernel windows both layouts this pipeline actually produces: - `stride = 1` — flat real frames (`[f0s0, f0s1, ...]`). - `stride = 2` — INTERLEAVED COMPLEX frames (`[re, im, re, im, ...]`), which is exactly what `stft.js` emits and what `fft1024_f32.js` consumes. Both the real and imaginary component of a sample get the SAME coefficient (the correct definition of windowing a complex signal), because `n` is derived from the logical sample index `i / stride`, not the raw component index. Without this stride, `n = i % frame_size` silently mis-assigns coefficients when handed interleaved-complex data (component index and sample index diverge by a factor of 2), so `stft.js`'s documented "output can be windowed (window.js) then fed directly to fft1024_f32.js" composition would produce a wrong window. That gap was found by the `tests/particle-voice/stft-istft-identity.html` integration test — each kernel passed its own parity test in isolation, but the composed WOLA round-trip did not reconstruct.


particle_voice/nn/memory/ActivationArena

Summary: ActivationArena.js — Phase 1 ParticleNN memory. Transient GPU-buffer pool for activation tensors, keyed by byte size: `acquire(tensorName, byteLength)` reuses a same-size buffer already released by an earlier tensor instead of allocating a new one, and `release(tensorName)` (driven by `TensorLifetimePlanner.freedAfter()`) returns a buffer to the pool the moment its last consumer has run.


particle_voice/nn/memory/GradientArena

Summary: GradientArena.js — Phase 1 ParticleNN memory. Transient GPU-buffer pool for gradient tensors during backward passes — identical pooling mechanism to `ActivationArena.js` (same size-bucketed acquire/release), given its own class identity/file per the plan's checklist rather than duplicated logic, since forward-pass activation buffers and backward-pass gradient buffers have separate lifetimes and should never be pooled together (an activation still needed for a backward computation must not be silently reused as a gradient buffer). Not wired into any executor until Phase 11 (autodiff), per the plan.


GradientArena

class GradientArena extends ActivationArena


particle_voice/nn/memory/TensorLifetimePlanner

Summary: TensorLifetimePlanner.js — Phase 1 ParticleNN memory. First/last-use analysis over a `ParticleProgram`'s step order: for every tensor name, which step produces it (or `-1` if it's an external input to the program) and which step is the last to read it. `ActivationArena.js`/ `GradientArena.js` use `freedAfter(stepIndex)` to know exactly when a buffer becomes free to hand to the next tensor that needs one, enabling buffer aliasing instead of one allocation per tensor per run.


particle_voice/nn/memory/WeightBindWindow

Summary: WeightBindWindow.js — Phase 1 ParticleNN memory. Storage-buffer bind group for a program segment's resident weight pages (up to 16 pages, per the plan's "never allocate one monolithic buffer over 128 MiB" rule) — one GPU buffer per page (not one big buffer for the whole window), matching `WeightPageStore.js`'s per-page granularity.


particle_voice/nn/memory/WeightPageStore

Summary: WeightPageStore.js — Phase 1 ParticleNN memory. Logical 16–32 MiB weight-page manifest: a page = a contiguous tensor set with its own SHA-256 hash, matching `PRNN-v0.md` §6's directory shape exactly (`{ index, byteLength, offset, hash }`) — the EXACT format R8 (`risk/WeightPagingProbe.js`) already proved works for interrupted downloads (retry, not silent data loss) and corruption detection (hash mismatch actually caught, not tautological). Network fetch is injected (`fetchPage(pageIndex, directoryEntry) => Promise<ArrayBuffer|TypedArray>`) rather than hardcoded to `fetch()`, so this store is testable with a deterministic fake fetcher (as R8 was) and still usable with a real HTTP fetch in production without a second implementation.


buildPageDirectory

Summary: Build a `PRNN-v0` §6-shaped directory from page byte data, computing each page's real hash and cumulative byte offset.


particle_voice/nn/memory/WeightPageUploader

Summary: WeightPageUploader.js — Phase 1 ParticleNN memory. Orchestrates `WeightPageStore.js` (download+hash-verify) and `WeightBindWindow.js` (GPU upload) together: priority-ordered upload, resumability (skip already-uploaded pages), and device-loss re-upload — the exact three properties R8 (`risk/WeightPagingProbe.js`) proved for a single/multi-page toy case, generalized here to an arbitrary page count and caller-chosen priority order.


attachDeviceLossRecovery

attachDeviceLossRecovery(device, uploader, recreateDevice, { maxBackoffMs = 8000 } = {})

Summary: Wire the `device.lost` recovery contract from `DeviceContract-v0.md` §5: skip recovery if `reason === 'destroyed'` (intentional disposal, not a crash); otherwise recreate the device with exponential backoff and call `uploader.reuploadAfterDeviceLoss(newDevice)`.


particle_voice/nn/memory/WeightResidencyPlan

Summary: WeightResidencyPlan.js — Phase 1 ParticleNN memory. For each program segment (from a `PRNN-v0.md` §7 bind-window directory: `{ windows: [{ segmentId, pageIndices, bindGroupLayoutHash }] }`), which pages must be resident — plus the diff between two segments' page sets, so a scheduler can decide exactly which pages to load/evict when moving from one segment to the next instead of reloading everything.


particle_voice/nn/runtime/DeviceProfile

Summary: DeviceProfile.js — Phase 1 ParticleNN runtime. Capability/tier detection, implementing exactly the tier rules frozen in `spec/DeviceContract-v0.md` §1–§2 (hard requirements + `full`/`no-f16`/ `unsupported` tiers) — not a new policy invented here. Reuses the engine's own WebGPU acquisition (`engine/core/gpu/GpuDevice.js`) rather than reimplementing adapter/device request logic, same as R1's `risk/DeviceMatrixProbe.js`.


detectDeviceProfile

Summary: Acquire a throwaway GPUDevice, classify its tier per DeviceContract-v0 §1–§2, then destroy it — mirrors R1's read-only probe discipline. The caller is expected to acquire its OWN long-lived device separately (this function is a classification probe, not a device provider).


particle_voice/nn/runtime/KernelValidation

Summary: KernelValidation.js — Phase 1 ParticleNN testing. Reusable dispatch-and-readback plumbing plus the plan's required edge-case test-vector generators ("odd dims, min dims, zeros, extreme FP16, quantized values") — kept generic over binding shape rather than one runner-per-kernel, since Phase 1's six kernels all differ in bind-group layout but share the same "encode uniform + storage buffers, dispatch, read one output buffer back, compare to a JS reference" shape.


dispatchAndReadback

async dispatchAndReadback(device, wgslSource, { uniform, storageInputs = [], outputByteLength, entryPoint = 'main', label = 'KernelValidation' }, workgroups)

Summary: Run one compute dispatch against a set of storage/uniform buffers built from plain typed arrays, then read one designated output buffer back. Compiles `wgslSource` itself (rather than accepting a pre-built pipeline) so it can pair the shader module with the matching explicit bind group layout described above.


checkParity

Summary: Compare an actual numeric array to an expected one and return a parity-check result object matching R2's `MatmulBenchProbe.js` shape.


randomVector

Summary: Random values in [-1, 1], deterministic given `seed`.


Summary: Values right at/near the extremes a well-behaved f16 encoder/kernel must not silently clip or NaN on.


quantizedEdgeVector

Summary: Values already rounded to representable int8 codes, for quantization-path parity tests.


Summary: Standard edge-case dimension set: odd, minimal (1), and a "normal" power-of-two size, for sweeping a kernel's dimension parameter.


particle_voice/nn/runtime/NeuralExecutor

Summary: NeuralExecutor.js — Phase 1 ParticleNN runtime. Dispatches a compiled `ParticleProgram`'s steps as one command encoder (one compute pass per step, in program order), then submits through the engine's own `GpuFrameBudgetBroker` when one is supplied — sharing the render queue's frame budget rather than a second, uncoordinated submission path. Falls back to a direct `device.queue.submit()` when no broker is given (e.g. an offline/background compile-and-run context with no render loop to share a budget with).


particle_voice/nn/runtime/NeuralScheduler

Summary: NeuralScheduler.js — Phase 1 ParticleNN runtime. Priority-ordered queue of `NeuralExecutor.run()` requests — for when several `ParticleProgram` runs are pending at once (e.g. multiple NPCs queued to speak in the same frame window). Mirrors `engine/core/gpu/AsyncComputeScheduler.js`'s high/normal/low priority bucket design (reference, not imported — ParticleNN stays decoupled from the render-side scheduler's internals) layered on top of `NeuralExecutor.js`, which already defers to a shared `GpuFrameBudgetBroker` for actual frame-budget admission — this file's job is only ordering *which* run gets submitted next, not re-deriving budget admission itself.


particle_voice/nn/runtime/NumericalTolerance

Summary: NumericalTolerance.js — Phase 1 ParticleNN testing. Canonical tolerance constants (single source — `chunks/f16_utils.js` re-exports these rather than defining its own copy) and the shared abs/rel-error comparison used by `KernelValidation.js` and every kernel parity test, matching the exact convention R2's `MatmulBenchProbe.js` already proved out (`maxAbsError`/`maxRelError` against a JS scalar reference).


maxErrors

Summary: Elementwise max abs/rel error between two same-length numeric arrays.


withinTolerance

withinTolerance(actual, expected, { absTolerance, relTolerance })

Summary: True if `maxErrors(actual, expected)` is within `{ absTolerance, relTolerance }` (either bound satisfied per-element is sufficient — matches R2's tolerance semantics).


particle_voice/nn/runtime/PipelineCache

Summary: PipelineCache.js — Phase 1 ParticleNN runtime. Hash-keyed compute pipeline cache: avoids recompiling the same WGSL source + entry point on every dispatch. Keyed by a cheap string hash of the generated WGSL source (kernels are JS-generated, so byte-identical source for the same params is expected — see `nn/kernels/ShaderComposer.js`), not by kernel name + params (two different param sets can legitimately generate identical WGSL, and caching by source avoids a redundant compile in that case too).


particle_voice/nn/runtime/PipelineUtils

Summary: PipelineUtils.js — Phase 1 ParticleNN runtime. Deliberate byte-for-byte mirror of `agi/llm/kernels/PipelineUtils.js` (design reference, not imported) per the plan's explicit instruction to keep ParticleNN decoupled from the LLM runtime. Already proven correct — every Phase -1 risk spike (R2, R3, R9, etc.) has been using the LLM runtime's copy of this exact logic directly; this is the same logic under ParticleVoice's own module tree.


createComputePipelineChecked

async createComputePipelineChecked(device, module, { label, name = label, entryPoint = 'main' } = {})


assertShaderCompiled

async assertShaderCompiled(module, name = 'shader')


particle_voice/risk/ActivityIntegrationProbe

Summary: ActivityIntegrationProbe.js — R15 Phase -1 risk spike orchestrator.


particle_voice/risk/BreathReservoir

Summary: BreathReservoir.js — R6A Phase -1 risk spike. "BreathReservoirState (lung pressure, compliance, target subglottal pressure) -> ParticleTract" — particle-voice-26357c.md, Phase 2. Minimal first-order lag model: lung pressure exponentially approaches whatever target subglottal pressure RespiratoryDriveHead commands, rate set by `compliance` (higher compliance = slower to respond, matching the physical intuition of a more elastic reservoir). This is the mechanism that lets a physiology intervention on breath effort actually change the glottal source's amplitude sample-by-sample rather than jumping instantly.


createBreathReservoir

createBreathReservoir({ sampleRate, compliance = 0.15 })


particle_voice/risk/CaptureGate

Summary: CaptureGate.js — R14 Phase -1 risk spike. "Custom-voice capture rejects insufficient speech, low SNR, clipping, echo, music, another speaker, gain drift, device changes, dropped samples, silence, or poor coverage." — Phase -1. Each check is a cheap, independently testable heuristic proxy (not a trained classifier) over a session of R10 `ParticleEnergyMeter` frame analyses plus out-of-band capture metadata (device-change/dropped-sample counts, target-phrase coverage) that a real `MicrophoneAudioWorklet` / `MicrophoneStream` layer — not the energy meter — would track.


checkEcho

Summary: Echo contamination: strong correlation between the recording and a known TTS reference.


checkMusic

Summary: Music proxy: natural speech's frame-to-frame RMS is far less stable than sustained tonal/musical content.


checkAnotherSpeaker

Summary: Another-speaker proxy: an abrupt, sustained shift in low/high band energy balance partway through.


checkGainDrift

Summary: Gain drift: mean level of the last third vs first third of the speech should stay close.


evaluateCaptureQuality

Summary: `session` = { frames, echoResidualRatio, deviceChangeEventCount, droppedSampleCount, phraseCoverageRatio }.


particle_voice/risk/CaptureGateProbe

Summary: CaptureGateProbe.js — R14 Phase -1 risk spike orchestrator.


particle_voice/risk/DeviceMatrixProbe

Summary: DeviceMatrixProbe.js — R1 Phase -1 risk spike. "device-matrix.html — record shader-f16, workgroup/storage/binding/invocation limits, AudioContext properties, crossOriginIsolated, AudioWorklet/ SharedArrayBuffer, OS VRAM quota." — particle-voice-26357c.md, Phase -1. Reuses the engine's own capability-probing primitives instead of reimplementing WebGPU adapter/device acquisition: - engine/core/gpu/GpuDevice.js  (GpuDevice.create, detectGpuPlatform) - engine/core/gpu/GpuInit.js    (isWebGpuSupported) - webgpu-os/kernel/VRAMTracker.js (default app/global quota) Read-only: the WebGPU device created here is destroyed immediately after limits/features are read, and the AudioContext is closed immediately after its properties are read.


probeWebGpu

Summary: Acquire a throwaway GPUDevice via the engine's own GpuDevice, read limits/features, destroy it.


probeAudioContext

Summary: Construct a throwaway AudioContext, read its properties, close it.


probeSharedArrayBuffer

Summary: Confirm SharedArrayBuffer is not just present but actually constructible.


probeVramQuota

Summary: Read the OS default VRAM quota (webgpu-os/kernel/VRAMTracker.js), no kernel required.


runDeviceMatrixProbe

Summary: Run every device-matrix probe and roll up an overall status. `unsupported` is a distinct outcome from `fail`, matching the plan's own Production Contract language ("Unsupported hardware is reported explicitly as unsupported... the project does not degrade to an external runtime"): - unsupported: WebGPU itself is unavailable on this device/browser. - fail: WebGPU is available, but a hard architectural requirement (crossOriginIsolated, SharedArrayBuffer, AudioWorklet) is missing — almost always a *serving* problem (missing COOP/COEP headers), not a hardware ceiling. Re-check via start_server.py before treating as real. - pass: WebGPU + AudioContext + cross-origin isolation + SharedArrayBuffer + AudioWorklet are all present.


buildDeviceMatrixReceipt

Summary: Wrap a raw probe result into the immutable JSON receipt described by the R1 exit gate: "Per-device limits + capability table."


particle_voice/risk/EchoBargeIn

Summary: EchoBargeIn.js — R12 Phase -1 risk spike. "echo reference ring, delay estimation, acoustic echo suppression, double-talk detection, no self-interruption." — Phase -1. Simplified fixed-delay echo cancellation (cross-correlation delay estimate + least-squares gain fit + direct subtraction), not an adaptive filter (NLMS/Kalman) — enough to prove the architecture correctly distinguishes pure echo from genuine double-talk. `AcousticEchoController.js` (Phase 2+) would use a real adaptive filter for real acoustic paths that drift over time; this fixed-delay model is a feasibility check.


estimateDelay

Summary: Cross-correlation delay estimate: which shift of `reference` best predicts `microphone`.


estimateGain

Summary: Least-squares gain fit: minimizes ||microphone - gain*referenceAligned||^2.


cancelEcho

cancelEcho(microphone, reference, delay, gain)


detectDoubleTalk

detectDoubleTalk(residual, referenceLevel, thresholdRatio = 0.3)


createBargeInController

createBargeInController({ minSustainedFrames = 3 } = {})

Summary: Commit bargeInConfirmed only when double-talk is detected AND sustained for `minSustainedFrames`.


particle_voice/risk/EchoBargeInProbe

Summary: EchoBargeInProbe.js — R12 Phase -1 risk spike orchestrator.


particle_voice/risk/EndpointController

Summary: EndpointController.js — R13 Phase -1 risk spike. "min speech/silence durations, max-segment cap, causal commit bits (no lookahead)." — Phase -1. A plain frame-counting debounce state machine on top of a boolean VAD decision (e.g. R11's `hysteresisState`) — every `update()` call only consumes the CURRENT frame's VAD decision plus its own prior internal state, never anything from the future, by construction (no buffering of upcoming frames anywhere in this module).


createEndpointController

createEndpointController({ frameMs, minSpeechMs = 200, minSilenceMs = 300, maxSegmentMs = 8000 })


particle_voice/risk/EndpointLatencyProbe

Summary: EndpointLatencyProbe.js — R13 Phase -1 risk spike orchestrator.


particle_voice/risk/EnergyDomains

Summary: EnergyDomains.js — R15 Phase -1 risk spike. "ObservedInputEnergy, GeneratedPhysiologyEnergy, and RenderedOutputEnergy are correctly separated; mic energy is labelled an uncertain acoustic-equivalent proxy, not lung pressure." — Phase -1. Three distinct, runtime-tagged value types plus an `assertDomain` guard that THROWS on cross-domain misuse — the concrete mechanism proving the architecture cannot silently conflate "what the mic heard" with "what the physiology model generated" or "what the renderer produced".


ObservedInputEnergy

Summary: Energy derived from the real microphone signal. This is explicitly an uncertain ACOUSTIC-EQUIVALENT proxy — it correlates with vocal effort but is NOT a measurement of lung pressure, glottal state, or any other physiological quantity (room acoustics, mic gain/placement, and background noise all corrupt the mapping).


GeneratedPhysiologyEnergy

Summary: Energy/state produced by the neural physiology model (R6A NeuralPhysiologyState/BreathReservoir) driving the Voice Box.


RenderedOutputEnergy

Summary: Energy of the final rendered PCM output (post Voice Box waveguide render).


assertDomain

Summary: Throws unless `energyObject.domain === expectedDomain` — the runtime separation guard.


driveVoiceBox

Summary: Stand-in consumer: drives the Voice Box — must only ever accept generated-physiology energy.


computeBargeInEvidence

Summary: Stand-in consumer: barge-in/VAD evidence — must only ever accept observed-input energy.


computeLoudnessDisplay

Summary: Stand-in consumer: UI loudness meter for what actually came out of the speakers — must only ever accept rendered-output energy.


particle_voice/risk/EnergyMeterProbe

Summary: EnergyMeterProbe.js — R10 Phase -1 risk spike orchestrator. No dedicated filename exists for R10 in the plan's risk/ page list either (named "mic-energy-meter.html" in the risk-page description text, used here directly).


Summary: Fft1024Kernel.js — R3 Phase -1 risk spike WGSL generator. No raw `.wgsl` files (Engine Alignment invariant #1): exports a `shader(params)` function, matching the eventual `nn/kernels/fft/fft1024_f32.js` / `ifft1024_f32.js` production kernels. Single-workgroup, in-workgroup-memory, iterative radix-2 Cooley-Tukey FFT. One workgroup (512 threads) computes exactly one 1024-point complex FFT; `workgroup_id.x` selects which frame of a batch to process, so N frames can be transformed in a single dispatch of N workgroups (needed to batch many overlapping STFT frames for the WOLA test). Direction is a per-dispatch uniform, not two separate shaders: forward uses the standard e^{-i2*pi*k/N} twiddle; inverse conjugates the input, runs the same forward butterfly network, then conjugates + scales the output by 1/N (IFFT(X) = (1/N) * conj(FFT(conj(X)))). Buffer layout (flat, frame-major): inputReal/inputImag/outputReal/outputImag are each `numFrames * 1024` f32 values; frame f occupies [f*1024, f*1024+1024).


particle_voice/risk/GlottalSource

Summary: GlottalSource.js — R6 Phase -1 risk spike. A simplified ("LF-lite") glottal flow source, not the full Liljencrants- Fant model (which requires an iterative numerical solve of its alpha/wg/ ta/b parameters from open/speed quotient — out of scope for a feasibility spike). Each glottal pulse is a raised-cosine bump over the open phase of the cycle: flow(n) = 0.5 * (1 - cos(2*pi*n/Topen))   for 0 <= n < Topen flow(n) = 0                                for Topen <= n < T This is deliberately C1-continuous and zero at both pulse boundaries — clicks are a discontinuity in the waveform or its derivative, and this shape has neither, by construction, regardless of how F0 (period T) changes from one pulse to the next. `articulatory/glottal_lf.js` (Phase 2) is the real LF implementation this stands in for. Aspiration noise is mixed in proportional to instantaneous flow (breathy voicing), driven by a counter-based hash so it is chunk-boundary independent (same technique as StreamingContinuitySource.js).


createGlottalSource

createGlottalSource({ sampleRate, f0AtSample, openQuotient = 0.6, aspirationLevel = 0.0, noiseSeed = 777 })

Summary: Create a glottal source generator. `f0AtSample(n)` lets callers drive a pitch sweep (F0 varies per absolute sample index); `openQuotient` is the fraction of each pitch period the folds are open (typical modal voice ~0.5-0.7).


particle_voice/risk/GpuReadbackRing

Summary: GpuReadbackRing.js — R4 Phase -1 risk spike. "readback-audio.html — 6-8 MAP_READ slots, per-buffer mapAsync(), no global queue.onSubmittedWorkDone() for normal readback." — Phase -1. A minimal round-robin ring of MAP_READ|COPY_DST staging buffers. Each slot is copied into and mapped independently; the only synchronization primitive used is that slot's own `mapAsync()` promise — there is no `device.queue.onSubmittedWorkDone()` call anywhere in this module. Backpressure (more than `slotCount` requests in flight) is the caller's responsibility: `acquireFreeSlotIndex()` throws if no slot is free, so a caller must await/release before requesting more than `slotCount` ahead. This is a disposable Phase -1 probe, not the production `streaming/GPUReadbackRing.js` — same technique, smaller surface.


createGpuReadbackRing

createGpuReadbackRing(device, slotByteSize, slotCount = 8)


runPipelinedReadbackBenchmark

async runPipelinedReadbackBenchmark(device, ring, sourceBuffer, byteSize, iterations, maxInFlight, makeFrame, updateBufferFn)

Summary: Run a pipelined readback benchmark: keeps up to `maxInFlight` mapAsync() requests outstanding at once (never more, never a global wait), writing a fresh frame into `sourceBuffer` before each new request.


particle_voice/risk/HeavySceneKernel

Summary: HeavySceneKernel.js — R9 Phase -1 risk spike. "renderer-voice-stress.html — voice compute beside a representative heavy ParticleRealms scene." — Phase -1. A fully procedural instanced-triangle render (positions/colors derived from @builtin(instance_index) in the vertex shader, no vertex/instance buffers needed) standing in for "a representative heavy scene" — many draw-time vertices without needing real mesh/scene data for a disposable risk spike.


particle_voice/risk/IntegrationManifest

Summary: IntegrationManifest.js — R0 Phase -1 risk spike data. Every named ParticleRealms module referenced by the ParticleVoice plan's "Codebase Integration Points" tables (particle-voice-26357c.md), with the named exports each entry is expected to provide. `integration-path-audit.html` dynamically imports each `path` and checks every name in `exports` is present on the resulting module namespace, producing a pass/fail/missing receipt. Paths are root-relative so they resolve the same regardless of which page imports this manifest (served from the repo root by start_server.py). Exports were verified directly against source (grep for `^export`), not assumed from the plan's prose "What it provides" column. Where the plan's description implied an export that does not actually exist at module scope, a `note` field records the discrepancy so the audit page can surface it even on entries that technically "pass" (file imports fine, but a *different* name provides the capability than the plan implied).


particle_voice/risk/IntegrationPathAudit

Summary: IntegrationPathAudit.js — R0 Phase -1 risk spike runner. "Dynamic import/export checks for every named ParticleRealms module" — particle-voice-26357c.md, Phase -1 / R0 Integration exit receipt. For every entry in IntegrationManifest.js this dynamically imports the module and checks that each expected named export is present on the resulting namespace object. No network/file mutation; read-only probing.


runIntegrationPathAudit

async runIntegrationPathAudit({ manifest = INTEGRATION_MANIFEST, importer = (path)

Summary: Run the audit against a manifest.


buildIntegrationAuditReceipt

Summary: Wrap a raw audit result into the immutable JSON receipt described by the R0 exit gate: "JSON file listing every named import, expected export, actual export, result pass/fail/missing."


particle_voice/risk/KellyLochbaumWaveguide

Summary: KellyLochbaumWaveguide.js — R6 Phase -1 risk spike. Single-delay-per-section digital waveguide vocal tract model (Kelly & Lochbaum 1962 scattering junctions, as described in e.g. Julius O. Smith's "Physical Audio Signal Processing"). N cylindrical tube sections with cross-sectional areas A[0..N-1] give N-1 internal junction reflection coefficients k_i = (A_i - A_{i+1}) / (A_i + A_{i+1}). Boundary conditions: - Glottal end: source flow is injected with a partial reflection (`glottalReflection`) of the wave returning from the tract. - Lip end: an idealized open-end reflection (`lipReflection`, negative) plus a first-difference "radiation" filter approximating the differentiating characteristic of lip radiation. All boundary/junction coefficients are kept strictly inside (-1, 1), so the closed-loop lattice is passive/BIBO-stable by construction (no combination of area profile changes can cause unbounded energy growth). Nasal coupling and constriction noise are deliberate simplifications, not a rigorous multi-port (3-way) junction scattering derivation — see createNasalBranch() and step()'s constrictionIndex handling. Phase 2's `NasalTractJunction.js`/`ConstrictionNoise.js` need the rigorous version; this is enough to prove the overall architecture is stable and produces plausible, controllable behavior.


shapedAreaProfile

Summary: Linearly interpolated area profile through named (position, area) control points, position in [0, 1].


createVocalTract

createVocalTract({ numSections, areas, glottalReflection = 0.85, lipReflection = -0.9 })


createNasalBranch

createNasalBranch({ numSections, areas, nostrilReflection = -0.9 })

Summary: Simplified nasal branch: its own short KL chain, driven by a coupled fraction of an oral-tract tap signal.


particle_voice/risk/MatmulBenchProbe

Summary: MatmulBenchProbe.js — R2 Phase -1 risk spike. "matmul-bench.html — W8A16 packed INT8 x FP16 matmul, FP32 accumulation, FP16 output; representative TTS shapes; full-model cost projection with >=30% headroom." — particle-voice-26357c.md, Phase -1. Shapes are PROVISIONAL: Phase 0 (spec freeze) has not happened yet, so the plan does not fix concrete model dimensions. These are derived from C:\Coding\PS2\configs\r0.yaml's own "architecture smoke test" convention (d_model=256, d_ff=4x d_model, n_layers=6) — the same repo's precedent for sizing a first small model before a full spec exists — combined with this plan's own VoiceChunker steady-state chunk bucket (64 frames) and stated mel-bin candidate (128). Revisit once spec/DeviceContract-v0.md exists. Reuses engine primitives instead of reimplementing device/buffer/pipeline plumbing: GpuDevice (engine/core/gpu/GpuDevice.js), GpuBuffer helpers (engine/core/gpu/GpuBuffer.js), and createComputePipelineChecked (agi/llm/kernels/PipelineUtils.js).


buildMatmulBenchReceipt

Summary: Wrap a raw probe result into the R2 exit receipt.


particle_voice/risk/MatmulReferenceCpu

Summary: MatmulReferenceCpu.js — deterministic test-vector generation and a plain-JS scalar reference matmul for R2 (matmul-bench.html) parity checking. Mirrors the dequantization + accumulation order of MatmulW8A16Kernel.js's `main()` exactly (same k4 outer / b inner loop) so the only expected divergence between CPU (double) and GPU (f32) is floating-point rounding, not algorithmic difference.


Summary: Int8Array of length K*N, logical layout weights[n*K + k], values in [-127, 127].


randomActivations

Summary: Float32Array of length M*K, layout activations[m*K + k], values in [-1, 1].


randomScales

Summary: Float32Array of length N — typical per-channel INT8 quantization scales.


Summary: Pack logical [K,N] int8 weights into the u32 layout MatmulW8A16Kernel.js expects.


referenceMatmul

referenceMatmul(activations, weightsInt8, scales, M, K, N)

Summary: Plain-JS reference matmul, same math/order as the WGSL kernel. Returns Float64Array[M*N].


compareResults

Summary: Max-abs and max-relative error between a GPU result and the CPU reference.


Summary: MatmulW8A16Kernel.js — R2 Phase -1 risk spike WGSL generator. No raw `.wgsl` files (Engine Alignment invariant #1): this module exports a `shader(params)` function that returns a WGSL string, same convention as `agi/llm/kernels/shaders/*.js` and the eventual `nn/kernels/matmul/matmul_w8a16.js`. MVP scope for this spike: INT8 weights (4-packed per u32, per-output-channel scale) x FP32 activations, FP32 accumulation, FP32 output. This is a deliberately smaller slice of the plan's stated "W8A16 packed INT8 x FP16 matmul, FP32 accumulation, FP16 output" — FP16 activation/output packing needs its own bit-packing design (2xf16 per u32, gated on `shader-f16` from R1) and is left as an explicit follow-up rather than an unverified claim. Weight layout expected by this kernel: - Logical weights are [K, N] (K = input/hidden dim, N = output dim). - Packed as `packedWeights[n * (K/4) + k4]`, a u32 whose 4 bytes are the int8 weights for k = k4*4 + 0..3 (little end = lowest k). - `scales[n]` is the per-output-channel dequantization scale. - K must be a multiple of 4.


particle_voice/risk/NeuralPhysiologyState

Summary: NeuralPhysiologyState.js — R6A Phase -1 risk spike. "NeuralPhysiologyState schema: RespiratoryDriveHead, GlottalStateHead, TractGestureHead, ArticulationEventHead, UncertaintyHead version 0x00010000." — particle-voice-26357c.md, Phase 2 (adopted early here for the R6A wiring receipt). No trained `ArticulationHead` model exists yet (that's Phase 3+). This provides the state SCHEMA plus a deterministic scripted stand-in ("fake head") that produces plausible per-frame physiology values as a pure function of time and an `intervention` parameter set — enough to prove the wiring ArticulationHead -> NeuralPhysiologyState -> BreathReservoir/ParticleTract -> renderer actually routes control values through to the final PCM, which is what R6A's exit gate asks for.


createNeuralPhysiologyState

createNeuralPhysiologyState({ timestampSeconds, respiratoryDrive, glottalState, tractGesture, articulationEvent, uncertainty, })


createScriptedArticulationHead

Summary: Deterministic scripted stand-in for a trained ArticulationHead: a short "utterance" that opens into a steady vowel then applies a brief closure gesture, with `intervention.respiratoryScale` and `intervention.constrictionOpennessFloor` letting a caller apply a physiology intervention and compare against a baseline run.


particle_voice/risk/OsMediaPermissionProbe

Summary: OsMediaPermissionProbe.js — R16 Phase -1 risk spike. "Secure context + user gesture + getUserMedia + enumerateDevices + AudioWorklet + SharedArrayBuffer all succeed through kernel.media.* / kernel.audio.* syscalls; media.audio / audio.play / microphone capabilities are correctly required and enforced by guardSyscalls; OsMediaBridge.js never touches raw navigator.mediaDevices." — Phase -1. `agi/particle_voice/activity/bridge/OsMediaBridge.js` does not exist yet (Phase -1 precedes Phase 1+ implementation) — that check is a forward contract placeholder here, not yet applicable. Everything else tests the REAL `webgpu-os/kernel/Syscalls.js` (`createSyscalls`, `guardSyscalls`) and `webgpu-os/kernel/Permissions.js` (`Permissions`) directly, not a stand-in — this spike is about the OS integration contract itself.


particle_voice/risk/ParticleEnergyMeter

Summary: ParticleEnergyMeter.js — R10 Phase -1 risk spike. "RMS, dBFS, peak, clipping, DC offset, crest factor, attack/release, SNR, low/mid/high band energy, spectral flux; adaptive noise floor while no speech/echo present." — Phase -1. Plain-JS CPU implementation (small analysis frames, not a GPU kernel — this is a metering/control-path computation, not a bulk DSP kernel like R3's FFT). Includes its own compact FFT for band energy/spectral flux rather than importing Fft1024Kernel.js, which generates a WGSL string for GPU dispatch, not a callable JS magnitude-spectrum function — a different form for a different execution context, same algorithm family as MatmulReferenceCpu.js is to matmul_w8a16.js.


createParticleEnergyMeter

createParticleEnergyMeter({ sampleRate, frameSize = 256, attackMs = 5, releaseMs = 60, noiseFloorRecoveryPerFrame = 0.0008 })


particle_voice/risk/ParticleVad

Summary: ParticleVad.js — R11 Phase -1 risk spike. "ParticleVAD-1 11-head output smoothness, hysteresis, no flicker; latency p50/p95 of speech-start/end decisions." — Phase -1. No trained ParticleVAD-1 model exists yet — this is a heuristic, energy/spectral-flux-driven stand-in (built on R10's ParticleEnergyMeter), not a neural VAD. The plan names "11 continuous probability heads" without enumerating all 11 in the sections read for this spike; the 11 names below are a reasonable, clearly-labeled interpretation, not a verified spec. Testing rigor here focuses on the primary `speechActive` head's smoothness/hysteresis/latency, since that is what a heuristic stand-in can meaningfully validate.


createParticleVad

createParticleVad({ sampleRate, frameSize = 256, smoothingMs = 30, riseThreshold = 0.55, fallThreshold = 0.35 })


Summary: R4ReadbackAudioWorklet.worklet.js — R4 Phase -1 risk spike. "allocation-free AudioWorklet." — Phase -1. Reference/mirror target: engine/audio/synth/PatchRunner.worklet.js (SAB parameter reads, no allocation in process()). Consumes a single-producer/single-consumer PCM ring living in a SharedArrayBuffer (written by the main thread, standing in for what the production path would fill via GPUReadbackRing.js + SharedPCMRing.js). `process()` performs zero allocation: no `new`, no object/array literals, no `await`, no `throw`, no `JSON` — only typed-array indexing and Atomics on pre-existing SharedArrayBuffer-backed views. Control layout (Int32Array, SharedArrayBuffer, 4 x int32): [0] writeIndex   — next PCM slot the producer will write (main thread) [1] readIndex    — next PCM slot this processor will read [2] underrunCount — incremented whenever fewer than one quantum of samples are available [3] processCallCount — incremented every process() call This file cannot be dynamically `import()`-ed from a normal page context: `AudioWorkletProcessor`/`registerProcessor` only exist inside the AudioWorkletGlobalScope. Load it via `audioContext.audioWorklet.addModule(url)`.


particle_voice/risk/ReadbackAudioProbe

Summary: ReadbackAudioProbe.js — R4 Phase -1 risk spike. Exit gate: "Per-buffer mapAsync() p50/p95/p99; AudioWorklet survives variable quantum and rebuild; one-hour zero-underrun baseline." — Phase -1. Honest scoping note: a literal one-hour automated soak is not run by this probe (impractical inside a single interactive/CI session). This runs a short deterministic soak (SHORT_BASELINE_SOAK_MS) as a scaled-down proxy, clearly labeled as such in the receipt, plus an underrun-detector self-test (proves the zero-underrun count is meaningful, not tautological) and a full teardown+rebuild cycle. The full one-hour soak is left as an explicit manual/CI follow-up — see `oneHourSoak` in the result. "Variable quantum" is not directly controllable from stable browser APIs (render quantum size is fixed at 128 in all current stable implementations); this is verified as a static property of the processor (R4ReadbackAudioWorklet.worklet.js reads `channel.length` for its loop bound rather than assuming 128) plus dynamic survival across two independent AudioContext instances (which may differ in `sampleRate`).


prefillRing

Summary: Synchronously write `prefillMs` of audio into the ring before the worklet node is ever pulled. Absorbs the startup race between the interval-based producer's first tick (~PRODUCER_INTERVAL_MS away) and the AudioWorklet's first process() call, which can happen sooner — without this, a handful of transient underruns at cold start are expected, not a sustained-rate problem, but they'd still show up in a "zero-underrun baseline" reading.


startProducer

startProducer(ring, sampleRate, initialSamplesProduced = 0)

Summary: Wall-clock-paced producer: writes exactly enough samples each tick to match elapsed real time (minus whatever was already prefilled), so it neither underruns (falls behind) nor overflows the fixed-capacity ring (a fixed-chunk-per-tick producer would race arbitrarily far ahead of the consumer and wrap/corrupt the ring).


runLongSoak

Summary: Exposed on window for a human/CI to opt into the real one-hour soak manually.


particle_voice/risk/ReceiptCrypto

Summary: ReceiptCrypto.js — shared canonicalization + SHA-256 hashing for every Phase -1 risk-spike JSON receipt under `agi/particle_voice/risk/`. Kept tiny and dependency-free so any risk page can import it directly.


canonicalizeForHash

Summary: Recursively sort object keys so hashing is independent of insertion order.


Summary: SHA-256 hex digest of a canonicalized JSON value, or null if WebCrypto is unavailable.


receiptHashOf

Summary: Convenience: `sha256:<hex>` prefixed digest, or null.


particle_voice/risk/RendererVoiceStressProbe

Summary: RendererVoiceStressProbe.js — R9 Phase -1 risk spike. Exit gate: "Voice + heavy scene runs without audio underruns or persistent render frame drops." — Phase -1. Runs a heavy procedural instanced-triangle render pass and a batched FFT "voice compute" dispatch every animation frame, concurrently with R4's real AudioWorklet + SharedArrayBuffer PCM ring (imported directly, not reimplemented), for a few seconds. Measures both render frame-time health and audio underrun count over the same window.


particle_voice/risk/SignalMetrics

Summary: SignalMetrics.js — shared signal-quality helpers for Phase -1 risk pages (extracted out of VoiceBoxWaveguideProbe.js when a third consumer appeared, to avoid a third copy-paste of rms/peakAbs/hasNonFinite).


rms

rms(signal, start = 0, end = signal.length)


peakAbs

peakAbs(signal, start = 0, end = signal.length)


maxAbsDiff

maxAbsDiff(a, b, startIndex = 0, endIndex = a.length)


detectClicks

detectClicks(signal, windowSize = 256, thresholdMultiplier = 12)


particle_voice/risk/StftWolaProbe

Summary: StftWolaProbe.js — R3 Phase -1 risk spike. "stft-wola.html — 1024-point f32 FFT/IFFT and normalized WOLA for 320-sample hop; identity, impulse, sweep, noise tests." — Phase -1. Exit gate: "Identity tests pass; workgroup limits respected; normalized WOLA ripple below audible threshold." Identity tests run a single frame through forward FFT then inverse FFT (both on GPU) and compare to the original signal. The WOLA test runs many overlapping analysis-windowed frames through the same round trip, applies a synthesis window, overlap-adds, and divides by the exact sum-of-squares envelope — with exact normalization the round trip is mathematically identity, so any measured "ripple" is real FFT/window/OLA implementation error, not a windowing-shape artifact.


particle_voice/risk/StreamingContinuityProbe

Summary: StreamingContinuityProbe.js — R5 Phase -1 risk spike orchestrator. Pure JS, no GPU/AudioContext dependency: R5's exit gate is an architectural/correctness property of the counter design (does chunking change the output?), not a GPU performance question, so no kernel is needed to validate it. See StreamingContinuitySource.js for the mechanism.


particle_voice/risk/StreamingContinuitySource

Summary: StreamingContinuitySource.js — R5 Phase -1 risk spike. Exit gate: "Phase-continuous harmonic/noise across arbitrary chunk boundaries; absolute-counter noise chunk-independent." — Phase -1. There is no dedicated filename for R5 in the plan's risk/ page list (it names R0-R4, R6-R16 pages explicitly but not R5) — this file/page covers that exit gate directly. The technique: every sample's value is a pure function of its ABSOLUTE sample index (harmonic phase = 2*pi*f0*n/sampleRate; noise = a counter-based hash of n), with no persistent oscillator/PRNG state threaded between chunks and no chunk-relative counter anywhere. Chunking therefore cannot affect the output — not "close enough", bit-identical — because the per-sample computation never reads chunk position at all. This is the mechanism `harmonic_source.js`/`noise_source.js` (Phase 2) must use for random chunk-boundary training and streaming to work.


noiseAt

Summary: Counter-based hash PRNG: deterministic pseudo-random float in [-1, 1], pure function of (index, seed).


harmonicAt

Summary: Pure function of absolute sample index — no carried oscillator phase state.


synthesizeAbsoluteCounter

synthesizeAbsoluteCounter(totalSamples, sampleRate, f0Hz, noiseSeed, chunkSizes)

Summary: Correct (absolute-counter) synthesis: chunking `chunkSizes` (cycled) has zero effect on the output because each sample only ever reads its absolute index `n`, never a position relative to the current chunk.


synthesizeChunkRelativeCounterBuggy

synthesizeChunkRelativeCounterBuggy(totalSamples, sampleRate, f0Hz, noiseSeed, chunkSizes)

Summary: Deliberately buggy (chunk-relative-counter) synthesis for the self-test: resets its local counter to 0 at the start of every chunk, so output DOES depend on how the signal was chunked — used only to prove the bit-exact comparison in this probe actually catches phase discontinuity.


particle_voice/risk/TinyAutodiff

Summary: TinyAutodiff.js — R7 Phase -1 risk spike. Hand-wired forward+backward layers (Linear, Conv1D, ReLU, Sigmoid), losses (MSE, BCE), and an AdamW optimizer — deliberately NOT a general computational-graph autodiff engine. `ParticleTape`/`BackwardGraphCompiler`/ `GradientGraph` (Phase 1, nn/autodiff/) are the real general system; R7's job is to prove models at this parameter scale can be forward+backward trained in-browser at all, not to build that general architecture early. All arrays are plain Float32Array with row/channel-major layouts documented per function; batch=1 throughout (a single fixed sequence/vector per step) is sufficient for R7's memorization-convergence test.


createConv1D(inChannels, outChannels, kernelSize, rng)


clipGradGlobalNorm

Summary: Clip the global L2 norm across all gradient arrays in place — cheap insurance against a deep stack diverging.


createAdamW

createAdamW(paramArrays, { lr = 0.01, beta1 = 0.9, beta2 = 0.999, eps = 1e-8, weightDecay = 0.01 } = {})


particle_voice/risk/TinyModels

Summary: TinyModels.js — R7 Phase -1 risk spike model definitions. "1M-parameter model, 5M Conv1D model, tiny adversarial audio pair" — particle-voice-26357c.md, Phase -1. Exact param counts land close to but not exactly at 1,000,000/5,000,000 (real counts are reported, not fudged to hit a round number) — the plan's own number is an order-of-magnitude descriptor.


createTinyMlpModel

Summary: ~0.93M-param MLP: Linear(64,650)-ReLU x3 -Linear(650,64).


createTinyConv1dModel(rng, sequenceLength = 16)

Summary: ~4.5M-param Conv1D stack: Conv1D(16,384,5)-ReLU, 6x[Conv1D(384,384,5)-ReLU], Conv1D(384,16,5).


createTinyGan

Summary: Tiny generator (noise[8] -> sample[16]) + discriminator (sample[16] -> real-probability).


particle_voice/risk/TrainingSpikeProbe

Summary: TrainingSpikeProbe.js — R7 Phase -1 risk spike orchestrator. Exit gate: "1M model converges; 5M Conv1D model converges; tiny GAN converges and checkpoints; AdamW memory estimate matches measurement." — Phase -1. "OPFS cache behavior" and "device-loss re-upload" are also named in the risk page description and covered here. Scoping notes (read before trusting a pass): - "Converges" here means memorizes a small fixed random input/target pair (loss drops sharply) — a feasibility/mechanics check, not a generalization claim. - "Device-loss re-upload" exercises the re-upload PROCEDURE after an intentional device.destroy(); there is no scriptable public API to force an uncontrolled device loss (crash), so that path is not tested.


particle_voice/risk/VadContinuityProbe

Summary: VadContinuityProbe.js — R11 Phase -1 risk spike orchestrator.


particle_voice/risk/VoiceBoxPhysiologyProbe

Summary: VoiceBoxPhysiologyProbe.js — R6A Phase -1 risk spike orchestrator. Exit gate: "ArticulationHead -> NeuralPhysiologyState -> BreathReservoir/ParticleTract -> acoustic renderer receipt; physiology intervention changes final PCM." — Phase -1. No dedicated filename exists for R6A in the plan's risk/ page list either (same situation as R5) — this page covers that exit gate directly. Wires: createScriptedArticulationHead (stand-in for the trained model) -> NeuralPhysiologyState -> createBreathReservoir -> createGlottalSource + createVocalTract (R6's waveguide, reused directly, not re-implemented) -> PCM. Renders the same scripted utterance twice — once at baseline, once with a reduced-respiratory-drive intervention — and requires the two PCM outputs to measurably differ while both remain stable.


particle_voice/risk/VoiceBoxWaveguideProbe

Summary: VoiceBoxWaveguideProbe.js — R6 Phase -1 risk spike orchestrator. Exit gate: "Stable vowels, nasal coupling, closure/release/frication, pitch sweeps with no clicks or energy explosions." — Phase -1. Pure JS/CPU (see KellyLochbaumWaveguide.js header for why): this proves the waveguide algorithm itself is stable and produces plausible, controllable behavior. GPU kernel parity (`tract_waveguide.js` etc.) is a separate Phase 2 concern, tested the same way R2 checked matmul parity.


particle_voice/risk/WeightPagingProbe

Summary: WeightPagingProbe.js — R8 Phase -1 risk spike. "Weight page download/recovery/page-hash/interrupted-download works; device-loss re-upload succeeds." — Phase -1. No dedicated filename exists for R8 in the plan's risk/ page list — named weight-paging.html here. Simulates WeightPageStore.js's core responsibilities: a per-page SHA-256 directory, an interrupted download that must retry (not silently drop data), corruption detection via hash mismatch, and multi-page device-loss re-upload (extends R7's single-buffer version to several pages).


particle_voice/risk/WolaSignals

Summary: WolaSignals.js — deterministic test signals, Hann window, and CPU-side sum-of-squares WOLA normalization/overlap-add for R3 (stft-wola.html).


sineSweepSignal

Summary: Linear chirp from f0 to f1 Hz across N samples at sampleRate.


sineSignal

sineSignal(length, sampleRate, freqHz, amplitude = 1.0)


computeSumOfSquaresEnvelope

computeSumOfSquaresEnvelope(window, hop, totalLength, numFrames)

Summary: sum_k window[t - k*hop]^2 for every output sample t in [0, totalLength).


overlapAdd

Summary: Overlap-add windowed frames (each already synthesis-windowed) into a flat buffer.


sliceFrames

Summary: Slice a flat signal into overlapping N-length frames at the given hop (zero-padded past the end).


peakAbs

peakAbs(a, startIndex = 0, endIndex = a.length)


particle_voice/streaming/GPUReadbackRing

Summary: GPUReadbackRing.js — Phase 2 ParticleVoice audio bridge. Production generalization of `risk/GpuReadbackRing.js` (R4)'s proven round-robin `MAP_READ` staging-slot ring — same core technique (each slot is copied into and mapped independently; the ONLY synchronization primitive is that slot's own `mapAsync()` promise, never a global `device.queue.onSubmittedWorkDone()`), generalized from R4's single-fixed-size/offset-0 probe surface to the production needs this module's callers (`ParticleTract.js`'s per-chunk PCM readback, eventually `SharedPCMRing.js`'s producer side) actually have: a byte-offset/length per request (not always the whole buffer from 0), `inFlightCount()` for backpressure introspection instead of only a throw on exhaustion, and GPU buffer accounting via `engine/core/gpu/GpuMetrics.js` (matching `GpuBuffer.js`'s own convention for every other buffer this project creates). R4's exit gate (`ReadbackAudioProbe.js`) already measured this exact technique's p50/p95/p99 readback latency and zero-underrun behavior across an 8-slot ring — this module does not re-derive or change that proven synchronization discipline, only widens its calling surface.


particle_voice/streaming/PolyphaseResampler

Summary: PolyphaseResampler.js — Phase 2 ParticleVoice audio bridge. "ParticleVoice's own DSP runs at 32 kHz internally and resamples via `PolyphaseResampler.js` at the audio bridge boundary" — `ARCHITECTURE.md` §"Measured lead values" / `spec/DeviceContract-v0.md` §7 (measured `AudioContext.sampleRate` 48000 Hz on the reference device). No risk-spike precedent exists for this module (checked `risk/` and `engine/` — the only "resample" hits in `engine/` are unrelated texture/render resample passes), so this is a fresh CPU-side (not GPU-kernel) implementation — appropriate for the audio bridge, which is already CPU-side per the plan (`SharedPCMRing.js`, `ParticleVoiceProcessor.js`). Classic Crochiere/Rabiner polyphase interpolator-decimator: for an EXACT integer ratio L/M (`inputSampleRate`/`outputSampleRate` reduced by their GCD — both are integers in practice, e.g. 32000 -> 48000 reduces exactly to L=3, M=2), a single windowed-sinc lowpass FIR (cutoff at `min(inputRate, outputRate)/2`, designed at the upsampled L*inputRate rate to suppress both upsampling images and downsampling aliases) is decomposed into L phase sub-filters of `tapsPerPhase` taps each. Per output sample, only `tapsPerPhase` multiplies are needed regardless of L — the O(tapsPerPhase*L) cost is paid once at construction (filter design), not per sample. STREAMING correctness (critical for a chunked audio bridge, where `WaveguideAcousticState` chunks arrive as small windows, not one whole signal): `process()` keeps a persistent tail of not-yet-fully-consumed input samples across calls (`_history`/`_historyBaseIndex`) and a continuously-advancing upsampled-domain position (`_pos`), so `resampler.process(wholeSignal)` and `chunks.map(c => resampler.process(c))` concatenated produce IDENTICAL output — verified directly in `tests/particle-voice/polyphase-resampler.html`. Known limitation (documented, not hidden): the FIR filter's group delay (~`tapsPerPhase/2` input samples) means the first `tapsPerPhase/2`-ish output samples ramp up from a zero-padded history rather than being bit-perfect from sample 0 — a normal, bounded FIR startup transient, not a correctness bug (every streaming FIR filter has this).


designWindowedSincLowpass

Summary: Windowed-sinc lowpass FIR, Hann-windowed, normalized to unity DC gain (`sum(h) === 1`). `cutoff` is normalized to the filter's OWN sample rate (0.5 = that rate's Nyquist) — callers designing a polyphase interpolator pass a cutoff normalized to the UPSAMPLED (L*inputRate) rate, per this module's `buildPolyphaseFilter`.


buildPolyphaseFilter

Summary: Builds the L-phase decomposition of a full-length windowed-sinc lowpass sized for L/M polyphase interpolation-decimation.


particle_voice/streaming/SafetyLimiter

Summary: SafetyLimiter.js — Phase 2 ParticleVoice audio bridge. "SafetyLimiter.js — soft clip, DC removal, fade." No existing DC-blocker/soft-clip/limiter precedent exists in this codebase to reuse (checked `engine/audio/` — `PatchRunner.worklet.js`'s `distortion` node is a bare `Math.tanh(x*drive)` waveshaper, not a limiter/DC-blocker), so this is a fresh CPU-side implementation. Sits at the very end of the audio bridge, downstream of `PolyphaseResampler.js`, as the last line of defense before PCM reaches `SharedPCMRing.js`/the AudioContext — a physics/neural solver misbehaving upstream (an unstable `tract_waveguide.js` reflection coefficient, a runaway `glottal_lf.js` Newton-Raphson solve, etc.) should never be able to produce a harsh digital-clip transient, a DC offset, or a click at an utterance boundary — that is this module's entire job. Three independent, composable stages, applied per-sample in this order: 1. DC removal — the standard DC-blocking filter (Julius O. Smith's well-known form): `y[n] = x[n] - x[n-1] + R*y[n-1]`. At DC this converges to exactly 0 (the `z=1` zero cancels any constant component); `R` close to 1 keeps the notch narrow so audible low frequencies are essentially untouched. `R` is derived via this codebase's own established exponential-pole convention (`risk/BreathReservoir.js`'s `lagRate = 1 - exp(-1/(tau*sampleRate))` pattern) rather than inventing a new formula style: here `R = exp(-2*pi*cornerHz/sampleRate)`. 2. Fade — an explicit, persistent linear gain ramp (`beginFadeIn`/`beginFadeOut`), so an utterance start/stop or a cancellation (`SharedPCMRing.js`'s `resetSharedPCMRing()` discontinuity) can be given a clean few-millisecond ramp instead of an audible click. 3. Soft clip — a `tanh`-saturating soft knee above `softClipThreshold` (default 0.8): transparent (identity) below the threshold, then a C1-continuous (matching slope at the knee, so no audible kink) saturating curve that asymptotically approaches +/-1 but never reaches or exceeds it — i.e. this NEVER hard-clips, by construction. All three stages carry PERSISTENT state across `process()` calls (DC filter memory, fade envelope position) — `reset()` clears all of it in one call, mirroring `SharedPCMRing.js`/`PolyphaseResampler.js`'s reset-on-discontinuity convention, so a caller can reset the whole audio bridge chain in lockstep on cancellation/interruption.


softClip

softClip(x, threshold = DEFAULT_SOFT_CLIP_THRESHOLD)

Summary: Pure, stateless: `tanh`-saturating soft knee. Identity below `threshold` in magnitude; above it, asymptotically approaches `+/-1` but never reaches or exceeds it. C1-continuous at the knee (`tanh'(0) === 1` matches the identity segment's slope exactly).


particle_voice/streaming/SharedPCMRing

Summary: SharedPCMRing.js — Phase 2 ParticleVoice audio bridge. Production generalization of `risk/ReadbackAudioProbe.js` (R4)'s proven `createSharedRing`/`prefillRing`/`startProducer` SharedArrayBuffer PCM ring (single-producer/single-consumer, `[writeIndex, readIndex, underrunCount, processCallCount]` control layout consumed by `R4ReadbackAudioWorklet.worklet.js`'s allocation-free `process()`). Adds the plan's explicit "generation/sequence IDs" requirement, which R4's risk spike did not need (a disposable probe never gets reset mid-run): - `generation` (control[4]): bumped by `resetSharedPCMRing()` whenever the producer discards the ring's contents outright (cancellation/ interruption per the plan's Phase 5 "cancellation resets state cleanly", or a device-loss GPU recovery restart). The CONSUMER side (`readCurrentGeneration`/the resync check in `worklet/ParticleVoiceProcessor.js`) compares its own cached generation against this value every `process()` call; on mismatch it resyncs `readIndex` to the CURRENT `writeIndex` (dropping whatever stale audio was in the ring before the reset) rather than trusting a `readIndex` that may now point at samples from a discarded utterance. - `writeSequence` (control[5]): incremented once per producer `writeSharedPCMRing()` call (a monotonic chunk counter, not a sample counter) — correlates a ring write with the `WaveguideAcousticState.sampleIndex` that produced it for diagnostics/telemetry; the consumer does not need to read it for correctness (unlike `generation`). The reader-side helper function here (`readAvailable`) is pure arithmetic over caller-supplied typed arrays — no allocation, no closures over per-call state — usable from a non-worklet consumer (e.g. a test harness) without violating an allocation-free constraint. `worklet/ParticleVoiceProcessor.js` (the AudioWorkletProcessor that actually drains this ring in real-time) does NOT import this module — both existing worklet files in this project (`engine/audio/synth/PatchRunner.worklet.js`, `risk/R4ReadbackAudioWorklet.worklet.js`) are self-contained with no ES imports, so `ParticleVoiceProcessor.js` duplicates `CONTROL_INDEX` and the read-position arithmetic inline rather than relying on an unverified assumption that `AudioWorkletGlobalScope` module imports behave identically to this project's other worklet loading paths. Both files' docstrings flag this duplication explicitly so the two `CONTROL_INDEX` definitions cannot silently drift apart unnoticed.


createSharedPCMRing

createSharedPCMRing({ sampleRate, capacitySeconds = DEFAULT_RING_CAPACITY_SECONDS })


writeSharedPCMRing

Summary: Producer-side write: copies `samples` into the ring starting at the current `writeIndex`, advancing it and incrementing `writeSequence`. Clamps to available free space (`capacity - 1 - available`, the usual ring-buffer one-slot gap to distinguish full from empty) rather than overflowing past the reader's current position and corrupting not-yet-read samples — R4's own `startProducer` avoided this by pacing writes to wall-clock time so it could never get ahead of the ring's capacity, but a production writer driven by GPU chunk completion (irregular timing) cannot assume that pacing, so this function must be defensive.


resetSharedPCMRing

Summary: Producer-side reset: zeroes both indices and bumps `generation` so the consumer discards its cached read position on its next `process()` call rather than replaying stale audio. Does not zero the PCM data itself (unnecessary — the consumer will resync past it).


readAvailable

Summary: Pure, allocation-free: samples currently available to read (does not mutate anything). Safe to call from `process()`.


particle_voice/streaming/VoiceChunker

Summary: VoiceChunker.js — Phase 2 ParticleVoice audio bridge. "VoiceChunker.js — legal buckets: first 12/16/24 frames, steady 32/48/64 frames, 16-frame left context, 8-12 frame overlap." A "frame" here is the Activity frame size (`ARCHITECTURE.md`'s measured-lead-values table: 256 samples @ 32 kHz, 8 ms/frame) — NOT the 100 Hz plan-level analysis frame rate, which `LengthRegulator.js` (Phase 3) reconciles separately. `spec/PRNN-v0.md` §4 ("Shape buckets") directly reuses these exact bucket definitions so a compiled model's execution plan only needs to support this small fixed set of shapes rather than arbitrary sequence lengths — this module is the single source of truth those buckets come from, not a second independent copy of them: firstChunkFrames:  [12, 16, 24] steadyChunkFrames: [32, 48, 64] leftContextFrames: 16 overlapFrames:     [8, 12] `risk/StreamingContinuityProbe.js` (R5) already exercised exactly these bucket sizes (`[12, 16, 24, 32, 48, 64]`) as one of its chunk-boundary patterns and proved that an ABSOLUTE (not chunk-relative) sample/phase counter design makes chunking bit-identical to processing a whole signal at once — this module's `totalFramesEmitted` bookkeeping exists for exactly that reason (a caller can derive an absolute frame index from it), not merely as a diagnostic counter. SCOPE (Phase 2, before any real streaming model exists): this module only decides CHUNK SIZE SCHEDULING (which legal bucket to use next, given how many frames are currently available) — it does not itself run a model, apply left-context frames to a real tensor, or overlap-add output audio (that's `overlap_add.js`/`crossfade.js`, already built and verified in Phase 2's DSP kernels; a future model-execution layer composes THOSE with the chunk-size decisions THIS module makes). Latency/deadline tradeoffs (when to prefer a smaller, faster-available bucket over waiting for a larger one) are explicitly `VoiceDeadlineScheduler.js`'s job, not this module's — `nextChunk()` here always greedily picks the LARGEST legal bucket that currently fits, which is the natural default in the absence of an explicit deadline override.


particle_voice/streaming/VoiceDeadlineScheduler

Summary: VoiceDeadlineScheduler.js — Phase 2 ParticleVoice audio bridge. "VoiceDeadlineScheduler.js — target >=300 ms, warning 200 ms, emergency 100 ms; GPU budget admission." These three thresholds classify how much PLAYBACK RUNWAY remains — how far ahead of the AudioContext's current playback position the produced-but-not-yet-played PCM currently extends (`bufferedAheadMs`, typically derived from `SharedPCMRing.js`'s `readAvailable()` via `bufferedAheadMsFromRing()` below) — into an urgency tier, and maps that tier onto EXISTING scheduling primitives rather than inventing new ones: - `NeuralScheduler.js`'s `'high'|'normal'|'low'` priority buckets (queue ordering among pending `ParticleProgram` runs). - `engine/core/gpu/GpuFrameBudgetBroker.js`'s `workClass === 'render-critical'` bypass (`beforeSubmit()`'s own docstring: "Render- critical work bypasses the budget") — this IS the "GPU budget admission" the plan names: in the `emergency` tier, this module recommends `workClass: 'render-critical'` so the broker admits the work immediately regardless of the current frame's compute budget, rather than letting a nearly-starved voice ring wait behind unrelated background GPU work. Tier boundaries (this module's own `v0` interpretation of the plan's three named thresholds — documented explicitly since the plan states three thresholds but only two transitions are unambiguous from the wording alone): bufferedAheadMs >= 300         -> 'target'      (healthy, target met) 200 <= bufferedAheadMs < 300   -> 'belowTarget'  (below target, not yet urgent) 100 <= bufferedAheadMs < 200   -> 'warning' bufferedAheadMs < 100          -> 'emergency'


bufferedAheadMsFromRing

Summary: Composes with `SharedPCMRing.js`'s `readAvailable()` — the natural real-world source of `bufferedAheadMs` for a live audio bridge — rather than re-deriving ring-position arithmetic here.


particle_voice/streaming/VoicePlayback

Summary: VoicePlayback.js — Phase 3 ParticleVoice audio bridge. The last hop of the Phase 3 path: model PCM → resample → `SharedPCMRing` → `AudioWorklet` → audible speech. Every piece it composes was built and verified in Phase 2; this module only wires them, which is why it is small. ParticleVoiceModel (32 kHz) → PolyphaseResampler (32 kHz → AudioContext.sampleRate) → SafetyLimiter is already applied upstream by the model → SharedPCMRing (SAB, single-producer/single-consumer) → ParticleVoiceProcessor (AudioWorkletProcessor) ## The resample is mandatory, not optional The model's internal DSP rate is 32 kHz (R6's proven `SAMPLE_RATE`), but an `AudioContext` picks its own rate — commonly 48 kHz, sometimes 44.1 kHz. Writing 32 kHz samples into a 48 kHz ring without resampling does not error: it plays back **1.5× too fast and a fifth too high**, which sounds like a chipmunk rather than like a bug. `PolyphaseResampler` handles the exact-integer L/M case (32k→48k reduces to L=3, M=2) and is streaming-correct across chunk boundaries, so feeding it per-utterance or per-chunk gives identical output. ## Backpressure is the producer's problem `writeSharedPCMRing()` CLAMPS to available space and returns how much it actually accepted — it never overwrites unread audio. A producer that ignores the return value silently drops the tail of every utterance that does not fit. `enqueue()` therefore loops, awaiting drain, until everything has been accepted, and reports the total written so a caller can assert it. ## What this module deliberately does NOT do It does not own the `AudioContext`. Creating and resuming one requires a user gesture in a real page, its lifetime belongs to the application, and a test harness needs to build its own — so the context and worklet node are passed in via `attach()`. That also keeps this module testable without assuming any particular page structure.


particle_voice/worklet/ParticleVoiceProcessor

Summary: ParticleVoiceProcessor.js — Phase 2 ParticleVoice audio bridge. Production AudioWorkletProcessor consuming `SharedPCMRing.js`'s ring, generalizing `risk/R4ReadbackAudioWorklet.worklet.js` (R4)'s proven allocation-free `process()` (no `new`, no object/array literals, no `await`, no `throw`, no `JSON` — only typed-array indexing and Atomics on pre-existing SharedArrayBuffer-backed views) with the plan's generation-aware resync requirement `SharedPCMRing.js`'s docstring describes. This file is deliberately SELF-CONTAINED (no ES module imports), matching this project's two existing worklet files (`engine/audio/synth/PatchRunner.worklet.js`, `risk/R4ReadbackAudioWorklet.worklet.js`) — `SharedPCMRing.js`'s `CONTROL_INDEX` constants are duplicated here as `CONTROL_INDEX` rather than imported; the two must be kept in sync by hand (flagged in both files' docstrings) rather than relying on an unverified assumption that `AudioWorkletGlobalScope` module imports behave identically to this project's other worklet loading paths. Control layout (Int32Array, SharedArrayBuffer, 6 x int32) — MUST match `SharedPCMRing.js`'s `CONTROL_INDEX` exactly: [0] writeIndex        — next PCM slot the producer will write [1] readIndex         — next PCM slot this processor will read [2] underrunCount     — incremented whenever fewer than one quantum of samples are available [3] processCallCount  — incremented every process() call [4] generation        — bumped by the producer's resetSharedPCMRing(); on change, this processor jumps readIndex to the CURRENT writeIndex (dropping stale pre-reset audio) instead of continuing from a readIndex that may now point at discarded samples. [5] writeSequence     — producer chunk counter; read-only from here, not required for correctness, exposed for parity with SharedPCMRing.js's layout only.


persistence/CheckpointContracts

Summary: Accept legacy {config,parameters} documents and return canonical v1 data.


prepareCheckpointEnvelope

Summary: Validate both canonical v1 and the pre-envelope checkpoint shape.


rig/BrainVisualizer

Summary: Brain Visualizer Visualizes neural network activity on the brain sphere


rig/InjectionSystem

Summary: Injection System Manages tentacle attachment from brain to ragdoll bones


rig/ParasiteRig

Summary: Parasite Rig The "brain sphere" entity that hovers above and controls the ragdoll


rig/TentacleRenderer

Summary: Tentacle Renderer Renders tentacles using lightning/electricity effect


scene/DebugVisualizer

Summary: Debug Visualizer Renders debug overlays for training visualization


scene/GroundPlane

Summary: Ground Plane Infinite grid ground for training environment


scene/SceneRenderer


scene/TrackingCamera

Summary: Tracking Camera Smooth camera that follows the ragdoll


scene/TrainingScene


studio/core/StudioApp

Summary: Studio Application Main AGI Studio workspace application


studio/core/StudioDirectoryHandleStore

Summary: Versioned IndexedDB owner for AGI Studio directory handles.


AGIStudioHandleStoreError

class AGIStudioHandleStoreError extends Error


prepareAGIStudioDirectoryHandleRecord

Summary: Validate a raw v1 handle or exact v2 envelope without serializing the handle.


studio/main


studio/ui/LoadingProgress

Summary: Loading Progress UI - Shows what's loading and progress


tensor/ComputeGraph

Summary: Compute Graph Manages operation chaining and execution order for tensor operations


tensor/GradientTape

Summary: Gradient Tape - Automatic Differentiation Records operations for backward pass computation


GradientContext

Summary: Context manager for gradient tape


tensor/Tensor

Summary: GPU-resident float32 tensor with explicit readback and ownership.


tensor/TensorCache

Summary: Tensor Cache - Memory Management for GPU Tensors Manages tensor buffer lifecycle and memory reuse


tensor/TensorOps

Summary: GPU-resident float32 elementwise and reduction operations.


tensor/TensorOpsAdvanced


tensor/shaders/activation

Summary: Activation Function Compute Shaders ReLU, Tanh, Sigmoid, and their derivatives


createActivationShader

createActivationShader(activationType = 'relu', workgroupSize = 256)


createActivationDerivativeShader

createActivationDerivativeShader(activationType = 'relu', workgroupSize = 256)


tensor/shaders/matmul

Summary: Matrix Multiplication Compute Shader C = A * B where A is [M x K] and B is [K x N]


tensor/shaders/reduction

Summary: Reduction Operations Compute Shaders Sum, Max, Min reductions along axes


ui/MetricsDisplay

Summary: Metrics Display Real-time training statistics display


ui/TrainingUI

Summary: Training UI Main control panel for training interface


EditorApp

Summary: EditorApp - Main application controller (Modular) Uses lazy loading for heavy modules to improve initial load time


ProjectManager

Summary: ProjectManager - Handles project folder selection and initialization


components/AudioEditorPanel

Summary: AudioEditorPanel.js - Main Audio Editor Panel Composes NodeGraph + Palette + Inspector + Materials + PreviewControls into a single editor tab panel for the engine editor.


createAudioEditorPanel

createAudioEditorPanel(parentContainer, options = {})

Summary: Create the audio editor panel.


destroyAudioEditorPanel

Summary: Destroy the audio editor panel.


components/AudioMotionTimeline

Summary: AudioMotionTimeline.js - Sound motion/automation timeline for Audio Editor V1 feature set: - Per-parameter tracks (nodeId + param) - Keyframe authoring (time, value, easing) - Loop + duration + playhead scrubbing - Runtime evaluation using AnimationTrack


createAudioMotionTimeline

createAudioMotionTimeline(container, callbacks = {})


setMotionPatch

setMotionPatch(timeline, motionData, options = {})


components/AudioNodeGraph

Summary: AudioNodeGraph.js - Canvas-Based Node Graph Editor Draggable nodes, wire connections (audio=thick, control=thin/dashed), zoom/pan, selection, and serialization to/from patch JSON.


createAudioNodeGraph

createAudioNodeGraph(container, callbacks = {})

Summary: Create an audio node graph editor.


addNode

Summary: Add a node to the graph.


removeNode

Summary: Remove a node and its wires.


addWire

Summary: Add a wire between two ports.


removeWire

Summary: Remove a wire.


duplicateNode

Summary: Duplicate a node (offset position by 20,20).


disconnectNode

Summary: Disconnect all wires from a node.


fitToView

Summary: Fit view to show all nodes.


setNodeTypes

Summary: Register available node types.


selectNode

Summary: Select a node.


importPatch

Summary: Import patch JSON into graph.


renderGraph

Summary: Render the entire graph.


startGraphAnimation

Summary: Start the animation loop for live waveform + wire animation.


stopGraphAnimation

Summary: Stop animation loop.


setNodeAnalysers

Summary: Set per-node analyser taps for live waveform display.


setMinimapContainer

Summary: Set an external container for the minimap (rendered separately from the main canvas).


destroyAudioNodeGraph

Summary: Destroy the graph editor.


components/AudioNodeInspector

Summary: AudioNodeInspector.js - Right Panel for Selected Node Parameters Shows parameter knobs/sliders for the selected node. Uses SoundSchema when available for rich typed controls. Falls back to PARAM_HINTS for legacy/unschemaed node types.


createAudioNodeInspector

Summary: Create the node inspector panel.


updateInspector

Summary: Update the inspector to show a node's parameters.


destroyAudioNodeInspector

Summary: Destroy the inspector.


components/AudioNodePalette

Summary: AudioNodePalette.js - Side Panel with Node Types Lists all available node types grouped by category. Supports drag-to-create from palette onto the graph canvas.


createAudioNodePalette

Summary: Create the node palette panel.