Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions packages/engine/src/services/vstBounce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,8 @@ describe("resolveVstHostCommand", () => {
expect(resolveVstHostCommand()).toEqual(["/opt/custom", "vst-host"]);
});

it("finds the monorepo packages/vst-host by walking up (not a fixed hop)", () => {
it("falls back to the bare hyperframes-vst command on PATH", () => {
delete process.env.HF_VST_HOST_CMD;
const cmd = resolveVstHostCommand();
// In this monorepo checkout it must resolve to the uv-run form pointing at
// a real packages/vst-host — never the bare fallback (that ENOENTs at
// render time when the sidecar isn't installed on PATH).
expect(cmd.slice(0, 3)).toEqual(["uv", "run", "--project"]);
expect(cmd[3]?.endsWith("/packages/vst-host")).toBe(true);
expect(cmd[4]).toBe("hyperframes-vst");
expect(existsSync(join(cmd[3] ?? "", "pyproject.toml"))).toBe(true);
expect(resolveVstHostCommand()).toEqual(["hyperframes-vst"]);
});
});
49 changes: 12 additions & 37 deletions packages/engine/src/services/vstBounce.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* VST Bounce Service
*
* Spawns the `hyperframes-vst` Python sidecar (packages/vst-host) to apply a
* VST plugin chain to a dry WAV track before it's mixed into the composition.
* Spawns the `hyperframes-vst` Python sidecar to apply a VST plugin chain to
* a dry WAV track before it's mixed into the composition. The sidecar lives
* in the standalone `heygen-com/hyperframes-vst-host` repo, published to
* PyPI as `hyperframes-vst-host` (`uv tool install hyperframes-vst-host`).
*/

import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { basename, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { basename, join } from "node:path";
import { trackChildProcess } from "../utils/processTracker.js";

export interface ApplyVstChainOptions {
Expand All @@ -19,51 +19,26 @@ export interface ApplyVstChainOptions {
// especially on first run (plugin validation) or with convolution reverbs.
const BOUNCE_TIMEOUT_MS = 10 * 60 * 1000;

/**
* Walks up from `startDir` looking for a `packages/vst-host/pyproject.toml`,
* returning the `packages/vst-host` dir when found. Searching upward (rather
* than a single fixed `../../../vst-host` hop) makes this robust to WHERE the
* engine module actually runs from — source vs. `dist/services` vs. a bundled
* `dist/index.js` (all at different depths), and to the render process's cwd
* being the user's project rather than the package. A fixed hop silently
* missed by one level and fell through to the bare command → `spawn
* hyperframes-vst ENOENT` at render time.
*/
function findMonorepoVstHostDir(startDir: string): string | null {
let dir = startDir;
for (let i = 0; i < 10; i += 1) {
const candidate = join(dir, "packages", "vst-host");
if (existsSync(join(candidate, "pyproject.toml"))) return candidate;
const parent = resolve(dir, "..");
if (parent === dir) break; // reached filesystem root
dir = parent;
}
return null;
}

/**
* Resolves the command used to invoke the VST host sidecar.
*
* Precedence:
* 1. `HF_VST_HOST_CMD` env var (space-split) — lets CI/dev machines point at
* an arbitrary executable (or, in tests, a fake shell script).
* 2. `uv run --project <packages/vst-host> hyperframes-vst` when a monorepo
* `packages/vst-host` is found by walking up from this module's directory
* OR the process cwd (a source checkout of hyperframes).
* 3. Bare `hyperframes-vst` on PATH (an installed/published sidecar).
* 2. Bare `hyperframes-vst` on PATH (an installed/published sidecar — see
* `uv tool install hyperframes-vst-host`).
*
* Duplicated in `@hyperframes/studio-server`'s `vstSidecar.ts` — this package
* doesn't export the function from its public entry point or a subpath, so
* that module carries its own copy (see its docblock for the full why).
*/
// fallow-ignore-next-line code-duplication
export function resolveVstHostCommand(): string[] {
const override = process.env.HF_VST_HOST_CMD;
if (override && override.trim().length > 0) {
return override.trim().split(/\s+/);
}

const moduleDir = fileURLToPath(new URL(".", import.meta.url));
const vstHostDir = findMonorepoVstHostDir(moduleDir) ?? findMonorepoVstHostDir(process.cwd());
if (vstHostDir) {
return ["uv", "run", "--project", vstHostDir, "hyperframes-vst"];
}

return ["hyperframes-vst"];
}

Expand Down
30 changes: 14 additions & 16 deletions packages/producer/src/services/vstRenderParity.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,37 @@
/**
* End-to-end integration test for the VST render path: a composition with a
* `data-vst-chain` audio track is rendered through the real
* `processCompositionAudio` mixer, which shells out to the Python
* `packages/vst-host` sidecar (via `applyVstChainToWav`,
* `packages/engine/src/services/vstBounce.ts`) to bounce the dry track
* through a plugin chain before mixing.
* `processCompositionAudio` mixer, which shells out to the Python VST host
* sidecar (via `applyVstChainToWav`, `packages/engine/src/services/vstBounce.ts`)
* to bounce the dry track through a plugin chain before mixing. The sidecar
* itself lives in the standalone `heygen-com/hyperframes-vst-host` repo,
* published to PyPI as `hyperframes-vst-host` — install with
* `uv tool install hyperframes-vst-host` to run this test locally.
*
* Uses a BUILTIN pedalboard plugin (`Gain`) rather than a real VST3/AU
* bundle: builtins are deterministic (see chain.py / Task 7's nondeterminism
* caveat about some external plugins), so this test can assert bit-for-bit
* reproducibility across two independent runs without depending on any
* plugin being installed on the host machine.
*
* Skips when `uv` isn't on PATH or `packages/vst-host` doesn't exist next to
* this checkout — both are required to spawn the sidecar via
* `resolveVstHostCommand()`'s monorepo `uv run --project <vst-host> ...` path.
* Skips when `hyperframes-vst` isn't on PATH — required to spawn the sidecar
* via `resolveVstHostCommand()`'s bare-PATH fallback.
*/
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { parseAudioElements, processCompositionAudio } from "@hyperframes/engine";
import { computeAudioResidualRmsDb } from "../utils/audioRegression.js";

const HAS_UV = spawnSync("uv", ["--version"], { encoding: "utf-8" }).status === 0;

// Mirrors resolveVstHostCommand's own monorepo-detection logic
// Mirrors resolveVstHostCommand's own bare-PATH fallback
// (packages/engine/src/services/vstBounce.ts) so the skip condition matches
// exactly what the sidecar spawn will look for.
const VST_HOST_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../../../vst-host");
const HAS_VST_HOST = existsSync(join(VST_HOST_DIR, "pyproject.toml"));
const HAS_VST_HOST_CLI =
spawnSync("hyperframes-vst", ["--help"], { encoding: "utf-8" }).status === 0;

describe.skipIf(!HAS_UV || !HAS_VST_HOST)("VST render parity (integration)", () => {
describe.skipIf(!HAS_VST_HOST_CLI)("VST render parity (integration)", () => {
let projectDir: string;
let workRoot: string;
let outRoot: string;
Expand Down
3 changes: 2 additions & 1 deletion packages/studio-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ export interface StudioApiAdapter {
/**
* Optional: start the VST host sidecar and return its bound port + the
* shared-secret token the sidecar generated for this run (see
* `packages/vst-host/src/hyperframes_vst/server.py`'s `_authenticate`).
* `hyperframes_vst/server.py`'s `_authenticate` in the
* `heygen-com/hyperframes-vst-host` repo).
* Absent means VST plugin hosting isn't supported in this studio mode. The
* browser connects directly to `ws://localhost:<port>/?token=<token>` —
* both are simply relayed back to the client, not proxied. The token
Expand Down
38 changes: 14 additions & 24 deletions packages/studio-server/src/vstSidecar.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
/**
* Lifecycle for the VST host sidecar (`packages/vst-host`), shared by both
* `hyperframes preview` (CLI, via `packages/cli/src/vst/sidecar.ts`'s
* re-export of this module) and the Studio Vite dev server. Spawns the
* sidecar's `serve` subcommand, waits for its ready handshake on stdout, and
* tracks the single running instance for the lifetime of this process.
* `hyperframes preview` registers the running child with the same
* signal-driven shutdown paths it uses for its own studio child processes so
* Ctrl-C during a preview session also tears the sidecar down.
* Lifecycle for the VST host sidecar (`heygen-com/hyperframes-vst-host` on
* PyPI as `hyperframes-vst-host`), shared by both `hyperframes preview` (CLI,
* via `packages/cli/src/vst/sidecar.ts`'s re-export of this module) and the
* Studio Vite dev server. Spawns the sidecar's `serve` subcommand, waits for
* its ready handshake on stdout, and tracks the single running instance for
* the lifetime of this process. `hyperframes preview` registers the running
* child with the same signal-driven shutdown paths it uses for its own
* studio child processes so Ctrl-C during a preview session also tears the
* sidecar down.
*/

import { spawn, type ChildProcess } from "node:child_process";
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { join, resolve } from "node:path";

// Captures the shared-secret token the sidecar prints alongside its port
// (see server.py's `_authenticate`/`start`) — required by every WS command,
Expand Down Expand Up @@ -46,30 +44,22 @@ let spawningChild: ChildProcess | null = null;
* Precedence:
* 1. `HF_VST_HOST_CMD` env var (space-split) — lets CI/dev machines point at
* an arbitrary executable (or, in tests, a fake shell script).
* 2. `uv run --project <packages/vst-host> hyperframes-vst` when the
* monorepo's `packages/vst-host` directory is present relative to this
* package (the common case: a source checkout of hyperframes).
* 3. Bare `hyperframes-vst` on PATH (an installed/published sidecar).
* 2. Bare `hyperframes-vst` on PATH (an installed/published sidecar — see
* `uv tool install hyperframes-vst-host`).
*
* Duplicated from `@hyperframes/engine`'s `resolveVstHostCommand`
* (packages/engine/src/services/vstBounce.ts) — that package doesn't export
* it from its public entry point or a subpath, so this module carries its own
* copy with a package-relative monorepo path. Exported for reuse by
* `vstCarve.ts`, which spawns the same sidecar's `carve` verb; `HF_VST_HOST_CMD`
* is the test seam (see vstSidecar.test.ts) for both call sites.
* copy. Exported for reuse by `vstCarve.ts`, which spawns the same sidecar's
* `carve` verb; `HF_VST_HOST_CMD` is the test seam (see vstSidecar.test.ts)
* for both call sites.
*/
export function resolveVstHostCommand(): string[] {
const override = process.env.HF_VST_HOST_CMD;
if (override && override.trim().length > 0) {
return override.trim().split(/\s+/);
}

const thisDir = fileURLToPath(new URL(".", import.meta.url));
const monorepoVstHostDir = resolve(thisDir, "../../vst-host");
if (existsSync(join(monorepoVstHostDir, "pyproject.toml"))) {
return ["uv", "run", "--project", monorepoVstHostDir, "hyperframes-vst"];
}

return ["hyperframes-vst"];
}

Expand Down
3 changes: 2 additions & 1 deletion packages/studio/src/hooks/vstHostWire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* connect sequence. Nothing here holds React state — it's all plain
* functions and module-level helpers the hook wires into refs/effects.
*
* Protocol (see `packages/vst-host/src/hyperframes_vst/server.py`): JSON text
* Protocol (see `hyperframes_vst/server.py` in the
* `heygen-com/hyperframes-vst-host` repo): JSON text
* frames for control commands/events, raw binary `ArrayBuffer` frames for
* interleaved-stereo PCM (`u32 trackIndex` + `f64 samplePos` + `f32` samples)
* that the hook forwards unopened to `onPcmFrame` subscribers — decoding is
Expand Down
2 changes: 1 addition & 1 deletion packages/studio/src/player/hooks/useVstPreviewHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function dbg(e: string, d?: unknown): void {
}

// ── Wire-format decode ────────────────────────────────────────────────────
// Mirrors the sidecar's `encode_frame` (packages/vst-host/.../stream.py):
// Mirrors the sidecar's `encode_frame` (stream.py in heygen-com/hyperframes-vst-host):
// little-endian u32 trackIndex, f64 samplePos, then interleaved f32 stereo.

export interface DecodedPcmFrame {
Expand Down
7 changes: 0 additions & 7 deletions packages/vst-host/.gitignore

This file was deleted.

Loading
Loading