@audio/spectral-pvoc 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +3 -2
  2. package/index.d.ts +6 -0
  3. package/package.json +1 -1
  4. package/pvoc.js +236 -39
package/README.md CHANGED
@@ -13,8 +13,9 @@ import { findPeaks, scatterLocked, lockPhase, makeFrameRatio } from '@audio/spec
13
13
  | `scatterGated(mag, phase, prevPhase, ratio, ctx, …scratch)` | Bernsee/SMB peak-gated bin scatter — pitch shift by ratio, RMS-preserving collisions |
14
14
  | `scatterLocked(mag, phase, prevPhase, reset, peaks, ratio, ctx, …scratch)` | Laroche-Dolson rigid-ROI scatter — phase-coherent shift |
15
15
  | `lockPhase(phase, propPhase, mag, half)` | lock non-peak bins to their peak's rotation (time-stretch coherence) |
16
- | `lockMap(mag, half, owner)` | which peak each bin locks to (−1: free) — the regions `lockPhase` uses |
17
- | `lockState(half)` / `lockAdvance(re, im, st, reset, anaHop, synHop, freqPerBin, half)` | the phase-locked vocoder frame on complex bins: locked bins rotate by one complex multiply, only free bins take trig |
16
+ | `lockMap(mag, half, owner)` | which peak each bin locks to (−1: free): the regions `lockPhase` uses |
17
+ | `lockState(half)` / `lockAdvance(re, im, st, reset, anaHop, synHop, freqPerBin, half)` | the phase-locked vocoder frame on complex bins: locked bins rotate by one complex multiply, only free bins take trig; with whole hops, a WebAssembly SIMD kernel on the state's own memory, the JS loop's bits |
18
+ | `lockStep(re, im, st, reset, anaHop, synHop, freqPerBin, half)` | `lockAdvance` with the frame's magnitudes taken from its bins first |
18
19
  | `findPeaks(mag, half)` / `nearestPeak(peaks, k)` | local-maxima peak indices + nearest lookup |
19
20
  | `makeFrameRatio(ratio)` | scalar or time-varying ratio → per-frame resolver |
20
21
  | `wrapPhase(p)` / `WIN_GAIN` / `PI2` | phase wrap, Hann OLA gain constant |
package/index.d.ts CHANGED
@@ -107,3 +107,9 @@ export function lockAdvance(
107
107
  re: Float64Array, im: Float64Array, st: LockState, reset: boolean,
108
108
  anaHop: number, synHop: number, freqPerBin: number, half: number,
109
109
  ): void
110
+
111
+ /** `lockAdvance` with this frame's magnitudes, √(re² + im²), taken from its bins into `st.mag` first. */
112
+ export function lockStep(
113
+ re: Float64Array, im: Float64Array, st: LockState, reset: boolean,
114
+ anaHop: number, synHop: number, freqPerBin: number, half: number,
115
+ ): void
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audio/spectral-pvoc",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Phase-vocoder engine — peak-gated and peak-locked spectral bin scatter (Bernsee, Laroche-Dolson), phase locking, peak finding; powers @audio/shift-pvoc* and @audio/stretch-pvoc*",
5
5
  "type": "module",
6
6
  "sideEffects": false,
package/pvoc.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // - scatterGated: Bernsee/SMB peak-gated bin scatter (pitch shift by ratio)
7
7
  // - scatterLocked: Laroche-Dolson rigid-ROI peak-locked scatter (phase coherent)
8
8
  // - lockPhase: lock non-peak bin phases to their nearest peak's rotation (time stretch)
9
- // - lockMap: which peak each bin locks to — the same regions, for complex-domain locking
9
+ // - lockMap: which peak each bin locks to, the same regions, for complex-domain locking
10
10
  // - lockState / lockAdvance: the phase-locked vocoder step on complex bins (no polar round trip)
11
11
  // - findPeaks / nearestPeak / makeFrameRatio / wrapPhase: the supporting primitives
12
12
 
@@ -205,15 +205,12 @@ export function makeFrameRatio(ratio) {
205
205
  // ── Time-stretch phase locking (Laroche & Dolson 1999) ──
206
206
  // Prominence-based peak mask tuned for stretch: keeps chord partials, rejects
207
207
  // leakage shoulders. Internal to lockPhase; distinct from the shift-side findPeaks.
208
- let _peakMask = new Uint8Array(0)
209
- function peakMask(mag, half) {
210
- if (_peakMask.length !== half + 1) _peakMask = new Uint8Array(half + 1)
211
- let peaks = _peakMask
212
- peaks.fill(0)
208
+ // The peaks in order into `list`, their count returned (an adjacent louder one takes the last one's place).
209
+ function peakList(mag, half, list) {
213
210
  if (half <= 1) {
214
- peaks[0] = 1
215
- if (half === 1) peaks[1] = 1
216
- return peaks
211
+ list[0] = 0
212
+ if (half === 1) { list[1] = 1; return 2 }
213
+ return 1
217
214
  }
218
215
 
219
216
  let maxMag = 0
@@ -221,7 +218,7 @@ function peakMask(mag, half) {
221
218
 
222
219
  let minMag = Math.max(1e-8, maxMag * 0.015)
223
220
  let minProm = Math.max(1e-9, maxMag * 0.003)
224
- let lastPeak = -2, lastPeakMag = 0
221
+ let lastPeak = -2, lastPeakMag = 0, n = 0
225
222
 
226
223
  for (let k = 1; k < half; k++) {
227
224
  let value = mag[k]
@@ -231,39 +228,40 @@ function peakMask(mag, half) {
231
228
  if (value - shoulder < minProm && value < maxMag * 0.1) continue
232
229
 
233
230
  if (k - lastPeak <= 1) {
234
- if (value > lastPeakMag) { peaks[lastPeak] = 0; peaks[k] = 1; lastPeak = k; lastPeakMag = value }
231
+ if (value > lastPeakMag) { list[n - 1] = k; lastPeak = k; lastPeakMag = value }
235
232
  continue
236
233
  }
237
- peaks[k] = 1; lastPeak = k; lastPeakMag = value
234
+ list[n++] = k; lastPeak = k; lastPeakMag = value
238
235
  }
239
236
 
240
- let found = false
241
- for (let k = 0; k <= half; k++) if (peaks[k]) { found = true; break }
242
- if (!found) {
237
+ if (!n) {
243
238
  let best = 0
244
239
  for (let k = 1; k <= half; k++) if (mag[k] > mag[best]) best = k
245
- peaks[best] = 1
240
+ list[n++] = best
246
241
  }
247
- return peaks
242
+ return n
248
243
  }
249
244
 
250
245
  // Which bins ride which peak: `owner[k]` = the peak whose rotation bin k takes (its region of
251
- // influence reaches halfway to the neighbouring peaks), or -1 for a free bin — a peak itself, or a
246
+ // influence reaches halfway to the neighbouring peaks), or -1 for a free bin: a peak itself, or a
252
247
  // bin under 3% of its peak's magnitude, which keeps its own phase advance. Returns `owner`.
253
- let _peakBins = new Int32Array(0)
248
+ // The regions tile 0…half (each starts where the one before ends), so every bin is set once; the free bins and the
249
+ // locked ones are also listed in order (_free, _locked; _nf, _nl of them), for lockAdvance to run through.
250
+ let _peakBins = new Int32Array(0), _free = new Int32Array(0), _locked = new Int32Array(0), _nf = 0, _nl = 0
254
251
  export function lockMap(mag, half, owner) {
255
- let peaks = peakMask(mag, half)
256
- if (_peakBins.length < half + 1) _peakBins = new Int32Array(half + 1)
257
- let peakBins = _peakBins, nBins = 0
258
- for (let k = 0; k <= half; k++) if (peaks[k]) peakBins[nBins++] = k
259
- owner.fill(-1, 0, half + 1)
252
+ if (_peakBins.length < half + 1) { _peakBins = new Int32Array(half + 1); _free = new Int32Array(half + 1); _locked = new Int32Array(half + 1) }
253
+ let peakBins = _peakBins, nBins = peakList(mag, half, peakBins), free = _free, locked = _locked, nf = 0, nl = 0
260
254
  for (let i = 0; i < nBins; i++) {
261
255
  let pk = peakBins[i]
262
256
  let start = i === 0 ? 0 : Math.floor((peakBins[i - 1] + pk) * 0.5) + 1
263
257
  let end = i === nBins - 1 ? half : Math.floor((pk + peakBins[i + 1]) * 0.5)
264
258
  let lockFloor = Math.max(1e-10, mag[pk] * 0.03)
265
- for (let k = start; k <= end; k++) if (k !== pk && !(mag[k] < lockFloor)) owner[k] = pk
259
+ for (let k = start; k <= end; k++) {
260
+ if (k !== pk && !(mag[k] < lockFloor)) { owner[k] = pk; locked[nl++] = k }
261
+ else { owner[k] = -1; free[nf++] = k }
262
+ }
266
263
  }
264
+ _nf = nf; _nl = nl
267
265
  return owner
268
266
  }
269
267
 
@@ -281,21 +279,212 @@ export function lockPhase(phase, propPhase, mag, half) {
281
279
  // ── Complex-domain phase locking ──
282
280
  // The propagate-then-lockPhase vocoder step on complex bins (pair with fourier-transform/stft's `complex` frames).
283
281
  // A bin riding a peak takes the peak's rotation R = Y(p)·conj(X(p))/|X(p)|², Y(k) = X(k)·R: one complex multiply,
284
- // the identity locking of Laroche & Dolson. Only free bins — the peaks, and bins too quiet to lock — advance at
282
+ // the identity locking of Laroche & Dolson. Only free bins (the peaks, and bins too quiet to lock) advance at
285
283
  // their instantaneous frequency, one atan2 and one sin/cos each. State keeps unit synthesis phasors and the previous
286
284
  // analysis bins, so no bin needs its phase as an angle.
285
+ // With whole hops (the stft engines' frames start at whole samples), a free bin's advance splits in two: the bin
286
+ // centre's, ω_k·hop with ω_k = 2πk/N, a whole number of N-th turns, read from a table (no reduction of an angle of a
287
+ // thousand radians, whose rounding alone is 1e-13); and its deviation from it, within a turn: the measured product
288
+ // rotated back by the centre's analysis advance gives it as one atan2, already wrapped, and its synthesis share is
289
+ // rotated on by sin and cos of under an eighth of a turn, the quarter turns again from the table. The trigonometry is
290
+ // fdlibm's polynomials (Sun Microsystems 1993: s_atan.c, k_sin.c, k_cos.c) in basic float ops, within 2 ulp of the
291
+ // exact values and the same on every platform (Math's differ by platform in the last bit).
292
+
293
+ const S1 = -1.66666666666666324348e-01, S2 = 8.33333333332248946124e-03, S3 = -1.98412698298579493134e-04,
294
+ S4 = 2.75573137070700676789e-06, S5 = -2.50507602534068634195e-08, S6 = 1.58969099521155010221e-10
295
+ const C1 = 4.16666666666666019037e-02, C2 = -1.38888888888741095749e-03, C3 = 2.48015872894767294178e-05,
296
+ C4 = -2.75573143513906633035e-07, C5 = 2.08757232129817482790e-09, C6 = -1.13596475577881948265e-11
297
+ const T0 = 3.33333333333329318027e-01, T1 = -1.99999999998764832476e-01, T2 = 1.42857142725034663711e-01,
298
+ T3 = -1.11111104054623557880e-01, T4 = 9.09088713343650656196e-02, T5 = -7.69187620504482999495e-02,
299
+ T6 = 6.66107313738753120669e-02, T7 = -5.83357013379057348645e-02, T8 = 4.97687799461593236017e-02,
300
+ T9 = -3.65315727442169155270e-02, T10 = 1.62858201153657823623e-02
301
+ const PIO2_1 = 1.57079632673412561417e+00, PIO2_1T = 6.07710050650619224932e-11, INV_PIO2 = 6.36619772367581382433e-01
302
+ const PIO4_LO = 3.06161699786838301793e-17, PIO2_LO = 6.123233995736766e-17, PI_LO = 1.2246467991473532e-16, TAN8 = Math.SQRT2 - 1
303
+
304
+ /** atan2(y, x) of a nonzero (x, y): the octant by min/max, atan on |u| ≤ tan(π/8) by fdlibm's polynomial (its range is
305
+ * |x| < 7/16), the octant's angle added back with its low part */
306
+ function atan2(y, x) {
307
+ let ax = Math.abs(x), ay = Math.abs(y), t = Math.min(ax, ay) / Math.max(ax, ay), big = t > TAN8
308
+ let u = big ? (t - 1) / (t + 1) : t, z = u * u, w = z * z
309
+ let p = u * (z * (T0 + w * (T2 + w * (T4 + w * (T6 + w * (T8 + w * T10))))) + w * (T1 + w * (T3 + w * (T5 + w * (T7 + w * T9)))))
310
+ let r = big ? Math.PI / 4 - ((p - PIO4_LO) - u) : u - p
311
+ if (ay > ax) r = Math.PI / 2 - (r - PIO2_LO)
312
+ if (x < 0) r = Math.PI - (r - PI_LO)
313
+ return y < 0 ? -r : r
314
+ }
315
+ /** sin and cos of |x| ≤ π/4 */
316
+ const sink = x => { let z = x * x; return x + z * x * (S1 + z * (S2 + z * (S3 + z * (S4 + z * (S5 + z * S6))))) }
317
+ const cosk = x => { let z = x * x, r = z * (C1 + z * (C2 + z * (C3 + z * (C4 + z * (C5 + z * C6))))), h = 0.5 * z, w = 1 - h; return w + (((1 - w) - h) + z * r) }
318
+
319
+ /** e^(2πi·m/N) for m < N: the first eighth by the kernels, the rest by its symmetries (exact) */
320
+ const TURNS = new Map()
321
+ function turns(N) {
322
+ let t = TURNS.get(N)
323
+ if (t) return t
324
+ let c = new Float64Array(N), s = new Float64Array(N), q = N >> 2
325
+ let put = (m, x, y) => { for (let j = 0; j < 4; j++, [x, y] = [-y, x]) { c[(m + j * q) % N] = x; s[(m + j * q) % N] = y } }
326
+ for (let m = 0; m <= N >> 3; m++) { let a = 2 * Math.PI * m / N, x = cosk(a), y = sink(a); put(m, x, y); put(q - m, y, x) }
327
+ TURNS.set(N, t = { c, s })
328
+ return t
329
+ }
330
+
331
+ // pvoc.wat, compiled by wat2wasm (3,824 bytes): lockAdvance's frame with whole hops on a state's own memory, two bins to a
332
+ // float64x2 where they run alike, the same float ops in the same order as the JS below (Math's atan2, cos and sin
333
+ // imported for the bins that take them)
334
+ const WASM = new Uint8Array([
335
+ 0,97,115,109,1,0,0,0,1,46,7,96,2,124,124,1,124,96,1,124,1,124,96,1,124,1,127,96,1,123,1,123,96,2,123,123,1,123,96,7,
336
+ 127,127,127,127,127,127,127,0,96,5,127,127,127,124,127,0,2,27,3,1,109,5,97,116,97,110,50,0,0,1,109,3,99,111,115,0,1,1,
337
+ 109,3,115,105,110,0,1,3,9,8,2,1,3,3,3,4,5,6,5,3,1,0,1,6,16,3,127,1,65,0,11,127,1,65,0,11,127,1,65,0,11,7,17,2,6,109,
338
+ 101,109,111,114,121,2,0,4,108,111,99,107,0,10,10,227,28,8,20,0,32,0,68,0,0,0,0,0,0,0,0,98,32,0,32,0,97,113,11,38,0,32,
339
+ 0,32,0,68,24,45,68,84,251,33,25,64,163,68,0,0,0,0,0,0,224,63,160,156,68,24,45,68,84,251,33,25,64,162,161,11,88,1,1,
340
+ 123,32,0,253,117,33,1,32,1,32,0,32,1,253,241,1,253,12,0,0,0,0,0,0,224,63,0,0,0,0,0,0,224,63,253,76,253,12,0,0,0,0,0,0,
341
+ 240,63,0,0,0,0,0,0,240,63,253,78,253,240,1,33,1,32,0,32,1,253,12,0,0,0,0,0,0,0,128,0,0,0,0,0,0,0,128,253,82,11,176,1,
342
+ 1,1,123,32,0,32,0,253,242,1,33,1,32,0,32,1,32,0,253,242,1,253,12,73,85,85,85,85,85,197,191,73,85,85,85,85,85,197,191,
343
+ 32,1,253,12,166,248,16,17,17,17,129,63,166,248,16,17,17,17,129,63,32,1,253,12,213,97,193,25,160,1,42,191,213,97,193,
344
+ 25,160,1,42,191,32,1,253,12,125,254,177,87,227,29,199,62,125,254,177,87,227,29,199,62,32,1,253,12,235,156,43,138,230,
345
+ 229,90,190,235,156,43,138,230,229,90,190,32,1,253,12,124,213,207,90,58,217,229,61,124,213,207,90,58,217,229,61,253,
346
+ 242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,
347
+ 11,133,2,1,4,123,32,0,32,0,253,242,1,33,1,32,1,253,12,76,85,85,85,85,85,165,63,76,85,85,85,85,85,165,63,32,1,253,12,
348
+ 119,81,193,22,108,193,86,191,119,81,193,22,108,193,86,191,32,1,253,12,144,21,203,25,160,1,250,62,144,21,203,25,160,1,
349
+ 250,62,32,1,253,12,173,82,156,128,79,126,146,190,173,82,156,128,79,126,146,190,32,1,253,12,196,177,180,189,158,238,33,
350
+ 62,196,177,180,189,158,238,33,62,32,1,253,12,212,56,136,190,233,250,168,189,212,56,136,190,233,250,168,189,253,242,1,
351
+ 253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,33,2,253,12,0,0,0,
352
+ 0,0,0,224,63,0,0,0,0,0,0,224,63,32,1,253,242,1,33,3,253,12,0,0,0,0,0,0,240,63,0,0,0,0,0,0,240,63,32,3,253,241,1,33,4,
353
+ 32,4,253,12,0,0,0,0,0,0,240,63,0,0,0,0,0,0,240,63,32,4,253,241,1,32,3,253,241,1,32,1,32,2,253,242,1,253,240,1,253,240,
354
+ 1,11,135,5,1,9,123,32,1,253,236,1,33,2,32,0,253,236,1,33,3,32,2,32,3,253,244,1,32,2,32,3,253,245,1,253,243,1,33,4,32,
355
+ 4,253,12,52,239,252,153,121,130,218,63,52,239,252,153,121,130,218,63,253,74,33,5,32,4,253,12,0,0,0,0,0,0,240,63,0,0,0,
356
+ 0,0,0,240,63,253,241,1,32,4,253,12,0,0,0,0,0,0,240,63,0,0,0,0,0,0,240,63,253,240,1,253,243,1,32,4,32,5,253,82,33,6,32,
357
+ 6,32,6,253,242,1,33,7,32,7,32,7,253,242,1,33,8,32,6,32,7,253,12,13,85,85,85,85,85,213,63,13,85,85,85,85,85,213,63,32,
358
+ 8,253,12,255,131,0,146,36,73,194,63,255,131,0,146,36,73,194,63,32,8,253,12,110,32,76,197,205,69,183,63,110,32,76,197,
359
+ 205,69,183,63,32,8,253,12,81,61,208,160,102,13,177,63,81,61,208,160,102,13,177,63,32,8,253,12,235,13,118,36,75,123,
360
+ 169,63,235,13,118,36,75,123,169,63,32,8,253,12,17,218,34,227,58,173,144,63,17,218,34,227,58,173,144,63,253,242,1,253,
361
+ 240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,32,8,253,12,196,235,
362
+ 152,153,153,153,201,191,196,235,152,153,153,153,201,191,32,8,253,12,113,22,35,254,198,113,188,191,113,22,35,254,198,
363
+ 113,188,191,32,8,253,12,109,154,116,175,242,176,179,191,109,154,116,175,242,176,179,191,32,8,253,12,154,253,222,82,45,
364
+ 222,173,191,154,253,222,82,45,222,173,191,32,8,253,12,47,108,106,44,68,180,162,191,47,108,106,44,68,180,162,191,253,
365
+ 242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,253,240,1,253,242,1,33,9,253,12,
366
+ 24,45,68,84,251,33,233,63,24,45,68,84,251,33,233,63,32,9,253,12,7,92,20,51,38,166,129,60,7,92,20,51,38,166,129,60,253,
367
+ 241,1,32,6,253,241,1,253,241,1,32,6,32,9,253,241,1,32,5,253,82,33,10,253,12,24,45,68,84,251,33,249,63,24,45,68,84,251,
368
+ 33,249,63,32,10,253,12,7,92,20,51,38,166,145,60,7,92,20,51,38,166,145,60,253,241,1,253,241,1,32,10,32,3,32,2,253,74,
369
+ 253,82,33,10,253,12,24,45,68,84,251,33,9,64,24,45,68,84,251,33,9,64,32,10,253,12,7,92,20,51,38,166,161,60,7,92,20,51,
370
+ 38,166,161,60,253,241,1,253,241,1,32,10,32,1,253,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,253,73,253,82,33,10,32,10,253,237,
371
+ 1,32,10,32,0,253,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,253,73,253,82,11,230,5,2,13,127,8,124,65,0,33,7,3,64,32,1,32,7,65,
372
+ 3,116,106,43,3,0,33,20,32,20,32,22,100,4,64,32,20,33,22,11,32,7,65,1,106,34,7,32,0,76,13,0,11,68,58,140,48,226,142,
373
+ 121,69,62,32,22,68,184,30,133,235,81,184,142,63,162,165,33,23,68,149,214,38,232,11,46,17,62,32,22,68,250,126,106,188,
374
+ 116,147,104,63,162,165,33,24,65,126,33,13,65,1,33,7,2,64,3,64,32,7,32,0,78,13,1,2,64,32,1,32,7,65,3,116,106,33,17,32,
375
+ 17,43,3,0,33,20,32,20,32,23,99,13,0,32,20,32,17,65,8,107,43,3,0,99,13,0,32,20,32,17,43,3,8,99,13,0,32,17,65,8,107,43,
376
+ 3,0,32,17,43,3,8,165,32,7,65,1,74,4,124,32,17,65,16,107,43,3,0,5,68,0,0,0,0,0,0,0,0,11,32,7,65,2,106,32,0,76,4,124,32,
377
+ 17,43,3,16,5,68,0,0,0,0,0,0,0,0,11,165,165,33,26,32,20,32,26,161,32,24,99,32,20,32,22,68,154,153,153,153,153,153,185,
378
+ 63,162,99,113,13,0,32,7,32,13,107,65,1,76,4,64,32,20,32,25,100,4,64,32,6,32,8,65,1,107,65,2,116,106,32,7,54,2,0,32,7,
379
+ 33,13,32,20,33,25,11,12,1,11,32,6,32,8,65,2,116,106,32,7,54,2,0,32,8,65,1,106,33,8,32,7,33,13,32,20,33,25,11,32,7,65,
380
+ 1,106,33,7,12,0,11,11,32,8,69,4,64,65,1,33,7,3,64,32,1,32,7,65,3,116,106,43,3,0,32,1,32,16,65,3,116,106,43,3,0,100,4,
381
+ 64,32,7,33,16,11,32,7,65,1,106,34,7,32,0,76,13,0,11,32,6,32,16,54,2,0,65,1,33,8,11,65,0,33,9,3,64,32,6,32,9,65,2,116,
382
+ 106,40,2,0,33,10,32,9,4,127,32,6,32,9,65,1,107,65,2,116,106,40,2,0,32,10,106,65,1,117,65,1,106,5,65,0,11,33,11,32,9,
383
+ 32,8,65,1,107,70,4,127,32,0,5,32,10,32,6,32,9,65,1,106,65,2,116,106,40,2,0,106,65,1,117,11,33,12,68,187,189,215,217,
384
+ 223,124,219,61,32,1,32,10,65,3,116,106,43,3,0,68,184,30,133,235,81,184,158,63,162,165,33,27,32,11,33,7,2,64,3,64,32,7,
385
+ 32,12,74,13,1,32,7,65,3,116,33,17,32,1,32,17,106,43,3,0,33,20,32,7,32,10,71,32,20,32,27,99,69,113,33,18,32,3,32,7,65,
386
+ 2,116,106,32,10,65,127,32,18,27,54,2,0,32,7,65,0,71,32,7,32,0,72,113,32,20,68,0,0,0,0,0,0,0,0,98,32,20,32,20,97,113,
387
+ 32,2,32,17,106,43,3,0,34,21,68,0,0,0,0,0,0,0,0,98,32,21,32,21,97,113,113,113,33,19,32,4,32,14,65,2,116,106,32,7,54,2,
388
+ 0,32,5,32,15,65,2,116,106,32,7,54,2,0,32,14,32,18,69,32,19,113,106,33,14,32,15,32,18,69,32,19,69,113,106,33,15,32,7,
389
+ 65,1,106,33,7,12,0,11,11,32,9,65,1,106,34,9,32,8,72,13,0,11,32,14,36,0,32,15,36,1,32,8,36,2,11,161,13,3,37,127,24,124,
390
+ 14,123,32,0,65,3,106,65,126,113,33,5,32,5,65,3,116,33,6,32,0,65,1,116,33,7,32,7,65,1,107,33,8,32,6,33,11,32,6,65,2,
391
+ 108,33,12,32,6,65,3,108,33,13,32,6,65,4,108,33,14,32,6,65,5,108,33,15,32,6,65,6,108,33,16,32,6,65,7,108,33,17,32,6,65,
392
+ 8,108,33,18,32,6,65,9,108,33,19,32,6,65,10,108,33,20,32,6,65,11,108,33,21,32,6,65,12,108,33,22,32,6,65,13,108,33,23,
393
+ 32,6,65,14,108,33,24,32,6,65,15,108,33,25,32,25,32,7,65,3,116,106,33,26,32,26,32,7,65,3,116,106,33,27,32,27,32,5,65,2,
394
+ 116,106,33,28,32,28,32,5,65,2,116,106,33,29,32,29,32,5,65,2,116,106,33,30,32,0,65,1,106,65,3,116,33,9,32,4,4,64,65,0,
395
+ 33,33,3,64,32,33,253,0,4,0,33,71,32,11,32,33,106,253,0,4,0,33,72,32,12,32,33,106,32,71,32,71,253,242,1,32,72,32,72,
396
+ 253,242,1,253,240,1,253,239,1,253,11,4,0,32,33,65,16,106,34,33,32,9,72,13,0,11,11,32,0,32,12,32,15,32,27,32,28,32,29,
397
+ 32,30,16,9,35,0,33,37,35,1,33,38,35,2,33,39,32,1,183,33,42,32,2,183,33,43,65,0,33,31,2,64,3,64,32,31,32,37,78,13,1,32,
398
+ 28,32,31,65,2,116,106,40,2,0,33,32,32,32,65,3,116,33,33,32,33,43,3,0,33,48,32,11,32,33,106,43,3,0,33,49,32,13,32,33,
399
+ 106,43,3,0,33,50,32,14,32,33,106,43,3,0,33,51,32,48,32,50,162,32,49,32,51,162,160,33,54,32,49,32,50,162,32,48,32,51,
400
+ 162,161,33,55,32,32,32,1,108,32,8,113,65,3,116,33,34,32,25,32,34,106,43,3,0,33,56,32,26,32,34,106,43,3,0,33,57,32,31,
401
+ 65,3,116,33,33,32,18,32,33,106,32,55,32,56,162,32,54,32,57,162,161,57,3,0,32,19,32,33,106,32,54,32,56,162,32,55,32,57,
402
+ 162,160,57,3,0,32,31,65,1,106,33,31,12,0,11,11,32,42,253,20,33,66,32,43,253,20,33,67,65,0,33,33,32,37,65,3,116,33,34,
403
+ 2,64,3,64,32,33,32,34,78,13,1,32,18,32,33,106,253,0,4,0,32,19,32,33,106,253,0,4,0,16,8,32,66,253,243,1,32,67,253,242,
404
+ 1,33,68,32,68,253,12,131,200,201,109,48,95,228,63,131,200,201,109,48,95,228,63,253,242,1,16,5,33,69,32,68,32,69,253,
405
+ 12,0,0,64,84,251,33,249,63,0,0,64,84,251,33,249,63,253,242,1,253,241,1,32,69,253,12,49,99,98,26,97,180,208,61,49,99,
406
+ 98,26,97,180,208,61,253,242,1,253,241,1,33,70,32,20,32,33,106,32,69,253,11,4,0,32,21,32,33,106,32,70,16,7,253,11,4,0,
407
+ 32,22,32,33,106,32,70,16,6,253,11,4,0,32,33,65,16,106,33,33,12,0,11,11,65,0,33,31,2,64,3,64,32,31,32,37,78,13,1,32,28,
408
+ 32,31,65,2,116,106,40,2,0,33,32,32,31,65,3,116,33,33,32,32,32,2,108,32,20,32,33,106,43,3,0,252,2,32,7,65,2,117,108,
409
+ 106,32,8,113,65,3,116,33,35,32,21,32,33,106,43,3,0,33,58,32,22,32,33,106,43,3,0,33,59,32,25,32,35,106,43,3,0,33,60,32,
410
+ 26,32,35,106,43,3,0,33,61,32,60,32,58,162,32,61,32,59,162,161,33,46,32,60,32,59,162,32,61,32,58,162,160,33,47,32,32,
411
+ 65,3,116,33,33,32,16,32,33,106,43,3,0,33,44,32,17,32,33,106,43,3,0,33,45,32,16,32,33,106,32,44,32,46,162,32,45,32,47,
412
+ 162,161,57,3,0,32,17,32,33,106,32,44,32,47,162,32,45,32,46,162,160,57,3,0,32,31,65,1,106,33,31,12,0,11,11,65,0,33,31,
413
+ 2,64,3,64,32,31,32,38,78,13,1,32,29,32,31,65,2,116,106,40,2,0,33,32,32,32,65,3,116,33,33,32,33,43,3,0,33,48,32,11,32,
414
+ 33,106,43,3,0,33,49,32,13,32,33,106,43,3,0,33,50,32,14,32,33,106,43,3,0,33,51,32,12,32,33,106,43,3,0,16,3,32,15,32,33,
415
+ 106,43,3,0,16,3,113,4,124,32,49,32,50,162,32,48,32,51,162,161,32,48,32,50,162,32,49,32,51,162,160,16,0,5,32,49,32,48,
416
+ 16,0,32,51,32,50,16,0,161,11,33,62,32,62,32,32,183,32,3,162,32,42,162,161,16,4,33,62,32,32,183,32,3,162,32,62,32,42,
417
+ 163,160,32,43,162,33,63,32,63,16,1,33,46,32,63,16,2,33,47,32,16,32,33,106,43,3,0,33,44,32,17,32,33,106,43,3,0,33,45,
418
+ 32,16,32,33,106,32,44,32,46,162,32,45,32,47,162,161,57,3,0,32,17,32,33,106,32,44,32,47,162,32,45,32,46,162,160,57,3,0,
419
+ 32,31,65,1,106,33,31,12,0,11,11,65,0,33,31,3,64,32,30,32,31,65,2,116,106,40,2,0,33,36,32,31,4,127,32,30,32,31,65,1,
420
+ 107,65,2,116,106,40,2,0,32,36,106,65,1,117,65,1,106,5,65,0,11,33,40,32,31,32,39,65,1,107,70,4,127,32,0,5,32,36,32,30,
421
+ 32,31,65,1,106,65,2,116,106,40,2,0,106,65,1,117,11,33,41,32,36,65,3,116,33,33,32,12,32,33,106,43,3,0,33,52,32,16,32,
422
+ 33,106,43,3,0,33,44,32,17,32,33,106,43,3,0,33,45,32,33,43,3,0,33,48,32,11,32,33,106,43,3,0,33,49,32,44,32,48,162,32,
423
+ 45,32,49,162,160,32,52,163,33,64,32,45,32,48,162,32,44,32,49,162,161,32,52,163,33,65,32,64,253,20,33,74,32,65,253,20,
424
+ 33,75,32,40,65,3,116,33,33,32,41,65,1,106,65,3,116,33,34,3,64,32,23,32,33,106,32,74,253,11,4,0,32,24,32,33,106,32,75,
425
+ 253,11,4,0,32,33,65,16,106,34,33,32,34,72,13,0,11,32,31,65,1,106,34,31,32,39,72,13,0,11,65,0,33,33,3,64,32,33,253,0,4,
426
+ 0,33,71,32,11,32,33,106,253,0,4,0,33,72,32,12,32,33,106,253,0,4,0,33,73,32,23,32,33,106,253,0,4,0,33,74,32,24,32,33,
427
+ 106,253,0,4,0,33,75,32,27,32,33,65,1,118,106,253,93,3,0,253,199,1,253,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,253,219,1,33,
428
+ 77,253,12,0,0,0,0,0,0,240,63,0,0,0,0,0,0,240,63,32,73,253,243,1,33,76,32,71,32,74,253,242,1,32,72,32,75,253,242,1,253,
429
+ 241,1,32,76,253,242,1,32,16,32,33,106,253,0,4,0,32,77,253,82,33,78,32,71,32,75,253,242,1,32,72,32,74,253,242,1,253,
430
+ 240,1,32,76,253,242,1,32,17,32,33,106,253,0,4,0,32,77,253,82,33,79,32,16,32,33,106,32,78,253,11,4,0,32,17,32,33,106,
431
+ 32,79,253,11,4,0,32,13,32,33,106,32,71,253,11,4,0,32,14,32,33,106,32,72,253,11,4,0,32,15,32,33,106,32,73,253,11,4,0,
432
+ 32,33,32,73,32,78,253,242,1,253,11,4,0,32,11,32,33,106,32,73,32,79,253,242,1,253,11,4,0,32,33,65,16,106,34,33,32,9,72,
433
+ 13,0,11,11,
434
+ ])
435
+
436
+ let mod
437
+ /** A state's arrays on a WebAssembly memory of its own, with the kernel that advances them (`k`, not enumerated);
438
+ * null where WebAssembly does not run */
439
+ function wasmState(half) {
440
+ if (mod === undefined) try { mod = new WebAssembly.Module(WASM) } catch { mod = null }
441
+ if (!mod || half < 2) return null
442
+ let n = half + 1, n2 = (n + 2) & ~1, B = 8 * n2, N = 2 * half, need = 15 * B + 16 * N + 16 * n2
443
+ let { exports: x } = new WebAssembly.Instance(mod, { m: { atan2: Math.atan2, cos: Math.cos, sin: Math.sin } })
444
+ if (need > x.memory.buffer.byteLength) x.memory.grow(Math.ceil((need - x.memory.buffer.byteLength) / 65536))
445
+ let buf = x.memory.buffer, f = i => new Float64Array(buf, i * B, n)
446
+ if ((N & (N - 1)) === 0) { let { c, s } = turns(N); new Float64Array(buf, 15 * B, N).set(c); new Float64Array(buf, 15 * B + 8 * N, N).set(s) }
447
+ let st = { mag: f(2), owner: new Int32Array(buf, 15 * B + 16 * N, n), xr: f(3), xi: f(4), xm: f(5), ur: f(6), ui: f(7) }
448
+ return Object.defineProperty(st, 'k', { value: { half, re: f(0), im: f(1), lock: x.lock } })
449
+ }
287
450
 
288
451
  /** Frame-to-frame state over half+1 bins: `mag` (the caller fills this frame's magnitudes), the lock map, the
289
452
  * previous analysis bins and magnitudes, the unit synthesis phasors. */
290
453
  export function lockState(half) {
454
+ let w = wasmState(half)
455
+ if (w) return w
291
456
  let n = half + 1, f = () => new Float64Array(n)
292
457
  return { mag: f(), owner: new Int32Array(n), xr: f(), xi: f(), xm: f(), ur: f(), ui: f() }
293
458
  }
294
459
 
460
+ // whole hops on a power-of-two frame: the turns' table serves every free bin (the kernel's frames, and the JS loop's)
461
+ const whole = (N, A, S, freqPerBin) => (N & (N - 1)) === 0 && Number.isInteger(A) && Number.isInteger(S) && freqPerBin === PI2 / N
462
+ const put = (to, from, n) => to.set(from.length === n ? from : from.subarray(0, n))
463
+
464
+ /** The frame through the state's kernel, on the bins copied into its memory and back, its magnitudes first with `mags`;
465
+ * false where it does not run it (no kernel, a reset, other hops, its i32 products k·hop out of range) */
466
+ function kernelStep(re, im, st, reset, anaHop, synHop, freqPerBin, half, mags) {
467
+ let w = st.k, n = half + 1
468
+ if (!w || reset || w.half !== half || !whole(2 * half, anaHop, synHop, freqPerBin) || !(Math.abs(half * anaHop) < 2 ** 31 && Math.abs(half * synHop) < 2 ** 31)) return false
469
+ put(w.re, re, n); put(w.im, im, n)
470
+ w.lock(half, anaHop, synHop, freqPerBin, mags)
471
+ re.set(w.re); im.set(w.im)
472
+ return true
473
+ }
474
+
475
+ /** lockAdvance with this frame's magnitudes taken from its bins, √(re² + im²) into `st.mag`, first */
476
+ export function lockStep(re, im, st, reset, anaHop, synHop, freqPerBin, half) {
477
+ if (kernelStep(re, im, st, reset, anaHop, synHop, freqPerBin, half, 1)) return
478
+ let { mag } = st
479
+ for (let k = 0; k <= half; k++) mag[k] = Math.sqrt(re[k] * re[k] + im[k] * im[k])
480
+ lockAdvance(re, im, st, reset, anaHop, synHop, freqPerBin, half)
481
+ }
482
+
295
483
  /** One vocoder frame in place: `re`/`im` hold the analysis bins, then the synthesis bins. `reset` (first frame, a
296
484
  * transient) restarts each bin's synthesis phase at its analysis phase; otherwise free bins advance by their
297
485
  * instantaneous frequency over the hops and locked bins take their peak's rotation. */
298
486
  export function lockAdvance(re, im, st, reset, anaHop, synHop, freqPerBin, half) {
487
+ if (kernelStep(re, im, st, reset, anaHop, synHop, freqPerBin, half, 0)) return
299
488
  let { mag, owner, xr, xi, xm, ur, ui } = st, n = half + 1
300
489
  if (reset) for (let k = 0; k < n; k++) {
301
490
  let m = mag[k]
@@ -304,26 +493,34 @@ export function lockAdvance(re, im, st, reset, anaHop, synHop, freqPerBin, half)
304
493
  ur[k] = Math.cos(a); ui[k] = Math.sin(a)
305
494
  } else {
306
495
  lockMap(mag, half, owner)
307
- for (let k = 0; k < n; k++) {
308
- if (owner[k] >= 0) continue
309
- // phase advance since the previous frame, arg(X·conj(Xprev)); a zero bin has no product angle: its own angles
310
- let d = mag[k] && xm[k] ? Math.atan2(im[k] * xr[k] - re[k] * xi[k], re[k] * xr[k] + im[k] * xi[k]) : Math.atan2(im[k], re[k]) - Math.atan2(xi[k], xr[k])
311
- let dp = wrapPhase(d - k * freqPerBin * anaHop)
312
- let a = (k * freqPerBin + dp / anaHop) * synHop, c = Math.cos(a), s = Math.sin(a), u = ur[k], v = ui[k]
496
+ let N = 2 * half, M = N - 1, A = anaHop, S = synHop, free = _free, nf = _nf, locked = _locked, nl = _nl
497
+ let tab = whole(N, A, S, freqPerBin) ? turns(N) : null, tc = tab?.c, ts = tab?.s
498
+ for (let i = 0; i < nf; i++) {
499
+ let k = free[i], c, s, u = ur[k], v = ui[k]
500
+ if (tab && k && k < half && mag[k] && xm[k]) {
501
+ // X·conj(Xprev) rotated back by the centre's ω_k·A: e^(i·dp), the deviation, wrapped; its synthesis share th
502
+ let dr = re[k] * xr[k] + im[k] * xi[k], di = im[k] * xr[k] - re[k] * xi[k], m = (k * A) & M, wc = tc[m], ws = ts[m]
503
+ let th = atan2(di * wc - dr * ws, dr * wc + di * ws) / A * S, q = Math.round(th * INV_PIO2), r = th - q * PIO2_1 - q * PIO2_1T
504
+ // on by the centre's ω_k·S and th: their quarter turns from the table, the rest by the kernels
505
+ let j = (k * S + q * (N >> 2)) & M, cr = cosk(r), sr = sink(r)
506
+ c = tc[j] * cr - ts[j] * sr; s = tc[j] * sr + ts[j] * cr
507
+ } else {
508
+ // phase advance since the previous frame, arg(X·conj(Xprev)); a zero bin has no product angle: its own angles
509
+ let d = mag[k] && xm[k] ? Math.atan2(im[k] * xr[k] - re[k] * xi[k], re[k] * xr[k] + im[k] * xi[k]) : Math.atan2(im[k], re[k]) - Math.atan2(xi[k], xr[k])
510
+ let dp = wrapPhase(d - k * freqPerBin * anaHop), a = (k * freqPerBin + dp / anaHop) * synHop
511
+ c = Math.cos(a); s = Math.sin(a)
512
+ }
313
513
  ur[k] = u * c - v * s
314
514
  ui[k] = u * s + v * c
315
515
  }
316
- for (let k = 0, p = -1, rr = 0, ri = 0; k < n; k++) {
317
- let pk = owner[k]
318
- if (pk < 0) continue
516
+ for (let i = 0, p = -1, rr = 0, ri = 0; i < nl; i++) {
517
+ let k = locked[i], pk = owner[k]
319
518
  if (pk !== p) { p = pk; let m = mag[p]; rr = (ur[p] * re[p] + ui[p] * im[p]) / m; ri = (ui[p] * re[p] - ur[p] * im[p]) / m }
320
519
  let q = 1 / mag[k]
321
520
  ur[k] = (re[k] * rr - im[k] * ri) * q
322
521
  ui[k] = (re[k] * ri + im[k] * rr) * q
323
522
  }
324
523
  }
325
- for (let k = 0; k < n; k++) {
326
- xr[k] = re[k]; xi[k] = im[k]; xm[k] = mag[k]
327
- re[k] = mag[k] * ur[k]; im[k] = mag[k] * ui[k]
328
- }
524
+ xr.set(re.subarray(0, n)); xi.set(im.subarray(0, n)); xm.set(mag.subarray(0, n))
525
+ for (let k = 0; k < n; k++) { re[k] = mag[k] * ur[k]; im[k] = mag[k] * ui[k] }
329
526
  }