mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-14 21:29:34 +00:00
Reads `data-fx-chain` off an audio element and runs the chain over the trimmed WAV before volume automation is baked in — effects should see the raw signal, and the envelope belongs on their output. The processing happens in an OfflineAudioContext inside the headless browser the engine already drives, running the same graph builders the studio previews with. That is the point of the approach: one implementation per effect, so the render agreeing with the preview is a property of the architecture rather than a tolerance to police. Reimplementing each effect as an FFmpeg filter would mean two implementations to keep in step, and for the dynamics processors and modulated delays there is no filter that behaves the same way. `build:audio-fx-runtime` bundles the graph builders into an injectable IIFE, following the same pattern as the existing runtime artifacts, so the browser runs exactly the code the studio does. The page loads from a file:// URL rather than about:blank because AudioWorklet is only exposed in a secure context — the compressor, limiter, gate and bitcrush processors would otherwise fail to register with an opaque error. file:// qualifies and needs no listening socket. The chain is serialised into the attribute the way colour grading carries its config, so there is no side-car file to resolve or lose. An FX failure is fatal for the whole mix rather than a per-track soft failure. Every other audio failure mode degrades gracefully — the track drops, siblings continue — but substituting the dry signal for a processed one ships a render that sounds plausible and is not what the author set up. Since the per-element work races under Promise.all, an internal AbortController chained off the caller's signal aborts in-flight siblings before workDir is removed.
51 lines
1.6 KiB
TypeScript
51 lines
1.6 KiB
TypeScript
/** Build the injectable audio-FX runtime artifact from the canonical runtime. */
|
|
|
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { buildSync } from "esbuild";
|
|
import { execFileSync } from "node:child_process";
|
|
|
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(thisDir, "..");
|
|
const entry = resolve(repoRoot, "stubs/audio-fx-runtime-entry.ts");
|
|
const generatedDir = resolve(repoRoot, "src/generated");
|
|
const outPath = resolve(generatedDir, "audio-fx-runtime-inline.ts");
|
|
|
|
const result = buildSync({
|
|
entryPoints: [entry],
|
|
bundle: true,
|
|
write: false,
|
|
platform: "browser",
|
|
format: "iife",
|
|
target: ["es2020"],
|
|
minify: true,
|
|
legalComments: "none",
|
|
});
|
|
const iife = result.outputFiles[0]?.text ?? "";
|
|
if (!iife) throw new Error("esbuild produced no output for audio-fx-runtime-entry.ts");
|
|
|
|
mkdirSync(generatedDir, { recursive: true });
|
|
writeFileSync(
|
|
outPath,
|
|
[
|
|
"// AUTO-GENERATED by scripts/build-audio-fx-runtime.ts - do not edit",
|
|
`const AUDIO_FX_RUNTIME_IIFE: string = ${JSON.stringify(iife)};`,
|
|
"",
|
|
"/** Returns the pre-built audio-FX runtime IIFE as a string constant. */",
|
|
"export function getAudioFxRuntimeScript(): string {",
|
|
" return AUDIO_FX_RUNTIME_IIFE;",
|
|
"}",
|
|
"",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
|
|
try {
|
|
execFileSync("bun", ["x", "oxfmt", outPath], { stdio: "ignore" });
|
|
} catch {
|
|
// Formatting is best effort when the generator runs in a minimal environment.
|
|
}
|
|
|
|
console.log(JSON.stringify({ event: "audio_fx_runtime_generated", outPath, bytes: iife.length }));
|