1/**
2 * A fixed latent randomness underlying everything: standard normals (and
3 * dither uniforms) indexed by absolute sample position. The pipeline
4 * x → h*x → (+dither) → round is evaluated on demand against these indices,
5 * so changing σ, the filter, or dither transforms the *same* underlying data
6 * — the display morphs smoothly instead of resampling — and the compression
7 * block (indices 0…N) shares its randomness with the displayed window.
8 */
9import { GaussianStream } from './random'
11export const LATENT_SEED = 20260729
13export class LatentSource {
14 private xs: number[] = []
15 private ds: number[] = []
16 private xStream: GaussianStream
17 private dStream: GaussianStream
19 constructor(seed: number) {
20 this.xStream = new GaussianStream(seed)
21 this.dStream = new GaussianStream((seed ^ 0x9e3779b9) >>> 0)
22 }
24 private ensure(n: number) {
25 while (this.xs.length <= n) {
26 this.xs.push(this.xStream.normal())
27 this.ds.push(this.dStream.uniformCentered())
28 }
29 }
31 /**
32 * Quantized samples for absolute indices [start, start + count). The kernel
33 * is applied zero-phase (centered on its midpoint), so changing its length
34 * does not shift features along the time axis. Latent indices before 0 read
35 * as zero input.
36 */
37 window(
38 start: number,
39 count: number,
40 kernel: Float64Array,
41 sigma: number,
42 dither: boolean,
43 ): Int16Array {
44 const L = kernel.length
45 const mid = (L - 1) >> 1
46 this.ensure(start + count - 1 + mid)
47 const { xs, ds } = this
48 const out = new Int16Array(count)
49 for (let j = 0; j < count; j++) {
50 const n = start + j
51 let y = 0
52 for (let k = 0; k < L; k++) {
53 const idx = n - k + mid
54 if (idx >= 0) y += kernel[k] * xs[idx]
55 }
56 y *= sigma
57 if (dither) y += ds[n]
58 out[j] = Math.max(-32768, Math.min(32767, Math.round(y)))
59 }
60 return out
61 }
62}