github gbozo/no-sdr v1.2.0
v1.2.0 — Server performance + audio pop fixes

latest releases: v2.10.5, v2.10.4, v2.10.3...
5 months ago

What's changed

Server performance

  • Float32Array-native FFT — replaced fft.js (which required plain number[] arrays V8 cannot optimise) with a self-contained radix-2 Cooley-Tukey FFT operating entirely on Float32Array. Pre-computed twiddle factors and bit-reversal table allocated once per instance.
  • Async deflate — deflateRawSync replaced with promisify(deflateRaw). Compression now runs on libuv's thread pool; was blocking the Node.js event loop for up to 150 ms/sec at N=65536 bins.
  • NCO lookup tables in FmStereoDemod — Math.sin(pllPhase) and Math.cos(2×pllPhase) replaced with 4096-entry Float32Array table + bitwise-AND index wrap. Eliminates ~480,000 trig calls/sec per WFM client. cos(2θ) computed via double-angle identity 2cos²θ−1 from the same table entry.
  • NCO lookup table in RdsDecoder — Math.cos(ncoPhase) at 240 kHz replaced with same table pattern. Eliminates ~240,000 calls/sec.
  • fastAtan2 for FM discriminator — 3rd-order minimax polynomial approximation replaces Math.atan2 in FmStereoDemod and FmMonoDemod. Max error ~0.005 rad; ~4–6× faster than V8's libm call.
  • SimpleFir Float64→Float32 — inner 51-tap MAC loop (24.5M iter/sec for WFM) now uses Float32Array; halves memory bandwidth, enables V8 SIMD paths.
  • pendingFrame pre-allocated — FftProcessor no longer allocates new Float32Array(fftSize) 30×/sec; eliminates 7.5 MB/sec of GC pressure at N=65536.
  • CQuamStereoDemod output buffers pre-allocated — was allocating two Float32Arrays per process() call (750 KB/sec GC pressure removed).
  • ImaAdpcmEncoder zero-alloc path — encode() now accepts an optional pre-allocated output buffer; IQ hot path passes a per-client scratch buffer.
  • encodeFftAdpcm singleton encoder — module-level ImaAdpcmEncoder instance replaces new ImaAdpcmEncoder() per FFT frame.
  • generateSignalSample inlined — demo mode no longer allocates ~20,000 {i,q} objects per 33 ms interval.

Audio pop / break fixes

  • Buffer corruption after async deflate — IQ extraction loop moved before the first await in _handleIqDataAsync with a defensive Buffer.from(rawData) copy. Node.js stream internals reuse the read buffer before an await resumes; this was silently corrupting IQ data and producing audio pops at ~2–3/sec.
  • Async deflate outside client iterator — deflate payloads are now computed before entering the client loop (safe Map iteration, no await inside the iterator).
  • Stale IQ on tune — iqAccumBuffer.fill(0) added alongside iqAccumPos = 0 on tune command. Old frequency data was bleeding into the first chunk after a retune.
  • Jitter buffer single-underrun pop — AudioWorklet now requires 3 consecutive underruns before resetting playing to false. A single late IQ chunk previously caused 150 ms of hard silence (perceived as a loud pop/click).
  • Opus channel snapshot — OpusAudioPipeline.process() snapshots this.channels into a local constant before the PCM accumulation loop. A mid-call stereo↔mono switch could previously corrupt pcmBufferPos and produce malformed Opus frames.
  • Dead decimCounterR removed — FmStereoDemod had an unused counter that was incremented but never read.

Don't miss a new no-sdr release

NewReleases is sending notifications on new releases.