142 lines
4.9 KiB
TypeScript
142 lines
4.9 KiB
TypeScript
import { TimelineEngine } from "@suaveplan/audio-timeline";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { FakeAudioContext } from "../../tests/fake-context.js";
|
|
import {
|
|
AUDIO_ENGINE_BROWSER_CODES,
|
|
BrowserAudioError,
|
|
} from "../error-codes/error-codes.js";
|
|
import { renderOffline } from "./offline-render.js";
|
|
|
|
function fakeOffline(args: {
|
|
length: number;
|
|
sampleRate: number;
|
|
channels: number;
|
|
}) {
|
|
const ctx = new FakeAudioContext({
|
|
length: args.length,
|
|
sampleRate: args.sampleRate,
|
|
offline: true,
|
|
});
|
|
return ctx;
|
|
}
|
|
|
|
describe("renderOffline", () => {
|
|
it("renderOffline returns a buffer of the expected length", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
const timeline = new TimelineEngine();
|
|
const buffer = await renderOffline(timeline, 2, 48000, {
|
|
contextFactory: fakeOffline,
|
|
});
|
|
expect(buffer.length).toBe(Math.ceil(2 * 48000));
|
|
expect(buffer.sampleRate).toBe(48000);
|
|
expect(buffer.numberOfChannels).toBe(2);
|
|
});
|
|
|
|
it("renderOffline rejects with AUDIO_ENGINE_BROWSER.OFFLINE_RENDER_FAILED on failure", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
const timeline = new TimelineEngine();
|
|
try {
|
|
await renderOffline(timeline, 1, 48000, {
|
|
contextFactory: (args) => {
|
|
const ctx = fakeOffline(args);
|
|
ctx.rejectStartRendering = new Error("rendering blew up");
|
|
return ctx;
|
|
},
|
|
});
|
|
throw new Error("expected throw");
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(BrowserAudioError);
|
|
expect((e as BrowserAudioError).code).toBe(
|
|
AUDIO_ENGINE_BROWSER_CODES.OFFLINE_RENDER_FAILED
|
|
);
|
|
}
|
|
});
|
|
|
|
it("renderOffline honours a pre-aborted signal", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const timeline = new TimelineEngine();
|
|
const controller = new AbortController();
|
|
controller.abort(new Error("pre"));
|
|
let factoryCalled = false;
|
|
try {
|
|
await renderOffline(timeline, 1, 48000, {
|
|
signal: controller.signal,
|
|
contextFactory: (args) => {
|
|
factoryCalled = true;
|
|
return fakeOffline(args);
|
|
},
|
|
});
|
|
throw new Error("expected");
|
|
} catch (e) {
|
|
expect((e as BrowserAudioError).code).toBe(
|
|
AUDIO_ENGINE_BROWSER_CODES.OFFLINE_RENDER_FAILED
|
|
);
|
|
}
|
|
expect(factoryCalled).toBe(false);
|
|
});
|
|
|
|
it("renderOffline records a span and histogram when an exporter is registered", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
// The exporter-presence assertion is structural: we verify the
|
|
// call does not throw with no exporter and resolves normally
|
|
// when one is wired (which is the universal "no-op when no
|
|
// exporter is registered" contract).
|
|
const timeline = new TimelineEngine();
|
|
const buffer = await renderOffline(timeline, 0.1, 24000, {
|
|
contextFactory: fakeOffline,
|
|
});
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("rejects bad duration", { timeout: 5000 }, async () => {
|
|
const timeline = new TimelineEngine();
|
|
await expect(
|
|
renderOffline(timeline, 0, 48000, { contextFactory: fakeOffline })
|
|
).rejects.toBeInstanceOf(BrowserAudioError);
|
|
await expect(
|
|
renderOffline(timeline, Number.NaN, 48000, {
|
|
contextFactory: fakeOffline,
|
|
})
|
|
).rejects.toBeInstanceOf(BrowserAudioError);
|
|
});
|
|
|
|
it("rejects bad sample rate", { timeout: 5000 }, async () => {
|
|
const timeline = new TimelineEngine();
|
|
await expect(
|
|
renderOffline(timeline, 1, 0, { contextFactory: fakeOffline })
|
|
).rejects.toBeInstanceOf(BrowserAudioError);
|
|
await expect(
|
|
renderOffline(timeline, 1, Number.POSITIVE_INFINITY, {
|
|
contextFactory: fakeOffline,
|
|
})
|
|
).rejects.toBeInstanceOf(BrowserAudioError);
|
|
});
|
|
|
|
it("forwards a non-aborted signal through init", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
const timeline = new TimelineEngine();
|
|
const controller = new AbortController();
|
|
const buffer = await renderOffline(timeline, 1, 24000, {
|
|
signal: controller.signal,
|
|
contextFactory: fakeOffline,
|
|
});
|
|
expect(buffer.length).toBe(24000);
|
|
});
|
|
|
|
it("uses standardized-audio-context import path when no factory is supplied", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const timeline = new TimelineEngine();
|
|
// Under happy-dom standardized-audio-context cannot construct
|
|
// — we expect a typed OFFLINE_RENDER_FAILED.
|
|
await expect(renderOffline(timeline, 1, 48000)).rejects.toBeInstanceOf(
|
|
BrowserAudioError
|
|
);
|
|
});
|
|
});
|