diff --git a/packages/engine/src/services/vstBounce.test.ts b/packages/engine/src/services/vstBounce.test.ts index 609881725e..95cd5b485f 100644 --- a/packages/engine/src/services/vstBounce.test.ts +++ b/packages/engine/src/services/vstBounce.test.ts @@ -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"]); }); }); diff --git a/packages/engine/src/services/vstBounce.ts b/packages/engine/src/services/vstBounce.ts index 45d2ab27e0..5090c5491a 100644 --- a/packages/engine/src/services/vstBounce.ts +++ b/packages/engine/src/services/vstBounce.ts @@ -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 { @@ -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 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"]; } diff --git a/packages/producer/src/services/vstRenderParity.test.ts b/packages/producer/src/services/vstRenderParity.test.ts index 9849a88e45..42d93d9389 100644 --- a/packages/producer/src/services/vstRenderParity.test.ts +++ b/packages/producer/src/services/vstRenderParity.test.ts @@ -1,10 +1,12 @@ /** * 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 @@ -12,28 +14,24 @@ * 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 ...` 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; diff --git a/packages/studio-server/src/types.ts b/packages/studio-server/src/types.ts index d68de52ff4..cd2e574068 100644 --- a/packages/studio-server/src/types.ts +++ b/packages/studio-server/src/types.ts @@ -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:/?token=` — * both are simply relayed back to the client, not proxied. The token diff --git a/packages/studio-server/src/vstSidecar.ts b/packages/studio-server/src/vstSidecar.ts index 0bb145e8e1..3e0245f09b 100644 --- a/packages/studio-server/src/vstSidecar.ts +++ b/packages/studio-server/src/vstSidecar.ts @@ -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, @@ -46,17 +44,15 @@ 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 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; @@ -64,12 +60,6 @@ export function resolveVstHostCommand(): string[] { 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"]; } diff --git a/packages/studio/src/hooks/vstHostWire.ts b/packages/studio/src/hooks/vstHostWire.ts index 7305118d8b..514f383da7 100644 --- a/packages/studio/src/hooks/vstHostWire.ts +++ b/packages/studio/src/hooks/vstHostWire.ts @@ -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 diff --git a/packages/studio/src/player/hooks/useVstPreviewHelpers.ts b/packages/studio/src/player/hooks/useVstPreviewHelpers.ts index 7f0d35b8f5..a8ff7362b8 100644 --- a/packages/studio/src/player/hooks/useVstPreviewHelpers.ts +++ b/packages/studio/src/player/hooks/useVstPreviewHelpers.ts @@ -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 { diff --git a/packages/vst-host/.gitignore b/packages/vst-host/.gitignore deleted file mode 100644 index 25072ab290..0000000000 --- a/packages/vst-host/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -__pycache__/ -*.pyc -.pytest_cache/ -.venv/ -*.egg-info/ -dist/ -build/ diff --git a/packages/vst-host/README.md b/packages/vst-host/README.md deleted file mode 100644 index 8749221049..0000000000 --- a/packages/vst-host/README.md +++ /dev/null @@ -1,143 +0,0 @@ -# hyperframes-vst-host - -Python sidecar that hosts VST3/AU plugins (via [pedalboard](https://github.com/spotify/pedalboard)) -for HyperFrames Studio and the render pipeline: an offline WAV → WAV `bounce` -used at render time, and a WebSocket `serve` mode used for live FX preview -(parameter tweaking, native plugin editor windows, streamed processed audio) -in Studio. - -## Install - -- **Standalone (published package):** - - ```bash - uv tool install hyperframes-vst-host - ``` - -- **Monorepo dev (source checkout, no install step):** - - ```bash - uv run --project packages/vst-host hyperframes-vst - ``` - - This is what the TypeScript callers use automatically — neither requires a - manual install in a source checkout. `resolveVstHostCommand()` - (`packages/engine/src/services/vstBounce.ts` for render, and its sibling - copy in `packages/studio-server/src/vstSidecar.ts` for the Studio sidecar) - falls back to `uv run --project hyperframes-vst` the - moment `packages/vst-host/pyproject.toml` is found relative to the caller, - and only falls back further to a bare `hyperframes-vst` on `PATH` (the - installed/published case) when that monorepo layout isn't present. - -- **Override (CI / dev, arbitrary executable or a test fake):** set - `HF_VST_HOST_CMD` to a space-split command; it takes precedence over both - of the above. - -## CLI - -``` -hyperframes-vst -``` - -| command | flags | purpose | -| -------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `bounce` | `--input --chain --output ` | Offline render a WAV through a chain, block-by-block, no realtime clock. Used by the render pipeline's `applyVstChainToWav`. | -| `scan` | `--dirs [...] --json` | Scan plugin directories (or the default set), print a registry JSON to stdout. | -| `probe` | `` | Probe one plugin bundle in a subprocess — some bundles crash pedalboard's loader, so callers isolate one path at a time. | -| `serve` | `--port N` (default: OS-assigned) | Run the WebSocket sidecar used by Studio's live FX preview. | - -### `bounce` exit codes - -- **`0`** — success; the processed WAV is written to `--output`. -- **`3`** — `PLUGIN_MISSING ` printed to stderr: the chain file - references a plugin that isn't installed/found on this machine. The render - pipeline (`packages/engine/src/services/vstBounce.ts`'s - `applyVstChainToWav`) reads this exit code + stderr line and hard-fails the - whole render, naming the specific missing plugin and the track — a missing - plugin at render time is never a silent fallback to unprocessed dry audio. -- Any other non-zero code is treated as a generic sidecar failure and - surfaced with the tail of stderr. - -## Sidecar readiness handshake - -`serve` prints exactly one readiness line to stdout the moment its WebSocket -server is bound: - -``` -VST-HOST-LISTENING port= -``` - -Callers wait for this line (matched against `/VST-HOST-LISTENING -port=(\d+)/`) before treating the sidecar as ready; a 30-second timeout -without it is treated as a failed start (see `startVstSidecar` in -`packages/studio-server/src/vstSidecar.ts`). - -## WebSocket protocol - -One socket, two lanes: JSON text frames for control commands/events, and raw -binary frames for interleaved PCM samples during playback. Implemented in -`src/hyperframes_vst/server.py` (dispatch) and `src/hyperframes_vst/stream.py` -(PCM framing); mirrored client-side in `packages/studio/src/hooks/useVstHost.ts`. - -### Client → server (`{"cmd": ...}`) - -| cmd | fields | effect | -| -------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | -| `scan` | `paths?: string[]` | Scans plugin dirs (or the default set); replies `registry` | -| `load-chain` | `trackId, chainJson, wavPath` | Builds the live plugin chain for a track, (re)opens its audio stream; replies `chain-loaded` | -| `unload-chain` | `trackId` | Tears down a track's stream and plugin instances | -| `set-param` | `trackId, pluginIndex, param, value` | Sets one live plugin parameter | -| `open-editor` | `trackId, pluginIndex` | Opens the plugin's native editor window (spawned on its own thread) | -| `close-editor` | `trackId, pluginIndex` | No-op server-side — pedalboard editor windows close from their own window chrome, not this command | -| `get-state` | `trackId` | Replies `state` with each plugin's base64-encoded state | -| `transport` | `action: "play" \| "pause" \| "seek", timeSec?, rate?` | Drives playback/seek for every loaded track's PCM streaming lane | - -### Server → client (`{"event": ...}`) - -| event | fields | meaning | -| -------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `registry` | `plugins: [...]` | Result of a `scan` | -| `chain-loaded` | `trackId, params` | A `load-chain` completed; `params` is per-plugin parameter metadata | -| `state` | `trackId, plugins: string[]` | Result of `get-state` — one base64 state blob per plugin, in chain order | -| `error` | `code, plugin?, trackId?` | A command failed. `code: "plugin_missing"` carries the plugin name in `plugin`; anything else is `code: "bad_command"` | - -### Binary PCM frame (server → client, during `transport: play`) - -Little-endian, one frame per streamed block: - -``` -u32 trackIndex -f64 samplePos -f32[...] interleaved stereo samples -``` - -1024 samples per block at 48 kHz (`block_size` / `sample_rate` in -`src/hyperframes_vst/stream.py`'s `TrackStream`). - -## Manual E2E verification checklist - -Run through this after any change that touches the sidecar, the render -pipeline's VST bounce, or the Studio FX panel/preview path — it's the only -check that exercises native plugin windows and real audio playback, which -automated tests can't cover. - -1. Load a composition with an audio track in Studio. -2. Add an EQ/effect to that track via the FX property panel. -3. Open the plugin's native editor window from the panel. -4. Twist a knob in the native editor. -5. Play the composition back and confirm you hear the change live (streamed - processed audio, not the dry track). -6. Close the editor window — confirm the tweaked state persists to the - track's `.vstchain.json` chain file (re-open the panel or reload the - project and see the same parameter value). -7. Run `hyperframes render` on the same composition. -8. Confirm the rendered audio reflects the same processing character you - heard live in preview (same effect, same rough parameter feel). -9. Delete or rename the plugin's bundle on disk (VST3/AU) so it can no - longer be found. -10. Render the same composition again — confirm it **fails**, with an error - naming the specific missing plugin (not a silent fallback to dry audio). -11. Attempt a Lambda cloud render (`hyperframes lambda render`) on the same - composition — confirm it's rejected **before any AWS call**, with an - error naming the track(s) carrying a VST chain (plugins can't run in - Lambda; see the guard in `packages/cli/src/commands/lambda.ts`). diff --git a/packages/vst-host/pyproject.toml b/packages/vst-host/pyproject.toml deleted file mode 100644 index d6f72a9da6..0000000000 --- a/packages/vst-host/pyproject.toml +++ /dev/null @@ -1,22 +0,0 @@ -[project] -name = "hyperframes-vst-host" -version = "0.1.0" -description = "VST3/AU sidecar host for HyperFrames Studio" -requires-python = ">=3.11" -dependencies = ["pedalboard>=0.9.24", "websockets>=12", "numpy>=1.26"] - -[project.scripts] -hyperframes-vst = "hyperframes_vst.__main__:main" - -[dependency-groups] -dev = ["pytest>=8", "pytest-asyncio>=0.23"] - -[tool.pytest.ini_options] -asyncio_mode = "auto" - -[tool.hatch.build.targets.wheel] -packages = ["src/hyperframes_vst"] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/packages/vst-host/src/hyperframes_vst/__init__.py b/packages/vst-host/src/hyperframes_vst/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/vst-host/src/hyperframes_vst/__main__.py b/packages/vst-host/src/hyperframes_vst/__main__.py deleted file mode 100644 index 8ba07d810d..0000000000 --- a/packages/vst-host/src/hyperframes_vst/__main__.py +++ /dev/null @@ -1,78 +0,0 @@ -import argparse -import sys - -from .chain import PluginMissingError - - -def main() -> int: - parser = argparse.ArgumentParser(prog="hyperframes-vst") - sub = parser.add_subparsers(dest="command", required=True) - - p_bounce = sub.add_parser("bounce", help="Offline render a WAV through a chain") - p_bounce.add_argument("--input", required=True) - p_bounce.add_argument("--chain", required=True) - p_bounce.add_argument("--output", required=True) - - p_probe = sub.add_parser("probe", help="Probe one bundle (may crash; run in subprocess)") - p_probe.add_argument("path") - - p_scan = sub.add_parser("scan", help="Scan plugin dirs, print registry JSON") - p_scan.add_argument("--dirs", nargs="*", default=None) - p_scan.add_argument("--json", action="store_true") - - p_carve = sub.add_parser("carve", help="Analyze a voiceover WAV, print carve bands JSON") - p_carve.add_argument("--voice", required=True) - p_carve.add_argument("--max-cut-db", type=float, default=4.0) - p_carve.add_argument("--json", action="store_true") - - p_serve = sub.add_parser("serve", help="Run the WebSocket sidecar") - p_serve.add_argument("--port", type=int, default=0) - - args = parser.parse_args() - - if args.command == "bounce": - from .bounce import bounce_file - - try: - bounce_file(args.input, args.chain, args.output) - except PluginMissingError as exc: - print(f"PLUGIN_MISSING {exc.plugin_name}", file=sys.stderr) - return 3 - return 0 - - if args.command == "probe": - import json as _json - - from .scan import probe_bundle - - print(_json.dumps(probe_bundle(args.path))) - return 0 - - if args.command == "scan": - import json as _json - - from .scan import default_plugin_dirs, scan_paths - - print(_json.dumps(scan_paths(args.dirs or default_plugin_dirs()))) - return 0 - - if args.command == "carve": - import json as _json - - from .carve import carve_file - - bands = carve_file(args.voice, max_cut_db=args.max_cut_db) - print(_json.dumps({"bands": bands})) - return 0 - - if args.command == "serve": - from .server import serve - - serve(args.port) - return 0 - - return 2 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/packages/vst-host/src/hyperframes_vst/bounce.py b/packages/vst-host/src/hyperframes_vst/bounce.py deleted file mode 100644 index e4ba906cb8..0000000000 --- a/packages/vst-host/src/hyperframes_vst/bounce.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Offline WAV -> WAV render through a chain. Block-by-block, no realtime clock.""" -from __future__ import annotations - -from pedalboard import Pedalboard -from pedalboard.io import AudioFile - -from .chain import build_chain, enabled_plugins, load_chain_spec - - -def bounce_file(input_wav: str, chain_json_path: str, output_wav: str, block_size: int = 8192) -> None: - with open(chain_json_path, "r", encoding="utf-8") as f: - spec = load_chain_spec(f.read()) - # Disabled plugins are bypassed at render exactly as in preview. - board = Pedalboard(enabled_plugins(spec, build_chain(spec))) - with AudioFile(input_wav) as src: - with AudioFile(output_wav, "w", src.samplerate, src.num_channels) as dst: - while src.tell() < src.frames: - chunk = src.read(min(block_size, src.frames - src.tell())) - dst.write(board(chunk, src.samplerate, reset=False)) diff --git a/packages/vst-host/src/hyperframes_vst/carve.py b/packages/vst-host/src/hyperframes_vst/carve.py deleted file mode 100644 index 8e07385255..0000000000 --- a/packages/vst-host/src/hyperframes_vst/carve.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Spectral carve: find a voiceover's dominant presence bands so a music -track can be EQ-dipped there (the "vocal pocket"). Pure numpy analysis; the -returned bands are plain pedalboard PeakFilter configs.""" -from __future__ import annotations - -import numpy as np - -SPEECH_LO_HZ = 150.0 -SPEECH_HI_HZ = 6000.0 -CARVE_Q = 1.5 -MIN_BANDS = 2 -MAX_BANDS = 4 -# Roughly third-octave centers spanning the speech-relevant range. -CANDIDATE_CENTERS_HZ = [160.0, 250.0, 400.0, 630.0, 1000.0, 1600.0, 2500.0, 4000.0, 6000.0] - -_FRAME = 4096 -_HOP = 2048 - - -def _to_mono(samples: np.ndarray) -> np.ndarray: - arr = np.asarray(samples, dtype=np.float64) - if arr.ndim == 2: - # pedalboard yields (channels, frames); average channels to mono. - arr = arr.mean(axis=0) - return arr.reshape(-1) - - -def _power_spectrum(mono: np.ndarray, sample_rate: float) -> tuple[np.ndarray, np.ndarray]: - """Welch-style averaged power spectrum. Returns (freqs, power).""" - n = mono.shape[0] - if n < _FRAME: - mono = np.pad(mono, (0, _FRAME - n)) - n = _FRAME - window = np.hanning(_FRAME) - acc = np.zeros(_FRAME // 2 + 1) - frames = 0 - for start in range(0, n - _FRAME + 1, _HOP): - seg = mono[start : start + _FRAME] * window - spec = np.abs(np.fft.rfft(seg)) ** 2 - acc += spec - frames += 1 - if frames == 0: - acc = np.abs(np.fft.rfft(mono[:_FRAME] * window)) ** 2 - frames = 1 - freqs = np.fft.rfftfreq(_FRAME, d=1.0 / sample_rate) - return freqs, acc / frames - - -def _band_power(freqs: np.ndarray, power: np.ndarray, center: float) -> float: - lo = center / (2.0 ** (1.0 / 6.0)) - hi = center * (2.0 ** (1.0 / 6.0)) - mask = (freqs >= lo) & (freqs < hi) - if not np.any(mask): - return 0.0 - return float(power[mask].mean()) - - -def carve(samples: np.ndarray, sample_rate: float, max_cut_db: float = 4.0) -> list[dict]: - mono = _to_mono(samples) - freqs, power = _power_spectrum(mono, sample_rate) - - centers = [c for c in CANDIDATE_CENTERS_HZ if SPEECH_LO_HZ <= c <= SPEECH_HI_HZ] - band_powers = [(c, _band_power(freqs, power, c)) for c in centers] - mean_power = np.mean([p for _, p in band_powers]) or 1.0 - - # Rank by how far each band exceeds the average; keep those above average - # (up to MAX_BANDS), but always return at least MIN_BANDS (the strongest). - ranked = sorted(band_powers, key=lambda cp: cp[1], reverse=True) - above = [cp for cp in ranked if cp[1] > mean_power] - selected = above[:MAX_BANDS] if len(above) >= MIN_BANDS else ranked[:MIN_BANDS] - - top = max(p for _, p in selected) or 1.0 - bands: list[dict] = [] - for center, p in selected: - # Deepest cut on the strongest band; shallower elsewhere, floored at - # half depth so weak-but-selected bands still get meaningful room. - depth = max_cut_db * (p / top) - depth = min(max_cut_db, max(max_cut_db / 2.0, depth)) - bands.append({"freq": float(center), "gainDb": float(-depth), "q": CARVE_Q}) - bands.sort(key=lambda b: b["freq"]) - return bands - - -def carve_file(voice_wav: str, max_cut_db: float = 4.0) -> list[dict]: - from pedalboard.io import AudioFile - - with AudioFile(voice_wav) as f: - samples = f.read(f.frames) # (channels, frames) - sr = f.samplerate - return carve(samples, sr, max_cut_db=max_cut_db) diff --git a/packages/vst-host/src/hyperframes_vst/chain.py b/packages/vst-host/src/hyperframes_vst/chain.py deleted file mode 100644 index 165deb43f4..0000000000 --- a/packages/vst-host/src/hyperframes_vst/chain.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Chain spec: the persisted .vstchain.json contents and live plugin construction.""" -from __future__ import annotations - -import base64 -import json -import os -from dataclasses import dataclass - -import pedalboard - -VALID_FORMATS = {"vst3", "au", "builtin"} - - -class ChainError(ValueError): - pass - - -class PluginMissingError(ChainError): - def __init__(self, name: str): - super().__init__(f"Plugin not available on this machine: {name}") - self.plugin_name = name - - -@dataclass -class PluginSpec: - format: str - path: str - plugin_name: str | None - name: str - state_b64: str | None - # Bypass toggle — absent in the JSON means enabled (backward compatible - # with chain files written before the field existed). A disabled plugin - # is still CONSTRUCTED (so set-param/get-state indices stay aligned with - # the chain file) but excluded from the processing board — see - # `enabled_plugins`. - enabled: bool = True - - -@dataclass -class ChainSpec: - version: int - plugins: list[PluginSpec] - - -def load_chain_spec(json_text: str) -> ChainSpec: - raw = json.loads(json_text) - if raw.get("version") != 1: - raise ChainError(f"Unsupported chain version: {raw.get('version')}") - plugins = [] - for p in raw.get("plugins", []): - fmt = p.get("format") - if fmt not in VALID_FORMATS: - raise ChainError(f"Unknown plugin format: {fmt}") - plugins.append( - PluginSpec( - format=fmt, - path=p["path"], - plugin_name=p.get("pluginName"), - name=p.get("name", p["path"]), - state_b64=p.get("stateB64"), - enabled=p.get("enabled", True) is not False, - ) - ) - return ChainSpec(version=1, plugins=plugins) - - -def _build_builtin(spec: PluginSpec): - cls = getattr(pedalboard, spec.path, None) - if cls is None: - raise PluginMissingError(spec.name) - plugin = cls() - if spec.state_b64: - params = json.loads(base64.b64decode(spec.state_b64)) - for key, value in params.items(): - setattr(plugin, key, value) - return plugin - - -def _build_external(spec: PluginSpec): - if not os.path.exists(spec.path): - raise PluginMissingError(spec.name) - try: - plugin = pedalboard.load_plugin(spec.path, plugin_name=spec.plugin_name) - except Exception as exc: - raise PluginMissingError(spec.name) from exc - if spec.state_b64: - plugin.raw_state = base64.b64decode(spec.state_b64) - return plugin - - -def build_chain(spec: ChainSpec) -> list: - return [_build_builtin(p) if p.format == "builtin" else _build_external(p) for p in spec.plugins] - - -def enabled_plugins(spec: ChainSpec, built: list) -> list: - """The subset of `built` (from `build_chain(spec)`, same order) that should - actually process audio. Disabled plugins are constructed but bypassed — - keeping the full list's indices aligned with the chain file for - set-param/get-state while the processing board skips them. An all-disabled - chain yields an empty board, which pedalboard treats as a passthrough.""" - return [plugin for plugin, p in zip(built, spec.plugins) if p.enabled] - - -def _is_builtin(plugin) -> bool: - return not hasattr(plugin, "raw_state") - - -def serialize_states(plugins: list) -> list[str]: - states = [] - for plugin in plugins: - if _is_builtin(plugin): - params = { - key: float(getattr(plugin, key)) - for key in dir(plugin) - if not key.startswith("_") and isinstance(getattr(plugin, key), float) - } - states.append(base64.b64encode(json.dumps(params).encode()).decode()) - else: - states.append(base64.b64encode(plugin.raw_state).decode()) - return states diff --git a/packages/vst-host/src/hyperframes_vst/scan.py b/packages/vst-host/src/hyperframes_vst/scan.py deleted file mode 100644 index 903a95e229..0000000000 --- a/packages/vst-host/src/hyperframes_vst/scan.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Plugin discovery. Every bundle probe runs in a throwaway subprocess: -a malformed bundle can SIGBUS the probing process (observed with macOS -CoreAudio.component), so the sidecar process itself never loads candidates.""" -from __future__ import annotations - -import json -import os -import subprocess -import sys - -BUNDLE_EXTENSIONS = {".vst3": "vst3", ".component": "au"} -PROBE_TIMEOUT_SEC = 10 - -# pedalboard's built-in effects that host cleanly with no constructor args and -# no external plugin file — the reliable, always-available options (unlike -# third-party VST3/AU, which pedalboard's headless host can't run for a real -# subset; see the stability guard). Each maps to a `pedalboard.` class in -# chain.py's `_build_builtin` (format "builtin", path = the class name). -# Ordered most-useful-first for the FX picker. Convolution and IIRFilter are -# excluded: they require constructor arguments (an impulse response / filter -# coefficients) this add flow can't supply. -BUILTIN_EFFECTS = [ - "Reverb", "Delay", "Chorus", "Phaser", "Distortion", "Compressor", - "Limiter", "NoiseGate", "Gain", "PitchShift", "Bitcrush", "Clipping", - "LadderFilter", "LowpassFilter", "HighpassFilter", "PeakFilter", - "LowShelfFilter", "HighShelfFilter", "MP3Compressor", "Invert", -] - - -def builtin_registry() -> list[dict]: - """The built-in effects, in FX-picker registry form. `pluginName` is None - (builtins take no sub-name), `path` is the pedalboard class name.""" - return [{"path": name, "name": name, "format": "builtin"} for name in BUILTIN_EFFECTS] - - -def default_plugin_dirs() -> list[str]: - home = os.path.expanduser("~") - if sys.platform == "darwin": - return [ - "/Library/Audio/Plug-Ins/VST3", - f"{home}/Library/Audio/Plug-Ins/VST3", - "/Library/Audio/Plug-Ins/Components", - f"{home}/Library/Audio/Plug-Ins/Components", - ] - if sys.platform == "win32": - return [r"C:\Program Files\Common Files\VST3"] - return [] - - -def _probe(bundle: str, probe_cmd: list[str]) -> list[dict]: - try: - proc = subprocess.run( - probe_cmd + [bundle], capture_output=True, text=True, timeout=PROBE_TIMEOUT_SEC - ) - except subprocess.TimeoutExpired: - return [] - if proc.returncode != 0: - return [] - try: - return json.loads(proc.stdout) - except json.JSONDecodeError: - return [] - - -def scan_paths(dirs: list[str], probe_cmd: list[str] | None = None) -> list[dict]: - cmd = probe_cmd or [sys.executable, "-m", "hyperframes_vst", "probe"] - registry: list[dict] = [] - for d in dirs: - if not os.path.isdir(d): - continue - for entry in sorted(os.listdir(d)): - _, ext = os.path.splitext(entry) - fmt = BUNDLE_EXTENSIONS.get(ext.lower()) - if fmt is None: - continue - bundle = os.path.join(d, entry) - for item in _probe(bundle, cmd): - registry.append({"path": bundle, "name": item["name"], "format": fmt}) - return registry - - -def probe_bundle(path: str) -> list[dict]: - """Runs IN the throwaway subprocess. May crash; caller tolerates.""" - from pedalboard._pedalboard import AudioUnitPlugin, VST3Plugin - - cls = AudioUnitPlugin if path.lower().endswith(".component") else VST3Plugin - names = cls.get_plugin_names_for_file(path) - return [{"name": n} for n in names] diff --git a/packages/vst-host/src/hyperframes_vst/server.py b/packages/vst-host/src/hyperframes_vst/server.py deleted file mode 100644 index 5c77a90f23..0000000000 --- a/packages/vst-host/src/hyperframes_vst/server.py +++ /dev/null @@ -1,417 +0,0 @@ -"""WebSocket sidecar server: JSON control lane + binary PCM lane on one socket. - -pedalboard enforces a single-thread affinity for ALL native VST3/AU work in -a process, not just editor windows: whichever thread first loads a plugin -becomes "the" thread for every later load, and showing a native editor -window additionally requires that thread to be the true OS main thread (a -hard Cocoa/AppKit constraint on macOS — windows can only be created there). -Loading a plugin from a second, different thread raises -`RuntimeError('... must be reloaded on the main thread ...')`; showing an -editor from a non-main thread raises `RuntimeError('Plugin UI windows can -only be shown from the main thread.')` or a JUCE-side ObjC exception, -depending on which check trips first. - -So every pedalboard-native call in this process — both `build_chain` -(loading plugins) and `show_editor` (opening a window) — is funneled -through `VstServer.run_pedalboard_thread`, one dedicated loop that runs on -the process's real main thread (`serve()`, bottom of this file) and -processes requests from a thread-safe queue, one at a time. The asyncio -WebSocket server itself runs on a background thread instead. -""" -from __future__ import annotations - -import asyncio -import json -import os -import queue -import secrets -import subprocess -import sys -import threading -import time -import traceback -from urllib.parse import parse_qs, urlsplit - -import websockets -from websockets.datastructures import Headers -from websockets.http11 import Request, Response - -from .chain import PluginMissingError, build_chain, enabled_plugins, load_chain_spec, serialize_states -from .scan import builtin_registry, default_plugin_dirs, scan_paths -from .stream import TrackStream, probe_chain_stability - - -def _raise_editor_window_macos() -> None: - """Bring the sidecar process's native plugin-editor window to the front. - - `plugin.show_editor()` opens a real VST3/AU editor window, but the sidecar - is a background (`uv run`) process, so on macOS the window opens BEHIND the - browser the user is looking at. There's no pedalboard API to raise it, so - activate this process via System Events by its pid. Fired on a short delay - (the window doesn't exist the instant show_editor is called) from a timer - thread, since show_editor blocks the pedalboard thread until the window - closes. Best-effort: any failure (osascript missing, permissions) is - swallowed — a window behind the browser is a worse-but-not-broken state. - """ - if sys.platform != "darwin": - return - script = ( - 'tell application "System Events" to set frontmost of ' - f"(first process whose unix id is {os.getpid()}) to true" - ) - try: - subprocess.run(["osascript", "-e", script], capture_output=True, timeout=5) - except Exception: - pass - - -class VstServer: - # Seconds of audio to burst-send ahead of the real-time playhead when a - # transport starts, pre-filling the client's ring buffer as a jitter - # cushion. Must stay below the client ring's capacity (1s — see - # useVstPreview's per-track `driftTracker`/worklet ring) so the lead can - # never overflow it. - _PUMP_LEAD_SEC = 0.5 - - def __init__(self) -> None: - self._tracks: dict[str, TrackStream] = {} - self._plugins: dict[str, list] = {} - self._play_task: asyncio.Task | None = None - self._play_owner: object | None = None - self._server: websockets.WebSocketServer | None = None - self._rate = 1.0 - # Shared-secret handshake (see `_authenticate`): the sidecar accepts - # native-plugin-loading and arbitrary-file-read commands over a plain - # loopback WebSocket, so without this any local process — or a - # webpage that guesses/scans the ephemeral port — could drive it. - # Generated once per process and printed alongside the ready line; - # only a client that already has it (relayed by studio-server's - # `/vst/start`, itself only reachable by the studio's own trusted - # HTTP server) can open a connection. - self._token = secrets.token_urlsafe(32) - # All pedalboard-native work (plugin loading + editor windows) hands - # off to the main thread through this queue (see module docstring). - # Each item is (fn, args, kwargs, future, loop); the main-thread loop - # calls fn(*args, **kwargs) and posts the result/exception back onto - # the asyncio loop that's awaiting it via call_soon_threadsafe. - self._pedalboard_queue: "queue.Queue[tuple | None]" = queue.Queue() - # Keyed by (trackId, pluginIndex) so a later `close-editor` can find - # and signal the right window before it's opened/while it's open. - self._editor_close_events: dict[tuple[str, int], threading.Event] = {} - - @property - def token(self) -> str: - return self._token - - async def start(self, port: int = 0) -> int: - self._server = await websockets.serve( - self._handle, "127.0.0.1", port, process_request=self._authenticate, - ) - bound = self._server.sockets[0].getsockname()[1] - print(f"VST-HOST-LISTENING port={bound} token={self._token}", flush=True) - return bound - - async def _authenticate(self, connection, request: Request) -> Response | None: - """`process_request` hook: rejects the HTTP upgrade (before any - WebSocket connection — and so before `_handle`/`_dispatch` ever see a - message) unless the request carries the correct `?token=` query - param. Returning `None` lets the handshake proceed normally.""" - query = parse_qs(urlsplit(request.path).query) - supplied = query.get("token", [None])[0] - if supplied != self._token: - body = b"Unauthorized: missing or invalid token\n" - return Response(401, "Unauthorized", Headers(), body) - return None - - async def stop(self) -> None: - if self._play_task: - self._play_task.cancel() - if self._server: - self._server.close() - await self._server.wait_closed() - - async def _handle(self, ws) -> None: - try: - async for raw in ws: - if not isinstance(raw, str): - continue - msg = json.loads(raw) - await self._dispatch(ws, msg) - finally: - if self._play_task and self._play_owner is ws: - self._play_task.cancel() - - async def _run_on_pedalboard_thread(self, fn, *args): - """Runs fn(*args) on the dedicated pedalboard thread (see module - docstring) and awaits its result without blocking the event loop or - this connection's other message handling.""" - loop = asyncio.get_running_loop() - future = loop.create_future() - - def _call(): - try: - result = fn(*args) - except Exception as exc: - loop.call_soon_threadsafe(future.set_exception, exc) - else: - loop.call_soon_threadsafe(future.set_result, result) - - self._pedalboard_queue.put(_call) - return await future - - async def _dispatch(self, ws, msg: dict) -> None: - cmd = msg.get("cmd") - try: - if cmd == "scan": - discovered = await asyncio.to_thread(scan_paths, msg.get("paths") or default_plugin_dirs()) - # Built-ins first: always-available, host-clean options that - # need no disk scan (see scan.py's BUILTIN_EFFECTS). - plugins = builtin_registry() + discovered - await ws.send(json.dumps({"event": "registry", "plugins": plugins})) - elif cmd == "load-chain": - track_id = msg["trackId"] - spec = load_chain_spec(json.dumps(msg["chainJson"])) - # Only chains with an external VST3/AU plugin need the - # dedicated pedalboard thread (see module docstring): - # builtins never touch JUCE's native plugin-loading - # machinery, so they're free to load off any thread pool - # worker, same as before this thread-affinity fix existed. - if any(p.format != "builtin" for p in spec.plugins): - plugins = await self._run_on_pedalboard_thread(build_chain, spec) - else: - plugins = await asyncio.to_thread(build_chain, spec) - old = self._tracks.pop(track_id, None) - if old: - old.close() - # The FULL constructed list keeps set-param/get-state indices - # aligned with the chain file; only the enabled subset - # processes audio (a disabled plugin is bypassed, not gone). - self._plugins[track_id] = plugins - active = enabled_plugins(spec, plugins) - # Probe whether pedalboard can host this chain without emitting - # NaN/Inf/runaway output (see probe_chain_stability). An - # unstable track is still registered — so its wire index stays - # in lockstep with the client's own counter — but flagged so it - # never streams; the client then keeps it on dry audio and warns - # instead of muting the original into NaN-driven silence. - stable = await asyncio.to_thread(probe_chain_stability, msg["wavPath"], active) - track = TrackStream(len(self._tracks), msg["wavPath"], active, stable=stable) - self._tracks[track_id] = track - params = [ - [{"name": k, "value": float(v.raw_value) if hasattr(v, "raw_value") else None} - for k, v in getattr(p, "parameters", {}).items()] - for p in plugins - ] - # The client hardcoding a sample rate (rather than reading the - # dry file's real one) plays streamed PCM at the wrong pitch - # and speed, and — since its drift-check compares elapsed - # wall-clock time against a WRONG samples-per-second - # assumption — measures genuine, ever-growing drift where - # none exists, repeatedly forcing a destructive reseek. - await ws.send(json.dumps({ - "event": "chain-loaded", "trackId": track_id, "params": params, - "sampleRate": track.sample_rate, "stable": stable, - })) - elif cmd == "unload-chain": - track = self._tracks.pop(msg["trackId"], None) - self._plugins.pop(msg["trackId"], None) - if track: - track.close() - elif cmd == "set-param": - plugin = self._plugins[msg["trackId"]][msg["pluginIndex"]] - setattr(plugin, msg["param"], msg["value"]) - elif cmd == "open-editor": - track_id, plugin_index = msg["trackId"], msg["pluginIndex"] - plugin = self._plugins[track_id][plugin_index] - close_event = threading.Event() - self._editor_close_events[(track_id, plugin_index)] = close_event - - def _open(plugin=plugin, close_event=close_event): - try: - plugin.show_editor(close_event) - finally: - self._editor_close_events.pop((track_id, plugin_index), None) - - # Fire-and-forget: don't await, so this connection's other - # messages (e.g. close-editor) keep being handled while the - # window is open (show_editor blocks the pedalboard thread, - # not this coroutine). - asyncio.create_task(self._run_on_pedalboard_thread(_open)) - # The editor window opens BEHIND the browser (this sidecar is a - # background process). Raise it to the front shortly after it's - # created — on a timer thread, since show_editor above blocks - # the pedalboard thread until the window closes. - threading.Timer(0.4, _raise_editor_window_macos).start() - elif cmd == "close-editor": - close_event = self._editor_close_events.get((msg["trackId"], msg["pluginIndex"])) - if close_event: - close_event.set() - elif cmd == "get-state": - states = serialize_states(self._plugins[msg["trackId"]]) - await ws.send(json.dumps({"event": "state", "trackId": msg["trackId"], "plugins": states})) - elif cmd == "transport": - await self._transport(ws, msg) - else: - await ws.send(json.dumps({"event": "error", "code": "bad_command"})) - except PluginMissingError as exc: - await ws.send(json.dumps({ - "event": "error", "code": "plugin_missing", - "plugin": exc.plugin_name, "trackId": msg.get("trackId"), - })) - except Exception: - # Unlike PluginMissingError, this is unexpected — print it so a - # real bug doesn't hide behind the generic "bad_command" wire - # error (a silent one cost a long live-debugging session before - # this print existed). - traceback.print_exc() - await ws.send(json.dumps({ - "event": "error", "code": "bad_command", - "trackId": msg.get("trackId"), - })) - - async def _transport(self, ws, msg: dict) -> None: - action = msg.get("action") - if action == "seek": - for track in self._tracks.values(): - track.seek(msg["timeSec"]) - elif action == "play": - self._rate = msg.get("rate", 1.0) - for track in self._tracks.values(): - track.seek(msg.get("timeSec", 0.0)) - if self._play_task: - self._play_task.cancel() - self._play_task = asyncio.create_task(self._pump(ws)) - self._play_owner = ws - elif action == "pause": - if self._play_task and self._play_owner is ws: - self._play_task.cancel() - self._play_task = None - self._play_owner = None - - def run_pedalboard_thread(self) -> None: - """Must run on the process's real main thread (see module - docstring). Blocks, processing one pedalboard-native call at a - time — a `show_editor` call itself blocks this loop until its - window is closed (by the user or `close-editor`), which is exactly - why plugin loading and editor windows must serialize through this - one thread rather than pedalboard's calls being split across - `asyncio.to_thread`'s pool. Returns when `stop_pedalboard_thread()` - enqueues the shutdown sentinel.""" - while True: - call = self._pedalboard_queue.get() - if call is None: - return - call() - - def stop_pedalboard_thread(self) -> None: - self._pedalboard_queue.put(None) - - async def _pump(self, ws) -> None: - # Absolute-deadline pacing. `deadline` is when the NEXT block is due; - # it advances by one block's real duration each iteration. Because it's - # an absolute target (not a fresh `sleep(block)` each iteration), the - # time spent processing+sending a block and the sleep's own overshoot - # never accumulate into a production deficit — when we fall behind, the - # deadline is already in the past and the next block goes out - # immediately to catch up. The old open-loop `sleep(block/rate)` - # delivered only ~96% of real time, starving the client's ring buffer: - # the worklet zero-filled the gaps (degraded audio) and the shortfall - # accrued as genuine drift until the drift-check forced a destructive - # reseek ("cuts out"). See test_pump_keeps_up_with_real_time. - loop = asyncio.get_running_loop() - # Prime the clock `_PUMP_LEAD_SEC` in the past so the first blocks are - # sent back-to-back (no sleep) until the deadline catches up to now — - # that burst pre-fills the client ring as a jitter cushion. - deadline = loop.time() - self._PUMP_LEAD_SEC - while True: - sent_any = False - block_dur = 1024 / 48000 - for track in list(self._tracks.values()): - frame = track.next_block() - if frame is None: - continue - block_dur = track.block_size / track.sample_rate - await ws.send(frame) - sent_any = True - if not sent_any: - return - deadline += block_dur / self._rate - # Cap how far behind real time the deadline may fall, so a - # transient stall (GC, a slow plugin block) re-primes the lead - # cushion instead of bursting an unbounded backlog into the - # client's fixed-size ring. - # ponytail: fixed lead cap; fine unless a plugin can't render real-time. - floor = loop.time() - self._PUMP_LEAD_SEC - if deadline < floor: - deadline = floor - sleep_for = deadline - loop.time() - if sleep_for > 0: - await asyncio.sleep(sleep_for) - - -def _process_alive(pid: int) -> bool: - """True if `pid` is a live process. `os.kill(pid, 0)` sends no signal but - raises ProcessLookupError once the process is gone (EPERM — a live process - we don't own — still counts as alive).""" - try: - os.kill(pid, 0) - except ProcessLookupError: - return False - except PermissionError: - return True - return True - - -def _watch_parent_and_exit( - watch_pid: int | None, initial_ppid: int, interval_sec: float = 1.0 -) -> None: - """Exit the process once the studio-server / `hyperframes preview` that - spawned us is gone. When it dies ungracefully (SIGKILL, crash) it can't run - its own child-teardown, so without this the sidecar is orphaned and lingers - — holding a port, showing up as a stale `hyperframes-vst serve` a later - studio has to `pkill` by hand. - - `watch_pid` is the spawner's OWN pid, passed explicitly (`--parent-pid`), - and polled via `os.kill(pid, 0)`. This is necessary because the spawner - launches us through `uv run`, so our real `getppid()` is the intervening - `uv` process — which survives the spawner's death — making a `getppid()` - change useless on its own. Falls back to the `getppid()`-change heuristic - when no `watch_pid` was given (bare/standalone launch). Runs on a daemon - thread; `os._exit` because there's nothing to flush and the asyncio server - owns the normal shutdown path.""" - while True: - time.sleep(interval_sec) - if watch_pid is not None: - if not _process_alive(watch_pid): - os._exit(0) - elif os.getppid() != initial_ppid: - os._exit(0) - - -def serve(port: int = 0, parent_pid: int | None = None) -> None: - server = VstServer() - started = threading.Event() - - # Self-reap if the spawner dies (see _watch_parent_and_exit). initial_ppid - # is the getppid()-change fallback for a bare launch with no --parent-pid. - threading.Thread( - target=_watch_parent_and_exit, - args=(parent_pid, os.getppid()), - daemon=True, - ).start() - - def _run_asyncio_server() -> None: - async def _run() -> None: - await server.start(port) - started.set() - await asyncio.Future() - - asyncio.run(_run()) - - thread = threading.Thread(target=_run_asyncio_server, daemon=True) - thread.start() - started.wait() - try: - server.run_pedalboard_thread() - except KeyboardInterrupt: - server.stop_pedalboard_thread() diff --git a/packages/vst-host/src/hyperframes_vst/stream.py b/packages/vst-host/src/hyperframes_vst/stream.py deleted file mode 100644 index 8800e01060..0000000000 --- a/packages/vst-host/src/hyperframes_vst/stream.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Per-track streaming: dry WAV -> chain -> wire frames. - -Wire frame (little-endian): u32 track_index, f64 sample_pos, interleaved f32 stereo. -""" -from __future__ import annotations - -import struct - -import numpy as np -from pedalboard import Pedalboard -from pedalboard.io import AudioFile - -HEADER = struct.Struct(" bool: - """A chain's output is stable if every sample is finite and within a sane - magnitude. Split out from `probe_chain_stability` so the accept/reject - rule is unit-testable without a misbehaving plugin.""" - if not np.all(np.isfinite(out)): - return False - return bool(np.max(np.abs(out)) <= MAX_STABLE_PEAK) if out.size else True - - -def probe_chain_stability(wav_path: str, plugins: list, seconds: float = 0.5) -> bool: - """Bounce a short slice of the dry file through the chain offline and - report whether the output is finite and bounded. - - pedalboard's headless VST3/AU host silently mis-initializes a real subset - of plugins: their DSP emits NaN/Inf from the first sample, or runs away to - astronomical magnitudes (~1e36). This is a known, unresolved pedalboard - limitation (github.com/spotify/pedalboard#390, closed "not planned"), and - pedalboard publishes no compatibility list — so the only reliable check is - to run the plugin and look at what comes out. An unstable chain is left - unregistered for streaming; the client keeps the track on its dry audio and - warns, rather than muting the original into NaN-driven silence. - """ - with AudioFile(wav_path) as f: - sr = f.samplerate - n = min(f.frames, max(1, int(sr * seconds))) - if f.frames == 0: - return True # nothing to stream — vacuously fine - chunk = f.read(n) - if chunk.shape[0] == 1: - chunk = np.vstack([chunk, chunk]) - return output_is_stable(Pedalboard(plugins)(chunk, sr, reset=True)) - - -def encode_frame(track_index: int, sample_pos: int, pcm: np.ndarray) -> bytes: - interleaved = np.ascontiguousarray(pcm.T, dtype=np.float32) - return HEADER.pack(track_index, float(sample_pos)) + interleaved.tobytes() - - -def decode_frame(data: bytes) -> tuple[int, int, np.ndarray]: - track_index, sample_pos = HEADER.unpack_from(data) - flat = np.frombuffer(data, dtype=np.float32, offset=HEADER.size) - return track_index, int(sample_pos), flat.reshape(-1, 2).T - - -class TrackStream: - def __init__( - self, track_index: int, wav_path: str, plugins: list, block_size: int = 1024, stable: bool = True - ): - self.track_index = track_index - self.block_size = block_size - # An unstable chain (see `probe_chain_stability`) still occupies a - # track slot so wire indices stay in lockstep with the client's own - # counter, but never emits frames — the client keeps it dry. - self.stable = stable - self._board = Pedalboard(plugins) - self._file = AudioFile(wav_path) - self.sample_rate = self._file.samplerate - self._pos = 0 - self._needs_reset = True - - def seek(self, time_sec: float) -> None: - self._pos = int(time_sec * self.sample_rate) - self._needs_reset = True - - def next_block(self) -> bytes | None: - if not self.stable: - return None - if self._pos >= self._file.frames: - return None - self._file.seek(self._pos) - chunk = self._file.read(min(self.block_size, self._file.frames - self._pos)) - if chunk.shape[0] == 1: - chunk = np.vstack([chunk, chunk]) - out = self._board(chunk, self.sample_rate, reset=self._needs_reset) - self._needs_reset = False - frame = encode_frame(self.track_index, self._pos, out.astype(np.float32)) - self._pos += chunk.shape[1] - return frame - - def close(self) -> None: - self._file.close() diff --git a/packages/vst-host/tests/test_bounce.py b/packages/vst-host/tests/test_bounce.py deleted file mode 100644 index faf7ed2038..0000000000 --- a/packages/vst-host/tests/test_bounce.py +++ /dev/null @@ -1,92 +0,0 @@ -import json -import subprocess -import sys - -import numpy as np -import pytest -from pedalboard.io import AudioFile - -from hyperframes_vst.bounce import bounce_file - - -@pytest.fixture -def dry_wav(tmp_path): - sr = 48000 - rng = np.random.default_rng(7) - audio = (rng.standard_normal((2, sr * 2)) * 0.1).astype(np.float32) - path = str(tmp_path / "dry.wav") - with AudioFile(path, "w", sr, 2) as f: - f.write(audio) - return path - - -@pytest.fixture -def reverb_chain(tmp_path): - path = tmp_path / "chain.json" - path.write_text(json.dumps({ - "version": 1, - "plugins": [{"format": "builtin", "path": "Reverb", "pluginName": None, "name": "Reverb", "stateB64": None}], - })) - return str(path) - - -def read_wav(path): - with AudioFile(path) as f: - return f.read(f.frames) - - -def test_bounce_changes_audio_and_preserves_length(dry_wav, reverb_chain, tmp_path): - out = str(tmp_path / "wet.wav") - bounce_file(dry_wav, reverb_chain, out) - dry, wet = read_wav(dry_wav), read_wav(out) - assert wet.shape == dry.shape - assert not np.array_equal(wet, dry) - - -def test_bounce_deterministic_for_builtin(dry_wav, reverb_chain, tmp_path): - a, b = str(tmp_path / "a.wav"), str(tmp_path / "b.wav") - bounce_file(dry_wav, reverb_chain, a) - bounce_file(dry_wav, reverb_chain, b) - assert np.array_equal(read_wav(a), read_wav(b)) - - -def test_cli_bounce_exit_codes(dry_wav, tmp_path): - missing_chain = tmp_path / "missing.json" - missing_chain.write_text(json.dumps({ - "version": 1, - "plugins": [{"format": "vst3", "path": "/nonexistent/Gone.vst3", "pluginName": None, "name": "Gone", "stateB64": None}], - })) - proc = subprocess.run( - [sys.executable, "-m", "hyperframes_vst", "bounce", - "--input", dry_wav, "--chain", str(missing_chain), "--output", str(tmp_path / "o.wav")], - capture_output=True, text=True, - ) - assert proc.returncode == 3 - assert "PLUGIN_MISSING Gone" in proc.stderr - - -def test_bounce_bypasses_disabled_plugins(dry_wav, tmp_path): - """A chain whose only plugin is disabled must bounce bit-identical to dry.""" - import json - - import numpy as np - from pedalboard.io import AudioFile - - chain_path = tmp_path / "disabled.vstchain.json" - chain_path.write_text(json.dumps({ - "version": 1, - "plugins": [{ - "format": "builtin", "path": "Distortion", "pluginName": None, - "name": "Distortion", "stateB64": None, "enabled": False, - }], - })) - out = tmp_path / "out.wav" - bounce_file(str(dry_wav), str(chain_path), str(out)) - with AudioFile(str(dry_wav)) as a, AudioFile(str(out)) as b: - dry = a.read(a.frames) - wet = b.read(b.frames) - assert dry.shape == wet.shape - # Within WAV-write quantization (the bounce re-encodes the file; a - # 16-bit round trip alone is ~3e-5) — NOT within a Distortion's reach, - # which the un-bypassed same chain fails by orders of magnitude. - assert np.allclose(dry, wet, atol=5e-4) diff --git a/packages/vst-host/tests/test_carve.py b/packages/vst-host/tests/test_carve.py deleted file mode 100644 index 08deb7b286..0000000000 --- a/packages/vst-host/tests/test_carve.py +++ /dev/null @@ -1,47 +0,0 @@ -import numpy as np -import pytest - -from hyperframes_vst.carve import carve, CARVE_Q, SPEECH_LO_HZ, SPEECH_HI_HZ - - -def _tone(freq_hz: float, sr: int = 44100, seconds: float = 1.0) -> np.ndarray: - t = np.arange(int(sr * seconds)) / sr - return np.sin(2 * np.pi * freq_hz * t).astype(np.float32) - - -def test_flags_the_band_where_the_voice_has_energy(): - sr = 44100 - # Strong 1 kHz tone over quiet broadband noise -> presence sits at ~1 kHz. - rng = np.random.default_rng(0) - sig = _tone(1000.0, sr) + 0.02 * rng.standard_normal(sr).astype(np.float32) - bands = carve(sig, sr, max_cut_db=4.0) - assert 2 <= len(bands) <= 4 - freqs = [b["freq"] for b in bands] - assert any(800.0 <= f <= 1250.0 for f in freqs), freqs - - -def test_bands_stay_in_speech_range_and_are_cuts(): - sr = 44100 - rng = np.random.default_rng(1) - sig = rng.standard_normal(sr).astype(np.float32) # broadband - bands = carve(sig, sr, max_cut_db=4.0) - for b in bands: - assert SPEECH_LO_HZ <= b["freq"] <= SPEECH_HI_HZ - assert b["gainDb"] < 0.0 - assert b["q"] == CARVE_Q - - -def test_amount_controls_depth(): - sr = 44100 - sig = _tone(1000.0, sr) - shallow = carve(sig, sr, max_cut_db=2.0) - deep = carve(sig, sr, max_cut_db=6.0) - assert min(b["gainDb"] for b in deep) < min(b["gainDb"] for b in shallow) - - -def test_accepts_stereo_shape(): - sr = 44100 - mono = _tone(1000.0, sr) - stereo = np.stack([mono, mono]) # (2, frames) - bands = carve(stereo, sr, max_cut_db=4.0) - assert 2 <= len(bands) <= 4 diff --git a/packages/vst-host/tests/test_carve_bounce.py b/packages/vst-host/tests/test_carve_bounce.py deleted file mode 100644 index 174b4dedcc..0000000000 --- a/packages/vst-host/tests/test_carve_bounce.py +++ /dev/null @@ -1,50 +0,0 @@ -# packages/vst-host/tests/test_carve_bounce.py -import base64 -import json - -import numpy as np -from pedalboard import Pedalboard, PeakFilter - -from hyperframes_vst.carve import carve - - -def _band_energy(sig: np.ndarray, sr: int, center: float) -> float: - spec = np.abs(np.fft.rfft(sig)) ** 2 - freqs = np.fft.rfftfreq(sig.shape[0], d=1.0 / sr) - lo = center / (2.0 ** (1.0 / 6.0)) - hi = center * (2.0 ** (1.0 / 6.0)) - mask = (freqs >= lo) & (freqs < hi) - return float(spec[mask].sum()) - - -def test_carve_bands_actually_dip_when_bounced(): - sr = 44100 - rng = np.random.default_rng(7) - # Voice: presence bump at 1 kHz so carve targets it. - t = np.arange(sr) / sr - voice = (np.sin(2 * np.pi * 1000.0 * t) + 0.05 * rng.standard_normal(sr)).astype(np.float32) - bands = carve(voice, sr, max_cut_db=6.0) - assert bands - - # Music: broadband noise we run through the carve chain. - music = rng.standard_normal(sr).astype(np.float32) - board = Pedalboard( - [PeakFilter(cutoff_frequency_hz=b["freq"], gain_db=b["gainDb"], q=b["q"]) for b in bands] - ) - out = board(music, sr) - - # Every carved band must have LESS energy after the chain than before. - for b in bands: - before = _band_energy(music, sr, b["freq"]) - after = _band_energy(out.reshape(-1), sr, b["freq"]) - assert after < before, (b["freq"], before, after) - - -def test_band_params_roundtrip_through_builtin_state(): - # The stateB64 the panel writes must decode to the exact PeakFilter params. - b = carve(np.sin(2 * np.pi * 1000.0 * np.arange(44100) / 44100).astype(np.float32), 44100)[0] - state = base64.b64encode( - json.dumps({"cutoff_frequency_hz": b["freq"], "gain_db": b["gainDb"], "q": b["q"]}).encode() - ).decode() - decoded = json.loads(base64.b64decode(state)) - assert decoded == {"cutoff_frequency_hz": b["freq"], "gain_db": b["gainDb"], "q": b["q"]} diff --git a/packages/vst-host/tests/test_carve_cli.py b/packages/vst-host/tests/test_carve_cli.py deleted file mode 100644 index 37330ff139..0000000000 --- a/packages/vst-host/tests/test_carve_cli.py +++ /dev/null @@ -1,33 +0,0 @@ -import json -import subprocess -import sys -import wave -from pathlib import Path - -import numpy as np - - -def _write_wav(path: Path, freq: float = 1000.0, sr: int = 44100, seconds: float = 1.0) -> None: - t = np.arange(int(sr * seconds)) / sr - sig = (0.5 * np.sin(2 * np.pi * freq * t) * 32767).astype(np.int16) - with wave.open(str(path), "w") as w: - w.setnchannels(1) - w.setsampwidth(2) - w.setframerate(sr) - w.writeframes(sig.tobytes()) - - -def test_carve_verb_prints_bands_json(tmp_path: Path): - voice = tmp_path / "voice.wav" - _write_wav(voice, freq=1000.0) - proc = subprocess.run( - [sys.executable, "-m", "hyperframes_vst", "carve", "--voice", str(voice), "--json"], - capture_output=True, - text=True, - ) - assert proc.returncode == 0, proc.stderr - payload = json.loads(proc.stdout) - assert "bands" in payload - assert 2 <= len(payload["bands"]) <= 4 - for b in payload["bands"]: - assert set(b) == {"freq", "gainDb", "q"} diff --git a/packages/vst-host/tests/test_chain.py b/packages/vst-host/tests/test_chain.py deleted file mode 100644 index c85e4545f2..0000000000 --- a/packages/vst-host/tests/test_chain.py +++ /dev/null @@ -1,96 +0,0 @@ -import base64 -import json - -import pytest - -from hyperframes_vst.chain import ( - ChainError, - PluginMissingError, - build_chain, - load_chain_spec, - serialize_states, -) - - -def make_spec_json(plugins): - return json.dumps({"version": 1, "plugins": plugins}) - - -def test_load_chain_spec_roundtrip(): - text = make_spec_json( - [{"format": "builtin", "path": "Reverb", "pluginName": None, "name": "Reverb", "stateB64": None}] - ) - spec = load_chain_spec(text) - assert spec.version == 1 - assert spec.plugins[0].format == "builtin" - assert spec.plugins[0].path == "Reverb" - - -def test_load_chain_spec_rejects_bad_version(): - with pytest.raises(ChainError): - load_chain_spec(json.dumps({"version": 99, "plugins": []})) - - -def test_load_chain_spec_rejects_unknown_format(): - with pytest.raises(ChainError): - load_chain_spec(make_spec_json([{"format": "vst2", "path": "x", "pluginName": None, "name": "x", "stateB64": None}])) - - -def test_build_chain_builtin_applies_state(): - state = base64.b64encode(json.dumps({"room_size": 0.75}).encode()).decode() - spec = load_chain_spec( - make_spec_json([{"format": "builtin", "path": "Reverb", "pluginName": None, "name": "Reverb", "stateB64": state}]) - ) - plugins = build_chain(spec) - assert abs(plugins[0].room_size - 0.75) < 1e-6 - - -def test_build_chain_missing_vst3_raises_named_error(): - spec = load_chain_spec( - make_spec_json([{"format": "vst3", "path": "/nonexistent/Nope.vst3", "pluginName": None, "name": "Nope", "stateB64": None}]) - ) - with pytest.raises(PluginMissingError) as exc: - build_chain(spec) - assert "Nope" in str(exc.value) - - -def test_serialize_states_builtin_roundtrip(): - spec = load_chain_spec( - make_spec_json([{"format": "builtin", "path": "Reverb", "pluginName": None, "name": "Reverb", "stateB64": None}]) - ) - plugins = build_chain(spec) - plugins[0].room_size = 0.42 - states = serialize_states(plugins) - restored = json.loads(base64.b64decode(states[0])) - assert abs(restored["room_size"] - 0.42) < 1e-6 - - -def test_enabled_defaults_true_and_reads_false(): - spec = load_chain_spec( - make_spec_json( - [ - {"format": "builtin", "path": "Reverb", "pluginName": None, "name": "R", "stateB64": None}, - {"format": "builtin", "path": "Gain", "pluginName": None, "name": "G", "stateB64": None, "enabled": False}, - ] - ) - ) - assert spec.plugins[0].enabled is True # absent -> enabled (old files) - assert spec.plugins[1].enabled is False - - -def test_enabled_plugins_filters_the_board_but_build_keeps_all(): - from hyperframes_vst.chain import enabled_plugins - - spec = load_chain_spec( - make_spec_json( - [ - {"format": "builtin", "path": "Reverb", "pluginName": None, "name": "R", "stateB64": None}, - {"format": "builtin", "path": "Gain", "pluginName": None, "name": "G", "stateB64": None, "enabled": False}, - {"format": "builtin", "path": "Delay", "pluginName": None, "name": "D", "stateB64": None}, - ] - ) - ) - built = build_chain(spec) - assert len(built) == 3 # ALL constructed — indices stay chain-file aligned - active = enabled_plugins(spec, built) - assert [type(p).__name__ for p in active] == ["Reverb", "Delay"] # bypassed Gain excluded diff --git a/packages/vst-host/tests/test_scan.py b/packages/vst-host/tests/test_scan.py deleted file mode 100644 index 99d9947d3c..0000000000 --- a/packages/vst-host/tests/test_scan.py +++ /dev/null @@ -1,38 +0,0 @@ -import json -import sys - -from hyperframes_vst.scan import scan_paths - - -def make_probe_script(tmp_path, body: str) -> list[str]: - script = tmp_path / "probe.py" - script.write_text(body) - return [sys.executable, str(script)] - - -def test_scan_collects_names_from_probe(tmp_path): - bundle = tmp_path / "Fake.vst3" - bundle.mkdir() - probe = make_probe_script( - tmp_path, "import json,sys; print(json.dumps([{'name': 'FakePlugin'}]))" - ) - result = scan_paths([str(tmp_path)], probe_cmd=probe) - assert result == [{"path": str(bundle), "name": "FakePlugin", "format": "vst3"}] - - -def test_scan_survives_crashing_probe(tmp_path): - (tmp_path / "Bad.vst3").mkdir() - good = tmp_path / "Good.vst3" - good.mkdir() - probe = make_probe_script( - tmp_path, - "import json,sys\n" - "if 'Bad' in sys.argv[1]: import os; os.abort()\n" - "print(json.dumps([{'name': 'GoodPlugin'}]))", - ) - result = scan_paths([str(tmp_path)], probe_cmd=probe) - assert [r["name"] for r in result] == ["GoodPlugin"] - - -def test_scan_ignores_missing_dirs(tmp_path): - assert scan_paths([str(tmp_path / "nope")]) == [] diff --git a/packages/vst-host/tests/test_server.py b/packages/vst-host/tests/test_server.py deleted file mode 100644 index f6964d86c2..0000000000 --- a/packages/vst-host/tests/test_server.py +++ /dev/null @@ -1,649 +0,0 @@ -import asyncio -import base64 -import json -import threading -import time - -import numpy as np -import pytest -import websockets -from pedalboard.io import AudioFile - -from hyperframes_vst.server import VstServer -from hyperframes_vst.stream import ( - MAX_STABLE_PEAK, - TrackStream, - decode_frame, - output_is_stable, - probe_chain_stability, -) - - -@pytest.fixture -def dry_wav(tmp_path): - sr = 48000 - audio = (np.ones((2, sr)) * 0.25).astype(np.float32) - path = str(tmp_path / "dry.wav") - with AudioFile(path, "w", sr, 2) as f: - f.write(audio) - return path - - -CHAIN = { - "version": 1, - "plugins": [{"format": "builtin", "path": "Gain", "pluginName": None, "name": "Gain", "stateB64": None}], -} - - -_USE_REAL_TOKEN = object() - - -def ws_uri(server: VstServer, port: int, token: object = _USE_REAL_TOKEN) -> str: - """Builds the sidecar's WS URI with the shared-secret `?token=` query - param (see server.py's `_authenticate`). Defaults to the server's real - token; pass `token=None` for no query param at all, or any other string - to test rejection with a wrong token.""" - used_token = server.token if token is _USE_REAL_TOKEN else token - if used_token is None: - return f"ws://127.0.0.1:{port}" - return f"ws://127.0.0.1:{port}/?token={used_token}" - - -async def recv_json(ws): - while True: - msg = await asyncio.wait_for(ws.recv(), timeout=5) - if isinstance(msg, str): - return json.loads(msg) - - -async def recv_binary(ws): - while True: - msg = await asyncio.wait_for(ws.recv(), timeout=5) - if isinstance(msg, bytes): - return msg - - -@pytest.mark.asyncio -async def test_chain_loaded_reports_the_dry_files_real_sample_rate(tmp_path): - # dry_wav (used by most other tests here) happens to be 48000Hz already, - # so it can't catch a client that ignores this field and hardcodes a - # constant instead — a 44100Hz file (the common case for real music - # tracks) makes the mismatch concrete: the sidecar must report the - # FILE's own rate, not the wire protocol's usual round-number default. - sr = 44100 - audio = (np.ones((2, sr)) * 0.25).astype(np.float32) - path = str(tmp_path / "dry-44100.wav") - with AudioFile(path, "w", sr, 2) as f: - f.write(audio) - - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send(json.dumps({"cmd": "load-chain", "trackId": "music", "chainJson": CHAIN, "wavPath": path})) - loaded = await recv_json(ws) - assert loaded["event"] == "chain-loaded" - assert loaded["sampleRate"] == 44100 - - -def test_builtin_registry_entries_are_real_pedalboard_effect_classes(): - import pedalboard - from hyperframes_vst.scan import builtin_registry - - entries = builtin_registry() - assert len(entries) >= 10 - names = {e["name"] for e in entries} - assert {"Reverb", "Delay", "Distortion"} <= names # the staples must be offered - for e in entries: - assert e["format"] == "builtin" - # `path` must resolve to a real pedalboard class (chain.py builds it via - # getattr(pedalboard, path)); a typo here would 404 the effect at add time. - assert isinstance(getattr(pedalboard, e["path"], None), type), e["path"] - - -@pytest.mark.asyncio -async def test_scan_lists_builtins_ahead_of_discovered_plugins(monkeypatch): - # Isolate from whatever plugins happen to be installed on the test machine. - monkeypatch.setattr("hyperframes_vst.server.scan_paths", lambda *a, **k: []) - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send(json.dumps({"cmd": "scan"})) - reg = await recv_json(ws) - assert reg["event"] == "registry" - names = [p["name"] for p in reg["plugins"]] - assert "Reverb" in names and "Delay" in names - assert all(p["format"] == "builtin" for p in reg["plugins"]) # only builtins, since disk scan is stubbed empty - await server.stop() - - -def test_output_is_stable_accepts_normal_and_rejects_nan_inf_and_runaway(): - sr = 1000 - assert output_is_stable(np.zeros((2, sr), dtype=np.float32)) is True - assert output_is_stable((np.ones((2, sr)) * 0.8).astype(np.float32)) is True - nan = np.zeros((2, sr), dtype=np.float32) - nan[0, 5] = np.nan - assert output_is_stable(nan) is False - inf = np.zeros((2, sr), dtype=np.float32) - inf[1, 3] = np.inf - assert output_is_stable(inf) is False - # Some plugins run away to astronomical (still-finite) magnitudes before - # hitting NaN — those are unstable too (see ValhallaFreqEcho under pedalboard). - runaway = np.full((2, sr), MAX_STABLE_PEAK * 10, dtype=np.float32) - assert output_is_stable(runaway) is False - - -def test_probe_reports_a_working_builtin_chain_as_stable(dry_wav): - from hyperframes_vst.chain import build_chain, load_chain_spec - - plugins = build_chain(load_chain_spec(json.dumps(CHAIN))) - assert probe_chain_stability(dry_wav, plugins) is True - - -def test_unstable_track_never_emits_a_frame(dry_wav): - # An unstable chain is still constructed (so its wire index stays in - # lockstep with the client), but must never stream — the client keeps it dry. - from hyperframes_vst.chain import build_chain, load_chain_spec - - plugins = build_chain(load_chain_spec(json.dumps(CHAIN))) - track = TrackStream(0, dry_wav, plugins, stable=False) - assert track.next_block() is None - track.close() - - -@pytest.mark.asyncio -async def test_chain_loaded_reports_stability_and_unstable_never_streams(dry_wav, monkeypatch): - # Force the probe to declare the chain unstable without needing a plugin - # that actually misbehaves in CI. - monkeypatch.setattr("hyperframes_vst.server.probe_chain_stability", lambda *a, **k: False) - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send(json.dumps({"cmd": "load-chain", "trackId": "m", "chainJson": CHAIN, "wavPath": dry_wav})) - loaded = await recv_json(ws) - assert loaded["event"] == "chain-loaded" - assert loaded["stable"] is False - - await ws.send(json.dumps({"cmd": "transport", "action": "play", "timeSec": 0.0, "rate": 1.0})) - # No PCM should ever arrive for an unstable track — the pump finds no - # sendable frame and stops. Expect a timeout, not a binary frame. - with pytest.raises(asyncio.TimeoutError): - await asyncio.wait_for(recv_binary(ws), timeout=1.0) - await server.stop() - - -@pytest.mark.asyncio -async def test_load_chain_and_stream(dry_wav): - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send(json.dumps({"cmd": "load-chain", "trackId": "music", "chainJson": CHAIN, "wavPath": dry_wav})) - loaded = await recv_json(ws) - assert loaded["event"] == "chain-loaded" - assert loaded["trackId"] == "music" - # The client hardcoding a sample rate instead of reading this field - # plays streamed PCM at the wrong pitch/speed, and its drift-check - # then misreads the resulting rate mismatch as ever-growing drift. - assert loaded["sampleRate"] == 48000 # dry_wav's real rate - assert loaded["stable"] is True # a builtin Gain chain hosts fine - - await ws.send(json.dumps({"cmd": "transport", "action": "play", "timeSec": 0.0, "rate": 1.0})) - frame = await recv_binary(ws) - idx, pos, pcm = decode_frame(frame) - assert idx == 0 - assert pcm.shape[0] == 2 - await ws.send(json.dumps({"cmd": "transport", "action": "pause"})) - await server.stop() - - -@pytest.mark.asyncio -async def test_pump_keeps_up_with_real_time(tmp_path): - # The client plays streamed PCM through a real-time AudioContext: it - # consumes exactly `sample_rate` samples per wall-clock second. If the - # sidecar's pump delivers fewer than that, the client's ring buffer - # starves — the worklet zero-fills the gaps (choppy/degraded audio) and - # the shortfall accumulates as genuine drift until the drift-check trips - # a destructive reseek (playback "cuts out"). - # - # `_pump`'s open-loop `await sleep(block/rate)` never subtracts the time - # spent processing+sending a block, nor the sleep's own overshoot, so the - # real period is always LONGER than one block — a systematic production - # deficit. This test drives real playback and asserts the delivered - # sample count keeps pace with the wall clock it took to deliver them. - sr = 48000 - seconds = 6 - audio = (np.ones((2, sr * seconds)) * 0.25).astype(np.float32) - path = str(tmp_path / "long.wav") - with AudioFile(path, "w", sr, 2) as f: - f.write(audio) - - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send(json.dumps({"cmd": "load-chain", "trackId": "m", "chainJson": CHAIN, "wavPath": path})) - loaded = await recv_json(ws) - assert loaded["event"] == "chain-loaded" - - await ws.send(json.dumps({"cmd": "transport", "action": "play", "timeSec": 0.0, "rate": 1.0})) - - # Drain the very first frame to mark the moment real streaming begins, - # then measure only the steady-state pump from there (excludes the - # one-time load/handshake latency, which the client anchors its drift - # baseline to anyway). - first = await recv_binary(ws) - _idx, _pos, first_pcm = decode_frame(first) - delivered = first_pcm.shape[1] - start = time.monotonic() - - target_wall = 3.0 - while True: - frame = await recv_binary(ws) - _i, _p, pcm = decode_frame(frame) - delivered += pcm.shape[1] - if time.monotonic() - start >= target_wall: - break - - elapsed = time.monotonic() - start - await ws.send(json.dumps({"cmd": "transport", "action": "pause"})) - - real_time_samples = sr * elapsed - ratio = delivered / real_time_samples - # A real-time consumer needs >=100%; allow 1% for measurement jitter. - assert ratio >= 0.99, ( - f"pump delivered {delivered} samples in {elapsed:.3f}s " - f"({ratio:.1%} of the {real_time_samples:.0f} a real-time client consumes) " - f"— production deficit starves the client ring buffer" - ) - await server.stop() - - -@pytest.mark.asyncio -async def test_missing_plugin_reports_error(dry_wav): - server = VstServer() - port = await server.start(0) - # format="vst3" routes load-chain through the pedalboard thread (see - # module docstring on server.py) — needs a consumer running, unlike the - # builtin-only chains most other tests in this file use. - host_thread = threading.Thread(target=server.run_pedalboard_thread, name="pedalboard", daemon=True) - host_thread.start() - bad = {"version": 1, "plugins": [{"format": "vst3", "path": "/no/Gone.vst3", "pluginName": None, "name": "Gone", "stateB64": None}]} - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send(json.dumps({"cmd": "load-chain", "trackId": "t", "chainJson": bad, "wavPath": dry_wav})) - err = await recv_json(ws) - assert err["event"] == "error" - assert err["code"] == "plugin_missing" - assert err["plugin"] == "Gone" - server.stop_pedalboard_thread() - host_thread.join(timeout=2) - await server.stop() - - -@pytest.mark.asyncio -async def test_get_state_roundtrip(dry_wav): - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send(json.dumps({"cmd": "load-chain", "trackId": "m", "chainJson": CHAIN, "wavPath": dry_wav})) - await recv_json(ws) - await ws.send(json.dumps({"cmd": "set-param", "trackId": "m", "pluginIndex": 0, "param": "gain_db", "value": -6.0})) - await ws.send(json.dumps({"cmd": "get-state", "trackId": "m"})) - state = await recv_json(ws) - assert state["event"] == "state" - params = json.loads(base64.b64decode(state["plugins"][0])) - assert abs(params["gain_db"] + 6.0) < 1e-6 - await server.stop() - - -@pytest.mark.asyncio -async def test_unrelated_client_disconnect_does_not_kill_other_clients_playback(dry_wav): - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port)) as ws_a: - await ws_a.send( - json.dumps({"cmd": "load-chain", "trackId": "music", "chainJson": CHAIN, "wavPath": dry_wav}) - ) - loaded = await recv_json(ws_a) - assert loaded["event"] == "chain-loaded" - - await ws_a.send(json.dumps({"cmd": "transport", "action": "play", "timeSec": 0.0, "rate": 1.0})) - frame = await recv_binary(ws_a) - idx, _pos, _pcm = decode_frame(frame) - assert idx == 0 - - # Client B connects and disconnects without ever calling play. - ws_b = await websockets.connect(ws_uri(server, port)) - await ws_b.close() - - # Client A's playback must still be alive: another frame should arrive. - frame2 = await recv_binary(ws_a) - idx2, _pos2, _pcm2 = decode_frame(frame2) - assert idx2 == 0 - - await ws_a.send(json.dumps({"cmd": "transport", "action": "pause"})) - await server.stop() - - -@pytest.mark.asyncio -async def test_unrelated_client_pause_does_not_kill_other_clients_playback(dry_wav): - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port)) as ws_a: - await ws_a.send( - json.dumps({"cmd": "load-chain", "trackId": "music", "chainJson": CHAIN, "wavPath": dry_wav}) - ) - loaded = await recv_json(ws_a) - assert loaded["event"] == "chain-loaded" - - await ws_a.send(json.dumps({"cmd": "transport", "action": "play", "timeSec": 0.0, "rate": 1.0})) - frame = await recv_binary(ws_a) - idx, _pos, _pcm = decode_frame(frame) - assert idx == 0 - - # Client B connects and sends pause without ever having loaded or played anything itself. - async with websockets.connect(ws_uri(server, port)) as ws_b: - await ws_b.send(json.dumps({"cmd": "transport", "action": "pause"})) - - # Client A's playback must still be alive: another frame should arrive. - frame2 = await recv_binary(ws_a) - idx2, _pos2, _pcm2 = decode_frame(frame2) - assert idx2 == 0 - - await ws_a.send(json.dumps({"cmd": "transport", "action": "pause"})) - await server.stop() - - -@pytest.mark.asyncio -async def test_command_for_unknown_track_id_replies_bad_command_instead_of_closing(dry_wav): - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send(json.dumps({"cmd": "get-state", "trackId": "never-loaded"})) - err = await recv_json(ws) - assert err["event"] == "error" - assert err["code"] == "bad_command" - - # Connection must still be alive and able to handle further commands. - await ws.send( - json.dumps({"cmd": "load-chain", "trackId": "music", "chainJson": CHAIN, "wavPath": dry_wav}) - ) - loaded = await recv_json(ws) - assert loaded["event"] == "chain-loaded" - await server.stop() - - -@pytest.mark.asyncio -async def test_connect_with_correct_token_succeeds(dry_wav): - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port, server.token)) as ws: - await ws.send(json.dumps({"cmd": "get-state", "trackId": "never-loaded"})) - err = await recv_json(ws) - assert err["event"] == "error" - assert err["code"] == "bad_command" - await server.stop() - - -@pytest.mark.asyncio -async def test_connect_with_missing_token_is_rejected(dry_wav): - server = VstServer() - port = await server.start(0) - with pytest.raises(websockets.exceptions.InvalidStatus): - async with websockets.connect(ws_uri(server, port, token=None)): - pass - await server.stop() - - -@pytest.mark.asyncio -async def test_connect_with_wrong_token_is_rejected(dry_wav): - server = VstServer() - port = await server.start(0) - with pytest.raises(websockets.exceptions.InvalidStatus): - async with websockets.connect(ws_uri(server, port, token="not-the-real-token")): - pass - await server.stop() - - -class _FakeEditorPlugin: - """Mimics pedalboard's real-plugin contract just enough to test the - hand-off: `show_editor(close_event)` blocks until the event is set.""" - - def __init__(self): - self.show_editor_calls = [] - self.calling_thread_name = None - - def show_editor(self, close_event=None): - self.calling_thread_name = threading.current_thread().name - self.show_editor_calls.append(close_event) - if close_event is not None: - close_event.wait(timeout=5) - - -async def _wait_until(predicate, timeout=5, interval=0.05): - elapsed = 0.0 - while not predicate(): - await asyncio.sleep(interval) - elapsed += interval - if elapsed >= timeout: - raise AssertionError("condition never became true") - - -@pytest.mark.asyncio -async def test_open_editor_runs_show_editor_on_the_pedalboard_thread_not_the_event_loop(dry_wav): - """Regression test: pedalboard's show_editor() raises RuntimeError unless - called from the process's real main thread (the pedalboard thread - stands in for that here in tests). open-editor must hand off via the - queue, not call show_editor() directly from the asyncio dispatch.""" - server = VstServer() - port = await server.start(0) - plugin = _FakeEditorPlugin() - server._plugins["music"] = [plugin] - - host_thread = threading.Thread(target=server.run_pedalboard_thread, name="pedalboard", daemon=True) - host_thread.start() - - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send(json.dumps({"cmd": "open-editor", "trackId": "music", "pluginIndex": 0})) - await _wait_until(lambda: len(plugin.show_editor_calls) == 1) - - assert plugin.calling_thread_name == "pedalboard" - assert isinstance(plugin.show_editor_calls[0], threading.Event) - assert ("music", 0) in server._editor_close_events - - await ws.send(json.dumps({"cmd": "close-editor", "trackId": "music", "pluginIndex": 0})) - await _wait_until(lambda: ("music", 0) not in server._editor_close_events) - - server.stop_pedalboard_thread() - host_thread.join(timeout=2) - assert not host_thread.is_alive() - await server.stop() - - -@pytest.mark.asyncio -async def test_load_chain_runs_on_the_same_pedalboard_thread_as_open_editor(dry_wav): - """Regression test: pedalboard requires every native plugin-loading call - AND every show_editor call to happen on the exact same thread for the - life of the process (confirmed against a real VST3: loading a second - plugin from a different thread than the first raises RuntimeError, and - show_editor requires that same thread to be the true main thread). If - load-chain's `build_chain` call ever goes back to running on - `asyncio.to_thread`'s pool instead of the pedalboard thread, this test - catches the regression before it reaches a real plugin.""" - server = VstServer() - port = await server.start(0) - - host_thread = threading.Thread(target=server.run_pedalboard_thread, name="pedalboard", daemon=True) - host_thread.start() - - calling_threads = [] - - class _RecordingPlugin: - def show_editor(self, close_event=None): - calling_threads.append(threading.current_thread().name) - if close_event is not None: - close_event.wait(timeout=5) - - import hyperframes_vst.server as server_module - - # format="vst3" (not "builtin"): this is the case that must route - # through the pedalboard thread, per the conditional in _dispatch. - external_chain = { - "version": 1, - "plugins": [{"format": "vst3", "path": "/fake.vst3", "pluginName": None, "name": "Fake", "stateB64": None}], - } - - def fake_build_chain(spec): - calling_threads.append(threading.current_thread().name) - return [_RecordingPlugin()] - - class _FakeTrackStream: - """Stands in for the real TrackStream, which internally builds a - real pedalboard.Pedalboard(plugins) — incompatible with the fake - _RecordingPlugin above. This test is only about which thread - build_chain/show_editor run on, not audio streaming.""" - - def __init__(self, track_index, wav_path, plugins, stable=True): - self.sample_rate = 48000 - self.stable = stable - - def close(self): - pass - - original = server_module.build_chain - original_track_stream = server_module.TrackStream - original_probe = server_module.probe_chain_stability - server_module.build_chain = fake_build_chain - server_module.TrackStream = _FakeTrackStream - # The stability probe wraps plugins in a real Pedalboard, which the fake - # _RecordingPlugin can't join; this test is only about thread affinity. - server_module.probe_chain_stability = lambda *a, **k: True - try: - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send( - json.dumps( - {"cmd": "load-chain", "trackId": "music", "chainJson": external_chain, "wavPath": dry_wav} - ) - ) - loaded = await recv_json(ws) - assert loaded["event"] == "chain-loaded" - - await ws.send(json.dumps({"cmd": "open-editor", "trackId": "music", "pluginIndex": 0})) - await _wait_until(lambda: len(calling_threads) == 2) - - assert calling_threads == ["pedalboard", "pedalboard"] - - await ws.send(json.dumps({"cmd": "close-editor", "trackId": "music", "pluginIndex": 0})) - await _wait_until(lambda: ("music", 0) not in server._editor_close_events) - finally: - server_module.build_chain = original - server_module.TrackStream = original_track_stream - server_module.probe_chain_stability = original_probe - - server.stop_pedalboard_thread() - host_thread.join(timeout=2) - await server.stop() - - -@pytest.mark.asyncio -async def test_builtin_chain_load_does_not_require_the_pedalboard_thread(dry_wav): - """A builtin plugin (Gain, Reverb, ...) never touches JUCE's native - plugin-loading machinery, so load-chain must keep working even when - nobody has started run_pedalboard_thread() — exactly the setup every - other test in this file already uses. This guards the fast path the - external-plugin fix (above) must not regress.""" - server = VstServer() - port = await server.start(0) - async with websockets.connect(ws_uri(server, port)) as ws: - await ws.send(json.dumps({"cmd": "load-chain", "trackId": "music", "chainJson": CHAIN, "wavPath": dry_wav})) - loaded = await recv_json(ws) - assert loaded["event"] == "chain-loaded" - await server.stop() - - -def test_raise_editor_window_is_a_safe_noop_off_macos(monkeypatch): - # The editor-raise helper is macOS-only (System Events activation); on any - # other platform it must return without spawning anything. Also proves it - # never throws — it's best-effort and called from a fire-and-forget timer. - import hyperframes_vst.server as server_module - - monkeypatch.setattr(server_module.sys, "platform", "linux") - called = False - - def _fail(*a, **k): - nonlocal called - called = True - raise AssertionError("must not spawn osascript off macOS") - - monkeypatch.setattr(server_module.subprocess, "run", _fail) - server_module._raise_editor_window_macos() # must not raise, must not call run - assert called is False - - -def test_watch_parent_exits_when_orphaned(monkeypatch): - # The sidecar self-reaps when its parent dies (getppid changes) so an - # ungraceful studio-server death doesn't leave a stale `serve` process. - import hyperframes_vst.server as server_module - - monkeypatch.setattr(server_module.time, "sleep", lambda *_: None) - # First poll: parent unchanged (still alive) → keep waiting. Second poll: - # ppid changed (orphaned) → must exit. - ppids = iter([1234, 1234, 1]) - - def fake_getppid(): - return next(ppids) - - monkeypatch.setattr(server_module.os, "getppid", fake_getppid) - - class _Exit(Exception): - pass - - def fake_exit(code): - raise _Exit(code) - - monkeypatch.setattr(server_module.os, "_exit", fake_exit) - with pytest.raises(_Exit) as exc: - server_module._watch_parent_and_exit(watch_pid=None, initial_ppid=1234, interval_sec=0) - assert exc.value.args[0] == 0 - - -def test_watch_parent_stays_alive_while_parent_lives(monkeypatch): - # Never exit while getppid keeps returning the original parent — guards - # against a watchdog that reaps a still-healthy sidecar. - import hyperframes_vst.server as server_module - - calls = {"n": 0} - - def fake_sleep(*_): - calls["n"] += 1 - if calls["n"] >= 5: - raise KeyboardInterrupt # break the loop after a few clean polls - - monkeypatch.setattr(server_module.time, "sleep", fake_sleep) - monkeypatch.setattr(server_module.os, "getppid", lambda: 4242) - - def fail_exit(_code): - raise AssertionError("must not exit while parent is alive") - - monkeypatch.setattr(server_module.os, "_exit", fail_exit) - with pytest.raises(KeyboardInterrupt): - server_module._watch_parent_and_exit(watch_pid=None, initial_ppid=4242, interval_sec=0) - - -def test_watch_parent_exits_when_watched_pid_dies(monkeypatch): - # With an explicit --parent-pid (the studio-server pid), the sidecar polls - # that pid via os.kill(pid, 0) — NOT getppid — because `uv run` sits - # between them and would otherwise mask the spawner's death. - import hyperframes_vst.server as server_module - - monkeypatch.setattr(server_module.time, "sleep", lambda *_: None) - # getppid stays constant (the uv wrapper is alive) — proving the exit is - # driven by the watched pid, not the ppid heuristic. - monkeypatch.setattr(server_module.os, "getppid", lambda: 999) - alive = iter([True, True, False]) - monkeypatch.setattr(server_module, "_process_alive", lambda _pid: next(alive)) - - class _Exit(Exception): - pass - - monkeypatch.setattr(server_module.os, "_exit", lambda code: (_ for _ in ()).throw(_Exit(code))) - with pytest.raises(_Exit) as exc: - server_module._watch_parent_and_exit(watch_pid=54321, initial_ppid=999, interval_sec=0) - assert exc.value.args[0] == 0 diff --git a/packages/vst-host/tests/test_stream.py b/packages/vst-host/tests/test_stream.py deleted file mode 100644 index b5d2a5ae24..0000000000 --- a/packages/vst-host/tests/test_stream.py +++ /dev/null @@ -1,57 +0,0 @@ -import json - -import numpy as np -import pytest -from pedalboard.io import AudioFile - -from hyperframes_vst.chain import build_chain, load_chain_spec -from hyperframes_vst.stream import TrackStream, decode_frame, encode_frame - - -@pytest.fixture -def dry_wav(tmp_path): - sr = 48000 - audio = (np.sin(np.linspace(0, 440 * 2 * np.pi, sr)) * 0.5).astype(np.float32) - stereo = np.stack([audio, audio]) - path = str(tmp_path / "dry.wav") - with AudioFile(path, "w", sr, 2) as f: - f.write(stereo) - return path - - -def gain_plugins(): - spec = load_chain_spec(json.dumps({ - "version": 1, - "plugins": [{"format": "builtin", "path": "Gain", "pluginName": None, "name": "Gain", "stateB64": None}], - })) - return build_chain(spec) - - -def test_frame_encode_decode_roundtrip(): - pcm = np.ones((2, 4), dtype=np.float32) * 0.5 - idx, pos, out = decode_frame(encode_frame(3, 12345, pcm)) - assert (idx, pos) == (3, 12345) - assert np.array_equal(out, pcm) - - -def test_stream_produces_sequential_blocks(dry_wav): - ts = TrackStream(0, dry_wav, gain_plugins()) - _, pos0, pcm0 = decode_frame(ts.next_block()) - _, pos1, _ = decode_frame(ts.next_block()) - assert pos0 == 0 - assert pos1 == 1024 - assert pcm0.shape[0] == 2 - - -def test_seek_jumps_sample_cursor(dry_wav): - ts = TrackStream(0, dry_wav, gain_plugins()) - ts.next_block() - ts.seek(0.5) - _, pos, _ = decode_frame(ts.next_block()) - assert pos == 24000 - - -def test_eof_returns_none(dry_wav): - ts = TrackStream(0, dry_wav, gain_plugins()) - ts.seek(10.0) - assert ts.next_block() is None diff --git a/packages/vst-host/uv.lock b/packages/vst-host/uv.lock deleted file mode 100644 index 01f8003f84..0000000000 --- a/packages/vst-host/uv.lock +++ /dev/null @@ -1,390 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.11" -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version < '3.12'", -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "hyperframes-vst-host" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pedalboard" }, - { name = "websockets" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pytest" }, - { name = "pytest-asyncio" }, -] - -[package.metadata] -requires-dist = [ - { name = "numpy", specifier = ">=1.26" }, - { name = "pedalboard", specifier = ">=0.9.24" }, - { name = "websockets", specifier = ">=12" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "pytest", specifier = ">=8" }, - { name = "pytest-asyncio", specifier = ">=0.23" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "numpy" -version = "2.4.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.12'", -] -sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, - { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, - { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, - { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, - { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, - { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, - { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, - { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, - { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, - { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, - { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, - { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, - { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, - { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, - { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, - { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, - { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, - { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, - { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, - { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, - { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, - { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, - { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, - { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, - { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, - { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, - { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, - { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, - { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, - { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, - { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, - { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, - { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, -] - -[[package]] -name = "numpy" -version = "2.5.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", -] -sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, - { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, - { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, - { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, - { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, - { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, - { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, - { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, - { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, - { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, - { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, - { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, - { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, - { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, - { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, - { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, - { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, - { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, - { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, - { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, - { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, - { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, - { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, - { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, -] - -[[package]] -name = "packaging" -version = "26.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, -] - -[[package]] -name = "pedalboard" -version = "0.9.24" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/86/d6c8d35595d0083e3627f6fd4ae87c14b3daecadfe538ca373f68682d376/pedalboard-0.9.24-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:fd675444a4bb278bb87e2b19eee728a8339961403a916f8de23ede73770db08b", size = 2637477, upload-time = "2026-07-08T19:38:47.534Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a4/ceb2f905cd5a02424147438d415273cc029c46abf246484b0094e759e1ef/pedalboard-0.9.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:302c923b5863cf122915f31c500c6788b2dfa0652cd57dac1b37aa37e842ddcc", size = 2443500, upload-time = "2026-07-08T19:38:48.906Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e2/bc7b43e4a8cfb4f30d681f4adb78cc6d1b814aac791a801d7c4e38dbcd8a/pedalboard-0.9.24-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddcbbd4cf0980a8f6c5a36ac4ada17731b8b5d95dec39eee6e017556ede64179", size = 4880807, upload-time = "2026-07-08T19:38:50.089Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a8/52bc8ae99558ce09d39d8a577d8a1fc72c19d9785f57273b0d7400c556b4/pedalboard-0.9.24-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0751d9b0592358f1afc070977cbf1150409e4ae27186fabfbf2dac4624e5026", size = 5081875, upload-time = "2026-07-08T19:38:51.435Z" }, - { url = "https://files.pythonhosted.org/packages/eb/43/29b48f230ec5a82dee260631e2529f2d94c7f087633e8246f679978835d8/pedalboard-0.9.24-cp311-cp311-win_amd64.whl", hash = "sha256:431b24eee52e84c1d701081379a003d44b63606ca5e746547e9bd53181ca55f2", size = 3679755, upload-time = "2026-07-08T19:38:52.992Z" }, - { url = "https://files.pythonhosted.org/packages/e1/38/cb7e2f5c82a17728ced8fc90ec01f408385af9123a8a506368613ea37928/pedalboard-0.9.24-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:230f31a6de76ab1653ecf5bcd2f76271d056c6fe0e02991b722bf84b60a652d5", size = 4749034, upload-time = "2026-07-08T19:38:54.291Z" }, - { url = "https://files.pythonhosted.org/packages/6f/81/0c7a797aae8676f0a03ca19ddcab24d2e5073260a13369d311835316621a/pedalboard-0.9.24-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35d58d6022532e34d595ca14183507eb26883cceeeea2dcbd9f696874c13089f", size = 2643461, upload-time = "2026-07-08T19:38:55.553Z" }, - { url = "https://files.pythonhosted.org/packages/37/03/397ff2c3fca9b8fd53f8a1e74fbbe64a32961f4863f627b6d8ff534f39aa/pedalboard-0.9.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7dadf85c44e9d3f2571686efadc8d060f56cb2f032f7847a188bd345f623a6d", size = 2445946, upload-time = "2026-07-08T19:38:56.99Z" }, - { url = "https://files.pythonhosted.org/packages/f9/df/8e9028b66feeb9b1b2584ceb9c0d6dd438f4a69b1c5cba9a5068ca4c87b7/pedalboard-0.9.24-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f9e14023f1df3d9a6a15ef9168af4092e0a9e4e136fa87bf9db01bf8a781c56", size = 4878436, upload-time = "2026-07-08T19:38:58.524Z" }, - { url = "https://files.pythonhosted.org/packages/d9/db/ed766261fe0a1251aa15f2ad9efa50f9daaa6d42f40da64341be14f8b8da/pedalboard-0.9.24-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:954fdea21d4c32e18856f57084a235e1213140d5b72afadf714b8d10787d5752", size = 5078294, upload-time = "2026-07-08T19:38:59.898Z" }, - { url = "https://files.pythonhosted.org/packages/2e/72/3c0833ca2f98d17209eeee986f36d85e2a01a31ddaefb0f12d387c00efba/pedalboard-0.9.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a3e2397e1b50636751cb64afb41a5c5ca1dd4a0aafc0ad83e59160d7fad9c38", size = 5909043, upload-time = "2026-07-08T19:39:01.256Z" }, - { url = "https://files.pythonhosted.org/packages/09/01/b3182790706eb2452ed1d83298ca46fab6cfe325e97b23330540f84856fb/pedalboard-0.9.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6fc0b3e551a6d72cac02b465bee8fc933eb80408760d20f7cd5bbba081b8072", size = 6100095, upload-time = "2026-07-08T19:39:02.557Z" }, - { url = "https://files.pythonhosted.org/packages/39/47/90df0a0f77ee7698ef72d1f0a3259bca34bb5b8dd29fdb95159fbe10bb58/pedalboard-0.9.24-cp312-cp312-win_amd64.whl", hash = "sha256:27ba467a7e0b5b6b8b060c34532ab629732343860d6a2f40af41543ce981b8f0", size = 3681319, upload-time = "2026-07-08T19:39:04.117Z" }, - { url = "https://files.pythonhosted.org/packages/e8/b4/c89f0ac371c8d38216216aa58276c5f6f0493c8a4aa2fcbac2c6adbd255e/pedalboard-0.9.24-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:beb62195e706bb0418de67d9bc93eed6f9c1c7597faf435ca2b747742d390b96", size = 2643324, upload-time = "2026-07-08T19:39:05.584Z" }, - { url = "https://files.pythonhosted.org/packages/37/55/3f1b376066ef147688236f6b01e6c96d6d4dd6855e5eac9e49ba6dc8f863/pedalboard-0.9.24-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50315bd080692daced31df627ac29001f535835a65ff465945f9734f5850c33b", size = 2445574, upload-time = "2026-07-08T19:39:06.855Z" }, - { url = "https://files.pythonhosted.org/packages/85/93/89066dca216ba49c8e3fa3408d9f82e97dff986c8c76a7298fefe6a08559/pedalboard-0.9.24-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fbc5dc0db0767f540010af67d5bf06304d29c1c3847aeb955fd6ef86c33183a", size = 4878382, upload-time = "2026-07-08T19:39:08.03Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7c/8f5f1adffe3fa4d206a6af42c668cacf0df1d581a6628856a18544468812/pedalboard-0.9.24-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50279ed07785e55536b30758d547cf15bdfbe20d1bdf6a67060ecd70a08aee48", size = 5089448, upload-time = "2026-07-08T19:39:09.279Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a6/c1cf2df7248d8fac6a42f22a76958d816073d353feb6592f02ed7eea0f39/pedalboard-0.9.24-cp313-cp313-win_amd64.whl", hash = "sha256:52f2520227b35cc4f998f9a931315eb7644cb4fbba1c597f2a19f58d54f387ee", size = 3681579, upload-time = "2026-07-08T19:39:10.647Z" }, - { url = "https://files.pythonhosted.org/packages/18/f1/cfef0922fdfc0382747f96d93469b7d7cfe60e5dcc799fc4d8d4efd2e61d/pedalboard-0.9.24-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d904c1707d56d6cad69e4458dd06032bfa2e29d74e1a241339d1761cc08ac5d2", size = 2446691, upload-time = "2026-07-08T19:39:11.971Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f3/462d1b523bc391bf4935ce512a2ee8bcc7cf1437c58ee733989b4cf3821b/pedalboard-0.9.24-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365983ca61e08255c5251d90ea4cb69591f56ff87df03580fd37c237065c89b4", size = 4879329, upload-time = "2026-07-08T19:39:13.452Z" }, - { url = "https://files.pythonhosted.org/packages/61/e5/71b5b09f900f843e2a11a1d52cb4bf39d014bb58fc17ff016af9c19a181a/pedalboard-0.9.24-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6fa220f1394fd83fee92b4ec761365d83bcb737cdf10aee774dec212b6fda02", size = 5062453, upload-time = "2026-07-08T19:39:15.284Z" }, - { url = "https://files.pythonhosted.org/packages/53/56/7c59c8b1bdb3143dcd1275f2f67a46e6a49a261ac1ec9fb27c49da345c93/pedalboard-0.9.24-cp314-cp314-win_amd64.whl", hash = "sha256:1c5a223de21eddd354087b262429d940b5ce65154b8ed0b6789eb5235abec11c", size = 3797208, upload-time = "2026-07-08T19:39:16.776Z" }, - { url = "https://files.pythonhosted.org/packages/bc/22/371a75bf5175441ad1d2c4d4b779f97dcb74ed6b4bfeb9a1331c3af64fd2/pedalboard-0.9.24-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:295533f0aa37f12c9bc2be4986adb28cc5cf0faf6ed2d88c583bccddebccdc8f", size = 2473794, upload-time = "2026-07-08T19:39:18.186Z" }, - { url = "https://files.pythonhosted.org/packages/a4/80/278d8f5e2c894eef1849fcb30ea7e5915129ce4a6056379cbd1dacba3221/pedalboard-0.9.24-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d297df87cfb995e47def363f4b1a8870bd72329c57cca9faa64421f2f9e7ad18", size = 4879178, upload-time = "2026-07-08T19:39:19.423Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, -] - -[[package]] -name = "pytest" -version = "9.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, -] - -[[package]] -name = "pytest-asyncio" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, -] - -[[package]] -name = "websockets" -version = "16.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/02/b9a097e1e16fee4e2fd1ec8c39f6a9c5d6257bae8fa12640caf869f54436/websockets-16.1.tar.gz", hash = "sha256:299468cbe42e2b9981134c7c51d99387d8a7bf562b00183b3eec53f882846dad", size = 182530, upload-time = "2026-07-10T06:32:57.734Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/13/d47429afcc2c28616c32640009c84ea3f95660dab805766345b9682468e0/websockets-16.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a9b1d7a63cba8e6b9b77e499a81eab29d31100298d090ad4507d1048c0b9cae0", size = 179770, upload-time = "2026-07-10T06:30:46.308Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c7/2f0a722039a1e0107be73ed672ba604449b4956e48733e8e6b8a005aea42/websockets-16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bedbc5efeb96621aa2921d2d92608246691399418cac22acba427eb11877ea1f", size = 177455, upload-time = "2026-07-10T06:30:47.601Z" }, - { url = "https://files.pythonhosted.org/packages/43/6a/c26b0ae449e93d256ce5cdd50d5fe97b575a63e8dcd311a1faa972fd6bc6/websockets-16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd847ab82133015afe65d778e7966ab42dba16bd7ad2e5b8a7918db6539f3f94", size = 177731, upload-time = "2026-07-10T06:30:49.102Z" }, - { url = "https://files.pythonhosted.org/packages/cc/3f/381550b344a02f0d2f84cda25e79b54575291bc7022128a41163fe8ba5b0/websockets-16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2fb33ccb16ee40a95cc676d7b0ff451a9a2632f11a0dbc2e666326892b2e1de", size = 187066, upload-time = "2026-07-10T06:30:50.505Z" }, - { url = "https://files.pythonhosted.org/packages/4a/87/5ab1ec2086910f23cfb9ec0c1c29fbcc24a9d190b5198b1557c00ce4a47e/websockets-16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f15b6d9ea9c2eaf6ccab964a082b09bfa6634a495bb0c2e9e7ee6943f58976", size = 188301, upload-time = "2026-07-10T06:30:51.835Z" }, - { url = "https://files.pythonhosted.org/packages/75/4b/bbbb8e6fac4cfc53d7aaa69a3d531bf10799354b0021f4b58914aced8c1a/websockets-16.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:638cf57c48b4ad8ac1ff1e453f4f97db2426b690ddc111e6da96b27b4a340bc3", size = 191594, upload-time = "2026-07-10T06:30:53.229Z" }, - { url = "https://files.pythonhosted.org/packages/5c/da/6c0c349443d6e999f481e3d9a0e57e7ac2956d75d6391bec24b92af3fe13/websockets-16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c1c85f61bc9d5eac57ce705d848dc2d2ce3680638300bf4e1da7d749e2cf4ce", size = 188862, upload-time = "2026-07-10T06:30:54.744Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ea/a368d37c010425a5451f42052fe804e754e23333e8448aef5d55c8a8d64f/websockets-16.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:eeab6d27f51c7e579023c971f5e6dff200deadf01faf6831beaecd32052dfaef", size = 187633, upload-time = "2026-07-10T06:30:56.055Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4e/2ecd59add10d0855ec03dbdedfcdacdbd1aaabcd44b7dcbeda27538662e9/websockets-16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ed64e5a97b0b97a0b66e18bfe281317a75fbbd5afe692f939ea8d14a4292f2c", size = 185089, upload-time = "2026-07-10T06:30:57.444Z" }, - { url = "https://files.pythonhosted.org/packages/6f/eb/c6c3dcd7a01097bb0d42f4e9ef21a2c2a491d36b77cd0870ab59f9e8e77f/websockets-16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b3b021d0ed4bc16eea9775f62c9fa71acdacba0fc790b38581754dedf29ca60", size = 187790, upload-time = "2026-07-10T06:30:58.731Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3e/775d36885d5e48ab8020aaf377de0ff5fbeb8bc2682a7e46419e4a14521c/websockets-16.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6eb604a4167f0a0d53c2243dfc667a29f0b43c3436057184e070bb82a1000fa2", size = 186381, upload-time = "2026-07-10T06:31:00.355Z" }, - { url = "https://files.pythonhosted.org/packages/ad/90/6305c00812a92e47d0582604c02bd759db0118bbafc13f707d712dbcf898/websockets-16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9a3f125e44c3e34d61d111652e608e0f5b85ce08c225c8d56ad0eb822fa40030", size = 188193, upload-time = "2026-07-10T06:31:01.677Z" }, - { url = "https://files.pythonhosted.org/packages/f6/32/96bf8302c81d961585b4d34a2ddd3f229782f9b8c57bc78bbf98f1b1a4ac/websockets-16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8fdf0b00d0d1f30d1f06a92cab46fe542eec3eb302a7aee7163f142d0780f216", size = 185771, upload-time = "2026-07-10T06:31:03.062Z" }, - { url = "https://files.pythonhosted.org/packages/e8/1f/e8fe44b1d2dc417d740d9959d28fd2a846f268e7df38a686c04ac7dfe947/websockets-16.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67b56828712f5fa7852de4c0265c28827311a657a4d275b7312ed0d1a918bee4", size = 186803, upload-time = "2026-07-10T06:31:04.34Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/b07d3a4e1eb2ab03e94e7f53f0c7a628e85fde6ad86011f7afd08f27b985/websockets-16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39c7e7730be33b8f0cd6f0aa8e8c82f9cdd1813f159765e073b2ece65f4824b5", size = 187041, upload-time = "2026-07-10T06:31:05.567Z" }, - { url = "https://files.pythonhosted.org/packages/a6/fd/e0abb8acc435642ac4a671490f6cf781c882f3fe682cdced9080ea455ab5/websockets-16.1-cp311-cp311-win32.whl", hash = "sha256:c54fe94fb2f11e11b48920c5f971e298cec73ac35db56efe57a49db63dfc95d4", size = 180158, upload-time = "2026-07-10T06:31:06.929Z" }, - { url = "https://files.pythonhosted.org/packages/81/06/85574d9458d3b913090087b817df0cc47b68e9a01dd0ab6ac04b77f49b0a/websockets-16.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9f4fb9ae8b802e55609685db98382d48fd3feb1397804e1e774968dea0f28c7", size = 180456, upload-time = "2026-07-10T06:31:08.247Z" }, - { url = "https://files.pythonhosted.org/packages/a1/52/748c014f07f4e0e170c8932de7e647a1511d5ab3049cd978797136aee577/websockets-16.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b6aa3f7ad345cf3862c21f4fbf2ef5e14d911348476c2845e137c091fe3a3f0b", size = 179798, upload-time = "2026-07-10T06:31:09.664Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5e/2a2e64d977d084e49d37c187c26c056daaff41965be7300cd5dbde6f8b07/websockets-16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b43fcfb521ac2f34ba80b7b8ea16303e4ad82dd8af667bf40839ad3a5d37b164", size = 177478, upload-time = "2026-07-10T06:31:11.072Z" }, - { url = "https://files.pythonhosted.org/packages/aa/12/5b85b4e75d697e548a94962ce5c036b05dd21cb9545759d555c5586422fc/websockets-16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bd3e12cd9afbe2baedae0b1eeade8ba64329b60fe2f9abdc966bd10fd2c2ef5", size = 177746, upload-time = "2026-07-10T06:31:12.386Z" }, - { url = "https://files.pythonhosted.org/packages/9d/62/79b1c8f0cee0da648b4899e1c5b0dbd3aa59846985136a54854db6827ab4/websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a", size = 187345, upload-time = "2026-07-10T06:31:13.754Z" }, - { url = "https://files.pythonhosted.org/packages/25/34/b7c5c52c2f24280e1c017acb7ad491a566750a5cceca7f3cf999373bba21/websockets-16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a24d1f35aef07d794a16c853c688e74956c50239bec37b4f2de080056046419b", size = 188581, upload-time = "2026-07-10T06:31:15.075Z" }, - { url = "https://files.pythonhosted.org/packages/bc/37/604193bebcbeffe96fdf795960b83a15d600880c64dc17ec9c31c5b3427d/websockets-16.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c64c024ddf7a35331b21fcddb562a039c275d2c82e8c2d12939e7da23997270", size = 191362, upload-time = "2026-07-10T06:31:16.395Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b4/5ee27575b367d7110d4d13945e2a9de067ec84dc71e54b87f01e38550d9a/websockets-16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e99757f5baafe20fc598e202ea6f5b0b265186ad38d0a17bd8beca16296955", size = 189216, upload-time = "2026-07-10T06:31:17.776Z" }, - { url = "https://files.pythonhosted.org/packages/7e/22/3e2dcc78d85fc5d9d814895ce6d07d0dfacc0f6aaa1d151f2b8c8d772299/websockets-16.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:353f3bc6e058ac1ccab4b3588e8598837a8c04cfc8351233e6d523be675d844c", size = 187971, upload-time = "2026-07-10T06:31:19.152Z" }, - { url = "https://files.pythonhosted.org/packages/9e/2f/cd271717b93d5ee19626cb5e38a85baab745c86e33db7c31a3ac729b31b8/websockets-16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0352f5b38b40e857b6428d468fa21dbb4dd4a567d933c26d9831b4efe1b92f43", size = 185381, upload-time = "2026-07-10T06:31:20.665Z" }, - { url = "https://files.pythonhosted.org/packages/78/91/6ad6f2f1426317b5001bd490534208c7360636b35bac1dec2e0c22bfc40e/websockets-16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70bd789afab579602968c39f21cb925466505f3edff22f0ae852bca54978a4f9", size = 188015, upload-time = "2026-07-10T06:31:22.024Z" }, - { url = "https://files.pythonhosted.org/packages/c7/6d/533733132ab4c07540efd4a8f0b9a435d3a5059b2f26cc476ace1abf7f45/websockets-16.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d0fb4b46f121eccd539353baebd1083a8767a9a351109453d1d1caecd1ba40c2", size = 186619, upload-time = "2026-07-10T06:31:23.376Z" }, - { url = "https://files.pythonhosted.org/packages/08/73/16c059f3d73b3331eba10793704afa4faa9939234fb08ef7dca35794e8f0/websockets-16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c14b6634af01541e4efe2954fd8f263386f7aa6d37c01e55dd8109fd17661452", size = 188497, upload-time = "2026-07-10T06:31:25.024Z" }, - { url = "https://files.pythonhosted.org/packages/4d/89/9a8fae7dd2acdcfb1a8844c29fe42b518a04b64fce38a0923b6290e452f1/websockets-16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a58532c49a851bcb481e58c1be23b315c17fe2fbbed509d75aeea12f543d2c15", size = 186051, upload-time = "2026-07-10T06:31:26.291Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/b240c7dd6a0e0c59c1f68377cc3015263521080c327c15f5e753c1f6d378/websockets-16.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4e969170c3b08e1d8dabd990fef1fa702c4233aeaabec33f871806e444f6a0e4", size = 187029, upload-time = "2026-07-10T06:31:27.605Z" }, - { url = "https://files.pythonhosted.org/packages/50/35/524e3fac40e47d6fdcf6c4b2c95ef1bc8a97e01593c90eff86621df7b716/websockets-16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff9b000064b88787ba9f7a3cb2af2b68a658ca5aad76458a46469e7124b678a0", size = 187308, upload-time = "2026-07-10T06:31:28.927Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/56840cf62c8859af6ba22b9529da937332468c80f32b598753e8a66d3990/websockets-16.1-cp312-cp312-win32.whl", hash = "sha256:b9f5d83f80f4d7c4bba6d97f3755ac05850c784dce0fd2ab371c4e41172f53ff", size = 180161, upload-time = "2026-07-10T06:31:30.316Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ff/87eb9eb44cb62424a8d729834f2b0515a47e2669fabec29820268f4d50a1/websockets-16.1-cp312-cp312-win_amd64.whl", hash = "sha256:6852c9f653966c16109d3b6f31181fd734f7914927e3f0fa1117af7a18c9aa21", size = 180462, upload-time = "2026-07-10T06:31:31.708Z" }, - { url = "https://files.pythonhosted.org/packages/d9/63/df158b155420b566f025e75613424ad9649a24bcb0e9f259321ab3d58bea/websockets-16.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b0232ed141cec3df2af5a3959a071c51f40036336b0d37e17faf9ef52fc73e47", size = 179791, upload-time = "2026-07-10T06:31:33.108Z" }, - { url = "https://files.pythonhosted.org/packages/74/cf/00fe9414dfeafa6fe54eae9f5716c8c8e9ac59d192be3b893c096d395846/websockets-16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a71b73d143991714144e159f767b698f03c4a70b8a65ae1733b650cff488045b", size = 177472, upload-time = "2026-07-10T06:31:34.522Z" }, - { url = "https://files.pythonhosted.org/packages/8b/76/b10633424d40681b4e892ffd08ca5226322b2426e62d4ab71eae484c3a32/websockets-16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:187323204c3b2fc465e8fc2609e60437c521790cb9c1acb49c4c452a33e57f37", size = 177737, upload-time = "2026-07-10T06:31:35.964Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/d3bb03b2229bb1afd72008742d586cf1ea240dce64dd48c71c8c7fd3294c/websockets-16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dba74233c8c3ce368850818c98354dad2570f57231b3fd3bd00d7aa57628881", size = 187403, upload-time = "2026-07-10T06:31:37.496Z" }, - { url = "https://files.pythonhosted.org/packages/26/16/cc2e80478f688fc3c39c67dc1fac6a0783858058914ebc2489917462cb42/websockets-16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63339bc8c63c86a463177775cb7c677691f5bcfac7b3b2f01b286d42acd41600", size = 188639, upload-time = "2026-07-10T06:31:38.86Z" }, - { url = "https://files.pythonhosted.org/packages/15/d6/ad87b2507e57de1cbf897a56c963f2925962ed5e85fbe06aaa83ced27acd/websockets-16.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23e545ea8ae4263e37cdfd4e22a217f519e48e432728bc461185bbf585f38a83", size = 190078, upload-time = "2026-07-10T06:31:40.218Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1a/5b37b3fd335d5811f29fc829f2646a3e6d1463a4bf09c3100708684c766e/websockets-16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2237081454846fb40403a80ba86d82e2038b9c45865ab96af0abe7d002a91045", size = 189267, upload-time = "2026-07-10T06:31:41.523Z" }, - { url = "https://files.pythonhosted.org/packages/42/98/06afc33e9450d4230f94c664db78875d90f5f6a5fb77f0bc6ec15ae74e1c/websockets-16.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f5218de1ed047385ca53744caba9435d65f75d008364970a3fae95a05812cf9", size = 188022, upload-time = "2026-07-10T06:31:42.838Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/42fef5d5887c18cf2d148b02debf56cecb9cfbffc68027cde9b12c8f432c/websockets-16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75c98e3920039d0edff03b74478ada504b7ce3a1bc406db2cabfca84320f7baf", size = 185435, upload-time = "2026-07-10T06:31:44.219Z" }, - { url = "https://files.pythonhosted.org/packages/a0/9b/8021c133add5fe40ed40312553a6cd1408c069d7efe3444ad483d4973ed3/websockets-16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1facd189d8190af30487a55b4c3688484dd50801628a3b5b2ccd26db08e67057", size = 188080, upload-time = "2026-07-10T06:31:45.986Z" }, - { url = "https://files.pythonhosted.org/packages/69/54/1e37384f395eaa127383aab15c1c45e200890a7d7b99db5c312233d193e0/websockets-16.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cc0c6a6eef613c7da32d4fb068f82ef834b58134f6a16b54e6c1e5bf9529ab3d", size = 186678, upload-time = "2026-07-10T06:31:47.449Z" }, - { url = "https://files.pythonhosted.org/packages/68/79/1caeacab5bc2081e4519288d248bc8bd2de30652e6eaa94be6be09a1fe5b/websockets-16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ad9411eded8988b879be6038206698bf7106c85a78f642c004485bcb95be17eb", size = 188554, upload-time = "2026-07-10T06:31:48.886Z" }, - { url = "https://files.pythonhosted.org/packages/ee/83/b3dca5fad71487b726e31cb0acf56f226792c1cc34e6ab18cbf146bd2d74/websockets-16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cd68f0914f3b64694895bc5e9b14e8b447e41d7bf5ffaf989bb8dcb5e2dfdce7", size = 186109, upload-time = "2026-07-10T06:31:50.508Z" }, - { url = "https://files.pythonhosted.org/packages/5b/0b/8f246c3712f07f207b52ea5fb47f3b2b66fafec7303162644c74aed51c6a/websockets-16.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fef2debfe7f7ebdda12176f26166f95b7af17af05ba06150fcf889032e0213e9", size = 187061, upload-time = "2026-07-10T06:31:51.861Z" }, - { url = "https://files.pythonhosted.org/packages/47/eb/27d6c92a01696b6495386af4fc941d7d0a13f2eab2bf9c336111d7321491/websockets-16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3cd6c9b798218798f4bb7b2e71c38f0e744bb94ca537b13376f88019d46384d", size = 187347, upload-time = "2026-07-10T06:31:53.246Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d5/eeee439921f55d5eaeabcea18d0f7ce32cdc39cb8fc1e185431a094c5c7b/websockets-16.1-cp313-cp313-win32.whl", hash = "sha256:84c170c6869633536921e4474b1cce7254c0c9b0053ef5725f966cee47e718e4", size = 180149, upload-time = "2026-07-10T06:31:55.058Z" }, - { url = "https://files.pythonhosted.org/packages/a3/03/971e98d4a4864cf263f9e94c5b2b7c9a9b7682d77bfbba4e732c55ee85a9/websockets-16.1-cp313-cp313-win_amd64.whl", hash = "sha256:bef52d327d70fa75dad93ee61ea2cb1d1489aca9f35c188833563f5a3b4df0a5", size = 180458, upload-time = "2026-07-10T06:31:56.767Z" }, - { url = "https://files.pythonhosted.org/packages/8d/e6/da1dc11507f8118145a81c751fe0c77e5e1c11b8554496addb39389e2dc2/websockets-16.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f881fca0a45dd6789939bd6637cd98169b92f1c3fdc78262f2cb9ec2cb1f324e", size = 179833, upload-time = "2026-07-10T06:31:58.19Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ac/c0d46f62e31e232487b2c123bc3cfd9a4e45684ca7dc0c37f0987f29baae/websockets-16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c379d5b207d3a7f0ba4c2e4602a895b0bcc63fb5f5371a4ae7fbddb03b672b", size = 177524, upload-time = "2026-07-10T06:31:59.563Z" }, - { url = "https://files.pythonhosted.org/packages/4a/33/abd966074b34a51e4f134e0aaed80f5a4a0a35163ea5ac58a1bc5a076d23/websockets-16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98ab58a4faa72b46da0127ccc1931dcbfc0985b0778892300a092185910c4cbe", size = 177743, upload-time = "2026-07-10T06:32:00.959Z" }, - { url = "https://files.pythonhosted.org/packages/ea/30/646e47b8a8dff04e227bdab512e6dde60663a647eeac7bbd6edddd92bbc5/websockets-16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e9c4e369fc181b2d41a99e01477215cecdc8546a39f7d41a59cc0a7065a0b09", size = 187474, upload-time = "2026-07-10T06:32:02.54Z" }, - { url = "https://files.pythonhosted.org/packages/d2/72/890ab9d77494af93ea65268230bfbc0a90ba789401ed7a44356a44785644/websockets-16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0704df094b2d5fa7f6f410925a594c2a5c9a09167731a76292e5410934208209", size = 188717, upload-time = "2026-07-10T06:32:04.156Z" }, - { url = "https://files.pythonhosted.org/packages/d5/aa/baedbbaa6bf9ed6029617ed5e8976535bd805f483ca9b3484e7ad9ee08bf/websockets-16.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b22b1f4950f6ab7126623329c3b47b3b90a14c05db517f2db2a026ad6c928352", size = 190090, upload-time = "2026-07-10T06:32:05.822Z" }, - { url = "https://files.pythonhosted.org/packages/52/4f/d813ec94e18002571ef4959d87a630eff6e01b72a51bcb0832b75ae8c51a/websockets-16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1ae4a686a662964a6671069f84f7f908cc3475e782227726b0c622c715962105", size = 189320, upload-time = "2026-07-10T06:32:07.223Z" }, - { url = "https://files.pythonhosted.org/packages/b8/3c/8ec52a6662f3df64090fba28cd521d405d54759268d8e820477037e8c80d/websockets-16.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:856bdd638f8277f86465057bfdd4da097c73058fb0f9d2bd5baea29e2bf2d367", size = 188068, upload-time = "2026-07-10T06:32:08.586Z" }, - { url = "https://files.pythonhosted.org/packages/96/7f/f0ae6042b14f86fa5f996c6563ea4cf107adc036ccbedc9d4f418d0095f9/websockets-16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9003a1fde1c21a322a3ca3fa0c4bda8c639da81dbc925162766086643b05ba87", size = 185493, upload-time = "2026-07-10T06:32:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/89/ad/5ffc53af9939c49fd653d147fa5b8f78ced1f6bce6c49a7446860945b0ce/websockets-16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39e947b1f5fdab045174306e3916785bf3ed537648acc1549827c08c33b10953", size = 188141, upload-time = "2026-07-10T06:32:11.434Z" }, - { url = "https://files.pythonhosted.org/packages/67/62/729206c0ee577a4db8eae6dd06e0eef725a1287c6df11b2ef831d003df31/websockets-16.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5dd0e666b5931c0509cf65714686a1c5126771e663a79ac5d40da4f58b1f9502", size = 186653, upload-time = "2026-07-10T06:32:12.845Z" }, - { url = "https://files.pythonhosted.org/packages/1b/86/e8806a99ec4589914f255e6b658853fe537bf359c05e6ba5762ad9c27917/websockets-16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a0285df7925657ad65a65fb8dc330808bce082827538fd50ef45fa12d1fc5bca", size = 188614, upload-time = "2026-07-10T06:32:14.236Z" }, - { url = "https://files.pythonhosted.org/packages/89/38/ac554e2fc6ff0b8deeff9798b92e7abd8f99e2bd9731532e7033de208220/websockets-16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:82d1c2cab3c133e9d059b3a5420bed9376bd30e21c185c63dda4ddadf6ddda47", size = 186165, upload-time = "2026-07-10T06:32:15.626Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c5/4ef4d8e53342f94f3c49e1ae089b32c1e8b3878e15e0022c7708c647f351/websockets-16.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c39907f1eaf11f6277def65aa02d68f30576b693d0c1ca332aafa3caa723ac6d", size = 187119, upload-time = "2026-07-10T06:32:17.114Z" }, - { url = "https://files.pythonhosted.org/packages/3a/33/4788b1dd417bd97eeb2698af3b9df6775ac656f96e9987da0419a067602f/websockets-16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:45c5ea55446171949eb99fd34b771ceddd511ca21958d40d0197ced33159e5ee", size = 187411, upload-time = "2026-07-10T06:32:18.629Z" }, - { url = "https://files.pythonhosted.org/packages/30/38/00d37aad6dc3244ce349e2864815362e50b3cfc00cac28d216db20efe40f/websockets-16.1-cp314-cp314-win32.whl", hash = "sha256:b8ef8b1c8d6bd029a475ac432e730fba2dfd456715d26c473e2a82291024b99c", size = 179822, upload-time = "2026-07-10T06:32:20.233Z" }, - { url = "https://files.pythonhosted.org/packages/9d/37/2a8cb0eaddee5eaebda47a90a3ba0898d1ce3d866b02a4857fea17d82e5b/websockets-16.1-cp314-cp314-win_amd64.whl", hash = "sha256:7358ff21632b5d062707f73e859c824f1c3807e73d8ca25e71caca7c4cdcf145", size = 180167, upload-time = "2026-07-10T06:32:21.749Z" }, - { url = "https://files.pythonhosted.org/packages/07/5a/262ad5fcaef4198997b165060f09a63f861e76939b1786ab546ccc3f8120/websockets-16.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d0f38f4c3e9b359e257c339c2cc1967ccaeedb102e57c1c986bdce4bf4f32268", size = 180166, upload-time = "2026-07-10T06:32:23.278Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c7/36377db690f4292826e4501a6dec2801dc55fd1cf0405923b04937e478df/websockets-16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3c3d2cbd1602593bad49bd86fa3fbb25407d87a3b4bf8857c0ac5ac4914e1901", size = 177697, upload-time = "2026-07-10T06:32:25.164Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c7/07171abce1e39799a76f473608580fe98bd43a1230f5146159622c02bccf/websockets-16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:36069b74671e7e667f48a7484249f84c45a825a134c8b1bdc01875d0daa10d79", size = 177902, upload-time = "2026-07-10T06:32:26.564Z" }, - { url = "https://files.pythonhosted.org/packages/14/17/c831f48e250bc4749f57c00dcce73337c41cd32f6d59a64567b84e782601/websockets-16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:587f83c2ce8a5d628e166384d77fa7f0ac69b9007d515ab442123e6615aa8da3", size = 187766, upload-time = "2026-07-10T06:32:27.981Z" }, - { url = "https://files.pythonhosted.org/packages/2c/2e/4dfe63e245b0ecfaf470cf082d25c6ce35808159135fd88c82653a6b11ab/websockets-16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6db7972d52bc1b66cefe2246902e256cbaebc9ba8a45eac09343d7eb6671b2", size = 188939, upload-time = "2026-07-10T06:32:29.365Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e5/5faf65aebd9562f6b4bc473d24ce38cc56f84eb5f5bee66ed9b86733f93c/websockets-16.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e7d6014888a0632e1ed7a4095248bb3095232999447f2d83bfb1900987dd9ed9", size = 191081, upload-time = "2026-07-10T06:32:30.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/cd/2634f2f2c0556c1aae6501ed6840019cc569dd6fdbcac6494378daea4dc0/websockets-16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cb074d150e4ad2a77aa8a332c2be85f3f64f2681519d2570c1225c12c9821ff", size = 189513, upload-time = "2026-07-10T06:32:32.399Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/2c700b51196104f09715b326b1f092ed25326bdf79a03e00a4842e503743/websockets-16.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d19c9067e1fe9490f974bffbc0e443b80a7674c5efb4980c429cc00771f07c5a", size = 188240, upload-time = "2026-07-10T06:32:33.897Z" }, - { url = "https://files.pythonhosted.org/packages/f1/20/86283636e499a1a357fa9441f690ba34f255e731f2fea174132b3b762b57/websockets-16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d440ff0c6c7469ad59c0a412c383c235935b43635e89425e3f6a0c36de90c31b", size = 185955, upload-time = "2026-07-10T06:32:35.279Z" }, - { url = "https://files.pythonhosted.org/packages/91/23/d7fb734b0095d43bc7f1c9f68afd50adb4176e7e513403e8c70ad7daa4fa/websockets-16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8613129a2533f08de24505e69a3e403cedaadae49abdb043c4d170ca71b7e4bd", size = 188491, upload-time = "2026-07-10T06:32:36.673Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5e/168a192689db468405ecf3b8e4a2c18811936b0724d017ad7e6d252734f0/websockets-16.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a5bf9c23f197b4ec88290fd5463f33db67362a1bb10f85fc2e8e7627f0ddab97", size = 186983, upload-time = "2026-07-10T06:32:38.207Z" }, - { url = "https://files.pythonhosted.org/packages/7e/9b/66795fa91ebe49019ebe4fa910282172252e37046b80e08fc52e0c365150/websockets-16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:520b0fd0395f075febb283c76755af724ab9fd19dffa4f3bfd18cb4e622790a3", size = 188890, upload-time = "2026-07-10T06:32:39.545Z" }, - { url = "https://files.pythonhosted.org/packages/5a/32/126bbc844be5afb3613fd43211dac10a9645f4cf39741d04acaa2ec7030c/websockets-16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7143aa09a67e1c013be44e81a88dfe90fc6244198ab86c7edd064152cf619805", size = 186583, upload-time = "2026-07-10T06:32:41.038Z" }, - { url = "https://files.pythonhosted.org/packages/22/b9/0b5db9cbcf6e4970db4496893244a8d92e07f71a8ef27cf34b08aa02fef1/websockets-16.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:7acb811fad08e611755800d1560e395c67e11a6bd563598ea6abb319afb86938", size = 187353, upload-time = "2026-07-10T06:32:42.501Z" }, - { url = "https://files.pythonhosted.org/packages/99/2e/254b2131a10d831b76e2c18dfe7add9729c6292c674a8085bf8de01ad151/websockets-16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c5cf88e3faa2f7931bc6baeee7599c97656a3f6ac7f831f4fccba233e141783a", size = 187784, upload-time = "2026-07-10T06:32:43.929Z" }, - { url = "https://files.pythonhosted.org/packages/21/dc/e7288aa8e3ac5a88a0924619984d663c1abf2a87d0ea98290c66fdaee0ec/websockets-16.1-cp314-cp314t-win32.whl", hash = "sha256:589f8842521c8307684ce0b40ce4ad70c5e0aa46484c6f1225a94ef4b8970341", size = 179947, upload-time = "2026-07-10T06:32:45.495Z" }, - { url = "https://files.pythonhosted.org/packages/d3/de/37edf1260ff0fbbd2f82433489c4cfbe799ac2ff21355331609879329fe6/websockets-16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c0e0857c30bbbc2bb5c30687508f0b7ec19aa026cd9f2ff8424d0fee42dcc07", size = 180291, upload-time = "2026-07-10T06:32:47.119Z" }, - { url = "https://files.pythonhosted.org/packages/4d/f4/84ef884775bbe77c46cce79bc7d705ea3bc6574cc00acf81af89754c077d/websockets-16.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7289d899c79e763e6221c8dcb8959361cb43274418538d7c7ad16a43b01d12f9", size = 177387, upload-time = "2026-07-10T06:32:48.574Z" }, - { url = "https://files.pythonhosted.org/packages/d3/d9/6831ec6f65e1eeac770375f4f4b604f23df9bafaa1b47004bc5f9488d513/websockets-16.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e22e9e3719f5131bd62da4db63c8da63eb8c91cc99e16c1cbd122f130e1ae07a", size = 177663, upload-time = "2026-07-10T06:32:50.043Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d4/21d4922fa7fe855813a8b38f181a0ecf02a586e16c1f095fd05471f78cc2/websockets-16.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83bdabafef431247e6b11a9aab8a0893fd8e82e1ed95b32e0373625b03ffce4a", size = 178501, upload-time = "2026-07-10T06:32:51.439Z" }, - { url = "https://files.pythonhosted.org/packages/91/87/7a0320df854dacd09507ca972cb04a4dc5aae279583cc5b80ad5f5819533/websockets-16.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b8d13ceabc5c60995f201b5211d76876e17e68706ebf5d3bc666b32eefff1a6", size = 179397, upload-time = "2026-07-10T06:32:52.892Z" }, - { url = "https://files.pythonhosted.org/packages/31/6a/0da1eb8c8da2ace7b578c8523d32618af85e62a9ebad56051d4a14a38a1c/websockets-16.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81495f9c0085361c582efbc3207fb877174cfe03370f17d9cd70624404aa526f", size = 180546, upload-time = "2026-07-10T06:32:54.619Z" }, - { url = "https://files.pythonhosted.org/packages/66/58/bd83247f39ddc26ffc2c24eb05087a3b749e00cb4509fc6d19daa23c8495/websockets-16.1-py3-none-any.whl", hash = "sha256:c5149dfe490ec7e5ee5dbf624c642fb725f93a5575c7f00ab594ca9eddb8dd81", size = 174031, upload-time = "2026-07-10T06:32:56.079Z" }, -]