From 09d5732b55333ac991ffdefa105af6dcfb1a839a Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sun, 6 Sep 2026 18:46:42 -0400 Subject: [PATCH] fix(cava): identical init re-uses the live plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cava_init/destroy churn leaks the old plan's FFTW work buffers — upstream frees only its own struct. init() now serializes the config and no-ops when unchanged, so pipeline restarts (focus/episode churn) keep the live plan instead of leaking a new one each cycle. --- src/utils/cavacore.ts | 18 ++++++++-- tests/cavacore-init-reuse.test.ts | 58 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 tests/cavacore-init-reuse.test.ts diff --git a/src/utils/cavacore.ts b/src/utils/cavacore.ts index f659540..1fe64e6 100644 --- a/src/utils/cavacore.ts +++ b/src/utils/cavacore.ts @@ -95,6 +95,8 @@ export class CavaCore { private _bars = 0; private _channels = 1; private _destroyed = false; + /** Serialized last init config — identical init() calls are no-ops. */ + private lastConfigKey = ""; /** Use loadCavaCore() instead of constructing directly. */ constructor(lib: CavaLib) { @@ -112,15 +114,25 @@ export class CavaCore { /** * Initialize the cavacore engine with the given configuration. - * Must be called before execute(). Can be called again after destroy() - * to reinitialize with different parameters. + * Must be called before execute(). Identical configs are a no-op: + * cava_init/destroy churn leaks the old plan's FFTW work buffers + * (upstream frees only its own struct), so a pipeline restart with + * unchanged bars/rate/cutoffs must re-USE the live plan. */ init(config: CavaCoreConfig = {}): void { + const cfg = { ...DEFAULTS, ...config }; + if ( + this.plan !== null && + !this._destroyed && + this.lastConfigKey === JSON.stringify(cfg) + ) { + return; + } + this.lastConfigKey = JSON.stringify(cfg); if (this.plan) { this.destroy(); } - const cfg = { ...DEFAULTS, ...config }; this._bars = cfg.bars; this._channels = cfg.channels; diff --git a/tests/cavacore-init-reuse.test.ts b/tests/cavacore-init-reuse.test.ts new file mode 100644 index 0000000..ca732bb --- /dev/null +++ b/tests/cavacore-init-reuse.test.ts @@ -0,0 +1,58 @@ +/** + * CavaCore.init() must be a no-op for an identical config: re-initializing + * the same plan leaks the old plan's native FFTW work buffers, so pipeline + * restarts (focus/episode churn) have to re-USE the live plan. + */ +import { test, expect } from "bun:test" +import { CavaCore } from "../src/utils/cavacore" + +function stubLib() { + const calls = { init: 0, destroy: 0 } + let plans = 0 + const lib = { + symbols: { + cava_init: () => { + calls.init++ + return { p: ++plans } + }, + cava_execute: () => {}, + cava_destroy: () => { + calls.destroy++ + }, + }, + close: () => {}, + } + return { lib, calls } +} + +test("identical init config reuses the plan", () => { + const { lib, calls } = stubLib() + const cava = new CavaCore(lib as never) + const cfg = { bars: 64, sampleRate: 22050, channels: 1, autosens: 0 } + cava.init(cfg) + cava.init(cfg) + cava.init(cfg) + expect(calls.init).toBe(1) + expect(cava.isReady).toBe(true) +}) + +test("changed config re-inits, destroying the old plan", () => { + const { lib, calls } = stubLib() + const cava = new CavaCore(lib as never) + cava.init({ bars: 64, sampleRate: 22050, channels: 1, autosens: 0 }) + cava.init({ bars: 32, sampleRate: 22050, channels: 1, autosens: 0 }) + expect(calls.init).toBe(2) + expect(calls.destroy).toBe(1) + expect(cava.bars).toBe(32) +}) + +test("init after destroy creates a fresh plan", () => { + const { lib, calls } = stubLib() + const cava = new CavaCore(lib as never) + const cfg = { bars: 64, sampleRate: 22050, channels: 1, autosens: 0 } + cava.init(cfg) + cava.destroy() + cava.init(cfg) + expect(calls.init).toBe(2) + expect(cava.isReady).toBe(true) +})