Initial Commit
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
AUDIO_FX_CODES,
|
||||
type AudioFxError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import { DefaultBusRegistry } from "./bus-registry.js";
|
||||
|
||||
function frame(samples: number[], channels = 1): Float32Array[] {
|
||||
const out: Float32Array[] = [];
|
||||
for (let ch = 0; ch < channels; ch++) {
|
||||
out.push(Float32Array.from(samples));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
describe("DefaultBusRegistry: publish + read round-trip", () => {
|
||||
it("returns undefined for an unknown bus", { timeout: 5_000 }, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
expect(r.get("missing")).toBeUndefined();
|
||||
expect(r.list()).toEqual([]);
|
||||
});
|
||||
|
||||
it("publishes a named bus reachable via get + list", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const pub = r.publish("kick", 2);
|
||||
expect(pub.bus.name).toBe("kick");
|
||||
expect(pub.bus.channels).toBe(2);
|
||||
expect(r.get("kick")).toBe(pub.bus);
|
||||
expect(r.list()).toEqual(["kick"]);
|
||||
});
|
||||
|
||||
it("round-trips a buffer from write to read", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const pub = r.publish("kick", 1);
|
||||
pub.write(frame([0.1, 0.2, 0.3]));
|
||||
const scratch = [new Float32Array(3)];
|
||||
r.get("kick")!.read(scratch);
|
||||
expect(Array.from(scratch[0]!)).toEqual([
|
||||
expect.closeTo(0.1, 6),
|
||||
expect.closeTo(0.2, 6),
|
||||
expect.closeTo(0.3, 6),
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads silence before any write", { timeout: 5_000 }, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
r.publish("kick", 1);
|
||||
const scratch = [new Float32Array(4)];
|
||||
r.get("kick")!.read(scratch);
|
||||
expect(Array.from(scratch[0]!)).toEqual([0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
it("pads with silence when scratch is larger than the written frame", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const pub = r.publish("kick", 1);
|
||||
pub.write(frame([1, 1]));
|
||||
const scratch = [new Float32Array(4)];
|
||||
r.get("kick")!.read(scratch);
|
||||
expect(Array.from(scratch[0]!)).toEqual([1, 1, 0, 0]);
|
||||
});
|
||||
|
||||
it("reuses the accumulator when consecutive writes are the same size", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const pub = r.publish("kick", 1);
|
||||
pub.write(frame([1, 2, 3]));
|
||||
pub.write(frame([4, 5, 6])); // same size — accumulator reused
|
||||
const scratch = [new Float32Array(3)];
|
||||
r.get("kick")!.read(scratch);
|
||||
expect(Array.from(scratch[0]!)).toEqual([4, 5, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefaultBusRegistry: channel-count enforcement", () => {
|
||||
it("throws BUS_CHANNEL_MISMATCH when read scratch length differs", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
r.publish("kick", 2);
|
||||
try {
|
||||
r.get("kick")!.read([new Float32Array(4)]);
|
||||
} catch (error) {
|
||||
expect((error as AudioFxError).code).toBe(
|
||||
AUDIO_FX_CODES.BUS_CHANNEL_MISMATCH
|
||||
);
|
||||
expect((error as AudioFxError).context).toEqual({
|
||||
bus: "kick",
|
||||
expected: 2,
|
||||
actual: 1,
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw new Error("expected BUS_CHANNEL_MISMATCH");
|
||||
});
|
||||
|
||||
it("throws BUS_CHANNEL_MISMATCH when write frame length differs", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const pub = r.publish("kick", 1);
|
||||
try {
|
||||
pub.write(frame([1, 1], 2));
|
||||
} catch (error) {
|
||||
expect((error as AudioFxError).code).toBe(
|
||||
AUDIO_FX_CODES.BUS_CHANNEL_MISMATCH
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error("expected BUS_CHANNEL_MISMATCH");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefaultBusRegistry: close semantics", () => {
|
||||
it("close removes the bus from get/list and reads as silence", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const pub = r.publish("kick", 1);
|
||||
const bus = pub.bus;
|
||||
pub.write(frame([1, 1]));
|
||||
pub.close();
|
||||
expect(r.get("kick")).toBeUndefined();
|
||||
expect(r.list()).toEqual([]);
|
||||
// The local bus handle is still callable but reads silence.
|
||||
const scratch = [new Float32Array(2)];
|
||||
bus.read(scratch);
|
||||
expect(Array.from(scratch[0]!)).toEqual([0, 0]);
|
||||
});
|
||||
|
||||
it("close is idempotent", { timeout: 5_000 }, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const pub = r.publish("kick", 1);
|
||||
pub.close();
|
||||
// second close must be a no-op
|
||||
expect(() => pub.close()).not.toThrow();
|
||||
});
|
||||
|
||||
it("write on a closed publisher is a no-op", { timeout: 5_000 }, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const pub = r.publish("kick", 1);
|
||||
pub.close();
|
||||
expect(() => pub.write(frame([1]))).not.toThrow();
|
||||
});
|
||||
|
||||
it("republishing the same name closes the previous publisher", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const first = r.publish("kick", 1);
|
||||
const events: Array<unknown> = [];
|
||||
r.subscribe("kick", (b) => events.push(b));
|
||||
const second = r.publish("kick", 2);
|
||||
expect(r.get("kick")).toBe(second.bus);
|
||||
expect(first.bus.channels).toBe(1);
|
||||
// subscribers see: close of first (undefined), then second (bus)
|
||||
expect(events.length).toBe(2);
|
||||
expect(events[0]).toBeUndefined();
|
||||
expect(events[1]).toBe(second.bus);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefaultBusRegistry: subscribe", () => {
|
||||
it("fires the listener on publish and on close", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const events: Array<unknown> = [];
|
||||
const unsub = r.subscribe("kick", (b) => events.push(b));
|
||||
const pub = r.publish("kick", 1);
|
||||
pub.close();
|
||||
unsub();
|
||||
expect(events.length).toBe(2);
|
||||
expect(events[0]).toBe(pub.bus);
|
||||
expect(events[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("multiple listeners on the same name all fire", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
let countA = 0;
|
||||
let countB = 0;
|
||||
r.subscribe("kick", () => countA++);
|
||||
r.subscribe("kick", () => countB++);
|
||||
r.publish("kick", 1);
|
||||
expect(countA).toBe(1);
|
||||
expect(countB).toBe(1);
|
||||
});
|
||||
|
||||
it("unsubscribe removes only that listener; cleans up empty buckets", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
let countA = 0;
|
||||
let countB = 0;
|
||||
const unsubA = r.subscribe("kick", () => countA++);
|
||||
r.subscribe("kick", () => countB++);
|
||||
unsubA();
|
||||
r.publish("kick", 1);
|
||||
expect(countA).toBe(0);
|
||||
expect(countB).toBe(1);
|
||||
});
|
||||
|
||||
it("subscribe before any publish is harmless", {
|
||||
timeout: 5_000,
|
||||
}, () => {
|
||||
const r = new DefaultBusRegistry();
|
||||
const events: Array<unknown> = [];
|
||||
r.subscribe("kick", (b) => events.push(b));
|
||||
expect(events.length).toBe(0);
|
||||
r.publish("kick", 1);
|
||||
expect(events.length).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user