本文へ移動
cccskills
無料GitHub で公開

pulp-web-demo

Generate and maintain browser demos of Pulp audio plugins (both web ABIs — WAM and WCLAP) from one declarative config, so every demo mounts the SAME shared player and the two ABIs stay in lockstep. Invoke it whenever you publish/update a browser demo of a Pulp plugin, add a plugin to a demo gallery, stand up a new gallery, or touch demo pages/hosting. It runs a deterministic generator (config → stamped WAM+WCLAP site + owned-files manifest) and a blocking validator (config schema, cross-origin-isolation coverage, OG metadata, regeneration ownership). Customization is declarative config in <repo>/.pulp-web-demo/config.json (schema bundled) plus at most two typed module seams; the shared player (pinned @danielraffel/web-player) holds all UX behavior and is never vendored. Works in Claude and Codex.

インストール方法を見る

含まれるファイル(14)

  • SKILL.md14.8 KB
  • config.schema.json11.0 KB
  • examples/example.config.json1.3 KB
  • generate.mjs10.8 KB
  • README.md5.0 KB
  • templates/ci/pages.yml.tmpl1.4 KB
  • templates/gallery.index.html.tmpl1.3 KB
  • templates/hosting/cloudflare/_headers.tmpl693 B
  • templates/hosting/cloudflare/wrangler.toml.tmpl361 B
  • templates/hosting/coi/coi-register.snippet.html391 B
  • templates/hosting/coi/coi-serviceworker.js3.9 KB
  • templates/wam.index.html.tmpl1.6 KB
  • templates/wclap.index.html.tmpl2.0 KB
  • validate.mjs8.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

pulp-web-demo — generative web-demo skill

The way Pulp plugin web demos are built so the two web ABIs behave identically: WAM (Emscripten → AudioWorklet, single-thread, plain static hosting) and WCLAP (threaded CLAP-compiled-to-wasm hosted by a worklet-resident CLAP host, needs cross-origin isolation). Apply by default when creating/updating a demo, adding a plugin to a gallery, or touching hosting.

The one rule

Both ABIs mount the SAME shared player. Every UX behavior — no-autoplay overlay, iOS/mobile touch hygiene, keyboard/polyphony, scope/meter/safety-limiter, PLST state round-trip, token theming — lives in @danielraffel/web-player (npm, pinned). The generated pages import that pinned player; they never vendor a copy (vendoring is the historical drift hazard). Fix any UX behavior once, in the player; both ABIs inherit it.

How it works

Customization is declarative data + at most two executable seams. The smarts are in the player; this skill is only the wiring + publish pipeline. Everything runs from the skill dir:

  1. Config — <repo-root>/.pulp-web-demo/config.json, validated against the bundled config.schema.json (schemaVersion: 1, unknown keys rejected). Declares: the pinned player, theme token/font hrefs, per-ABI deploy profiles, the plugin catalog, gallery nav/cross-links, and OG/metadata policy. See examples/example.config.json and README.md.
  2. Generate — node generate.mjs --config <cfg> --out <siteDir>. Deterministic (no network, no clock/random): identical config ⇒ byte-identical output. Stamps versioned templates into a WAM+WCLAP site and writes a .pulp-web-demo.manifest.json of owned files (with hashes) for upgrade safety. Use --check for a dry run.
  3. Validate (blocking) — node validate.mjs --config <cfg> --site <siteDir> --check-isolation --check-metadata --check-ownership. Fails CI on: schema/invariant violations, cross-origin-isolation not covering the deployed base path, missing OG tags, or locally-modified owned files.

Deploy profiles (declarative, per ABI)

Selected independently for WAM and WCLAP under deploy.wam / deploy.wclap:

  • github-pages — plain static. Correct for WAM (no isolation needed).
  • cloudflare — server COOP/COEP/CORP via a generated _headers. Default for WCLAP.
  • github-pages+coi — a coi-serviceworker isolation mirror. A labeled fallback only — it enlarges the failure surface around isolation; prefer a header-capable host.

Each profile carries a basePath (server path — isolation headers/scope MUST cover it) and a publicUrl (the ABI's own absolute origin+base for canonical/OG urls). WAM and WCLAP commonly live on different origins — keep both accurate.

The only two executable seams (optional .pulp-web-demo/hooks.mjs)

Everything else is data. Reach for these only when a value can't express it:

  • artifactResolver — (plugin, abi) => { dspUrl, processorUrl, extraAssets[] }. Maps a nonstandard build's outputs into the canonical site manifest. This is THE seam that stops people forking when their build emits dist/web/<target>/… instead of the default layout.
  • adapterModule — a non-default createAdapter (e.g. a custom WCLAP host). Omit to use the player's bundled WAM/WCLAP adapters.

Do not add general behavioral hooks or an "escape hatch" — they become places to recreate player behavior outside the package, which is the whole thing this skill prevents.

Non-negotiable pipeline policy

  • Pinned player, imported not vendored; cache-bust the main-thread import only (?v=<hash>), never the worklet/dsp URLs (both sides must resolve one processor name).
  • Cross-origin isolation must cover the real deployed path (e.g. /my-plugins/), and every worklet/wasm dependency must be isolation-compatible — validated, not assumed.
  • OG/metadata checks are blocking (never continue-on-error). The generator emits the og:image tag but does not render the image (rendering needs a browser; the generator is offline + deterministic). So with ogImageStrategy: "screenshot" your pipeline MUST render the images before validate.mjs --check-metadata runs — the validator fails if the tag points at a file that isn't there, or isn't really a PNG. Do not settle for an HTTP 200: a static host (Cloudflare Pages) will serve a missing asset as 200 and cheerfully confirm an image that does not exist. Check the bytes. Use ogImageStrategy: "text" if you don't render images.
  • Regeneration is ownership-aware: the validator refuses to clobber a locally-edited owned file without reconciliation.

File upload (dialog and drag-and-drop)

If a plugin takes a user-supplied file (a convolver's impulse response, a sample, a preset), declare it with the per-plugin fileUpload config (accept, label, hint). The demo must then offer both a file-dialog button and a drop zone — people drag files onto anything that looks like a target.

This is a PLAYER behavior, not a per-demo one. Like every other UX invariant, the drop-zone mechanics belong in the shared player so both ABIs inherit them. Re-implementing a drop zone in one demo page is exactly the drift this skill exists to prevent — its WCLAP twin would then need its own copy, and the two would diverge.

Put it INSIDE the plugin, directly under the controls. Placement is not cosmetics here. A loader parked below the plugin panel — in the page chrome, past the scope and the meter — reads as page furniture rather than part of the instrument, and people simply do not find it: it is the one control the demo is asking them to use, and it is the one sitting outside the box everything else lives in. It belongs in the panel, immediately under the last row of controls, with the gap between them tight enough that they read as one unit. The player exposes a slot for exactly this (chrome the plugin owns, rendered inside the panel); use it rather than appending to the page.

Any implementation MUST satisfy all six rules. Each one, skipped, makes the zone feel broken:

  1. Swallow drops on the whole document. The browser's default action for a file dropped anywhere on the page is to navigate to it, destroying the running demo — audio context, loaded state, knob positions. Add document listeners for dragover and drop that call preventDefault() and nothing else, so a ten-pixel miss is inert rather than session-ending. That is a brutal punishment for a gesture you invited. Unbind them in destroy().
  2. Count dragenter/dragleave depth — do not toggle. dragleave bubbles from the zone's own children, so dragging across a button inside the zone fires it and the highlight strobes. Keep a depth counter; clear the highlight only at zero.
  3. Scope the highlight to the drop zone, never the whole plugin — the highlight is the thing that tells someone where the target is.
  4. Set dataTransfer.dropEffect = "copy" on dragover, so the cursor shows a copy badge rather than a "no entry" sign.
  5. Handle the empty drop. dataTransfer.files[0] can be undefined (dragged text, a URL). Say so; don't throw.
  6. Keep the button. Drop is a shortcut, not a replacement — and it is the only path on touch devices.

Two testing gotchas — both make a correct implementation look broken:

  • Drive it with real DragEvents and a real DataTransfer carrying a real File. The real browser order when the pointer crosses into a child is dragenter on the new target, then dragleave on the old — so a test that fires a bare dragleave will report a highlight-flicker bug that does not exist.
  • Regeneration is ownership-aware: the validator refuses to clobber a locally-edited owned file without reconciliation.

Gallery and theme (both optional)

  • gallery.emit — "auto" (default): emit the gallery landing page only when the catalog has more than one plugin (a gallery is pointless for a single plugin). true / false force it; use false to keep a hand-curated landing page of your own.
  • theme — omit it entirely and the player uses its own bundled skin. Only set tokensHref / fontHref when you are actually supplying a token stylesheet.

File upload (dialog and drag-and-drop)

If a plugin takes a user-supplied file (a convolver's impulse response, a sample, a preset), declare it with the per-plugin fileUpload config (accept, label, hint). The demo must then offer both a file-dialog button and a drop zone — people drag files onto anything that looks like a target.

This is a PLAYER behavior, not a per-demo one. Like every other UX invariant, the drop-zone mechanics belong in the shared player so both ABIs inherit them. Re-implementing a drop zone in one demo page is exactly the drift this skill exists to prevent — its WCLAP twin would then need its own copy, and the two would diverge.

Any implementation MUST satisfy all six rules. Each one, skipped, makes the zone feel broken:

  1. Swallow drops on the whole document. The browser's default action for a file dropped anywhere on the page is to navigate to it, destroying the running demo — audio context, loaded state, knob positions. Add document listeners for dragover and drop that call preventDefault() and nothing else, so a ten-pixel miss is inert rather than session-ending. That is a brutal punishment for a gesture you invited. Unbind them in destroy().
  2. Count dragenter/dragleave depth — do not toggle. dragleave bubbles from the zone's own children, so dragging across a button inside the zone fires it and the highlight strobes. Keep a depth counter; clear the highlight only at zero.
  3. Scope the highlight to the drop zone, never the whole plugin — the highlight is the thing that tells someone where the target is.
  4. Set dataTransfer.dropEffect = "copy" on dragover, so the cursor shows a copy badge rather than a "no entry" sign.
  5. Handle the empty drop. dataTransfer.files[0] can be undefined (dragged text, a URL). Say so; don't throw.
  6. Keep the button. Drop is a shortcut, not a replacement — and it is the only path on touch devices.

Two testing gotchas — both make a correct implementation look broken:

  • Drive it with real DragEvents and a real DataTransfer carrying a real File. The real browser order when the pointer crosses into a child is dragenter on the new target, then dragleave on the old — so a test that fires a bare dragleave will report a highlight-flicker bug that does not exist.
  • dropEffect cannot be asserted from a synthetic drag. The spec honours it only during a real user-initiated drag, so on a synthetic DataTransfer dropEffect = "copy" silently stays "none" — a browser-driven test will "fail" a line that is perfectly correct. Pin rule 4 in a DOM shim (where the assignment is observable) and do not "fix" the code to satisfy a synthetic drag.

The player implements this (src/ui/file-upload.js in @danielraffel/web-player): declare fileUpload and you get the zone, the button, and all six rules for free, on both ABIs. The player owns the interaction; the encoding is yours — only the plugin knows how its bytes want to look — so pass onFile(file, api) and use the api.writeBlob() you're handed, which preserves the plugin's params (loading a file must never reset the knobs). With no onFile, the player writes the file's raw bytes and lets the plugin decode them.

One related trap worth knowing: decodeAudioData resamples, so the decoded buffer never carries the file's real sample rate. If you tell the user anything about their file, parse the WAV/AIFF header instead of trusting the decoded buffer.

Gotchas (learned the hard way — do not re-derive)

  • Never put a ?query on the player's import specifier. An import map keys on the exact bare specifier, so import ... from "<pkg>?v=<hash>" never matches the "<pkg>" key and the module fails to resolve — the demo does not load at all. The cache-bust belongs on the mapped URL. A versioned CDN pin is already its own cache key.
  • A midi-effect demo without synthUrls is SILENT. It renders fine and looks correct, but a MIDI effect emits notes with nothing to play them. synthUrls chains it into a synth voice pool. Always set it for mode: "midi-effect".
  • The WAM artifact set is three files, not two: wam-dsp.js, wam-processor.js, and wam-runtime.mjs. The worklet imports the runtime relative to itself, so it must be co-located with the processor. Miss it and audioWorklet.addModule() fails with a bare AbortError: Unable to load a worklet's module.
  • Never invent a tokensHref. A made-up relative path just 404s and silently drops the skin.
  • Pin a player version that actually HAS the features you declare. The config pins the player, so declaring fileUpload against a version predating it yields a demo that silently renders no zone. fileUpload needs >= 0.2.0. When the player gains a feature, bump the pin in your config — the pin is what the generated page imports.
  • Cache-bust the main-thread entry only — never the worklet/dsp URLs; both sides must resolve to one processor name.

Example

examples/example.config.json is a copy-and-edit starting point: one plugin, WAM on GitHub Pages, WCLAP on Cloudflare, with placeholder origins. Drop it at .pulp-web-demo/config.json in your plugin repo and change the URLs, theme, and plugin catalog. The reader-facing WAM-vs-WCLAP explainer that ships with the demo galleries is the companion doc.

Provenance

templates/hosting/coi/coi-serviceworker.js is vendored MIT (Guido Zuidhof / coi-serviceworker). Everything else here is part of the Pulp SDK. Follow pulp-feature-lifecycle for review/PR/merge around any change to demos or this skill.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

aax

無料

Optional AAX support for Pulp, including developer-supplied Avid SDK setup, CMake enablement, DigiShell/AAX Validator workflows, and local AAX builds on macOS or Windows.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

Configure, implement, and test Pulp's optional desktop Ableton Link tempo-sync adapter while preserving the developer-supplied SDK, licensing, realtime, latency-compensation, and no-install boundaries.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

Maintain Pulp's installed design-time agent capability manifest and public-surface ledger. Use when adding, removing, renaming, or materially changing public audio, MIDI, signal, timebase, or sequence APIs; registering a new algorithm for generators; changing capability support or deprecation state; or repairing agent-capabilities freshness, schema, fingerprint, tombstone, or installed-SDK tests.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

android

無料

Android platform development for Pulp — NDK cross-compilation, Oboe audio, Dawn/Skia GPU rendering, JNI bridge, touch interaction, emulator workflows, and end-to-end smoke validation. Covers build, deploy, debug, and the gotchas discovered during bringup.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

ara

無料

Optional ARA support for Pulp, including developer-supplied ARA SDK setup, CMake enablement, adapter companion APIs, validation, and ARA-aware plugin implementation guidance.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

The measurement surface for ALL Pulp DSP and audio-pipeline work — read it BEFORE writing or gating DSP, not only when something already sounds wrong. Covers the C++ harness (signal generators, metrics, assertions, RenderScenario, contracts), the offline Audio Doctor (magnitude/frequency response, THD/THD+N, phase/group delay), and their Python sibling the Audio Quality Lab (tools/audio/quality-lab — null residual + alignment, LTAS log-spectral distance, spectral flux/centroid, HNR, Theil-Sen drift slope, Kaiser-sinc resampling, license-guarded corpus, regression-net ratchet). TRIGGER on AUTHORING work — "build/design an oscillator/filter/synth/effect", "add a DSP module", "what should the acceptance gate be", "how do I measure aliasing / anti-aliasing / alias floor", "null against a reference", "is this DSP correct", "choose a tolerance", "golden/regression corpus for audio", "measure drift or jitter", "A/B two renders" — AND on DEBUGGING work — "is there sound / no audio / I hear nothing", "does this filter/compressor/synth/delay produce the right signal", "prove the DSP / prove the contract", "measure the frequency response", "what's the THD / is it distorting", "what's the group delay / phase response / measured latency", "magnitude response curve", "render a test tone and assert", "audio regression", "64-frame works but 128 is silent", "sample-rate change pitch-shifted it", "describe what's in this buffer", "audio doctor", "compare before/after a DSP refactor". Reach for this BEFORE hand-rolling any FFT, null test, alias measurement, pitch tracker, or golden-render script — most of it already exists in one of the two lanes. Test/tool layer over HeadlessHost — deterministic, no audio device, no speakers. Off the realtime thread entirely.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月10日 更新

Generous-Corp のスキルをすべて見る

このスキルの問題を報告する