fix(cava): identical init re-uses the live plan
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.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
58
tests/cavacore-init-reuse.test.ts
Normal file
58
tests/cavacore-init-reuse.test.ts
Normal file
@@ -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)
|
||||
})
|
||||
Reference in New Issue
Block a user