npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@underscore-audio/sdk

v0.1.0

Published

TypeScript SDK for loading and playing Underscore synths

Readme

@underscore-audio/sdk

TypeScript SDK for integrating Underscore AI-generated synthesizers into web applications.

npm version License: MIT

Features

  • Load and play AI-generated synthesizers in the browser
  • Real-time parameter control
  • Generate new synths with natural language
  • Full TypeScript support
  • Works with React, Vue, Svelte, vanilla JS, and any web framework

Installation

The fastest path is the install wizard. Run it inside an existing web project (Vite, Next.js, or vanilla HTML). It signs you in, installs this SDK plus supersonic-scsynth, patches your build config with the required COOP/COEP headers, writes your publishable key to .env.local, and scaffolds a small demo component:

npx @underscore-audio/wizard@latest

If you'd rather wire things up by hand:

npm install @underscore-audio/sdk supersonic-scsynth

The SDK lives in its own standalone repo: underscore-audio/underscore-web-sdk.

# Install directly from GitHub
npm install github:underscore-audio/underscore-web-sdk supersonic-scsynth

# Or reference a local clone in your package.json
# "@underscore-audio/sdk": "file:../path/to/underscore-web-sdk"

Quick Start

import { Underscore } from "@underscore-audio/sdk";

const client = new Underscore({
  apiKey: "us_pub_your_publishable_key",
  wasmBaseUrl: "/supersonic/",
});

// Initialize from user interaction (required by browsers)
document.getElementById("play")?.addEventListener("click", async () => {
  await client.init();
  const synth = await client.loadSynth("cmp_abc123");
  await synth.play();
});

Setup

1. Get an API Key

Sign up at underscore.audio -- a publishable API key is created for you automatically.

Underscore uses two key types:

| Type | Prefix | Where to use | Scopes | | --------------- | --------- | -------------------------- | ------------------------------ | | Publishable | us_pub_ | Browser / client-side code | synth:read only | | Secret | us_sec_ | Server-side only | synth:read, synth:generate |

Use your publishable key in the browser SDK. Use your secret key on your server for generating new synths (which consumes LLM credits).

2. Copy WASM Assets

npx underscore-sdk ./public/supersonic

3. Configure Server Headers

Your server must send these headers for WebAssembly to work:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
// vite.config.ts
export default defineConfig({
  server: {
    headers: {
      "Cross-Origin-Opener-Policy": "same-origin",
      "Cross-Origin-Embedder-Policy": "require-corp",
    },
  },
  optimizeDeps: {
    exclude: ["@underscore-audio/sdk", "supersonic-scsynth"],
  },
});
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
          { key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
        ],
      },
    ];
  },
};

Example

See examples/ for a complete working example that demonstrates the recommended backend-proxy pattern: a tiny Express server holds the secret key and proxies generation, while the browser uses only a publishable key for playback.

cd examples
npm install
npm run copy-assets

# Terminal 1: backend proxy (holds UNDERSCORE_SECRET_KEY)
UNDERSCORE_SECRET_KEY=us_sec_your_secret npm run server

# Terminal 2: browser app (uses the publishable key)
npm run dev

If you only need playback, skip npm run server and just run npm run dev. See examples/README.md for the full set of environment variables.

API Reference

Client

const client = new Underscore({
  apiKey: "us_pub_...", // Required (publishable key for browser)
  wasmBaseUrl: "/supersonic/", // Required for browser playback; omit server-side
  baseUrl: "https://underscore.audio", // Optional
  logLevel: "none", // Optional: debug | info | warn | error | none
});

await client.init(); // Initialize audio engine
client.isInitialized(); // Check status
await client.listSynths("cmp_..."); // List synths in composition
await client.loadSynth("cmp_...", name); // Load synth for playback

Synth

await synth.play(); // Start playback
synth.stop(); // Stop playback
synth.isPlaying(); // Check if playing

synth.setParam("cutoff", 2000); // Set parameter
synth.setParams({ cutoff: 2000 }); // Set multiple
synth.resetParams(); // Reset to defaults

synth.name; // Synth name
synth.description; // Description
synth.params; // ParamMetadata[]

Generation (backend-proxy pattern, recommended)

Generation consumes LLM credits and requires a secret key (us_sec_...) that must never ship to the browser. The SDK splits generation into two primitives so each half can run in its correct environment.

On your server (Node, holds the secret key):

import { Underscore } from "@underscore-audio/sdk";

// wasmBaseUrl is only consumed by init()/loadSynth(), so it can be
// omitted when the SDK is used purely for its HTTP client methods.
const server = new Underscore({
  apiKey: process.env.UNDERSCORE_SECRET_KEY!, // us_sec_...
});

// Expose a route your browser can call with { compositionId, description }.
const { jobId, streamUrl } = await server.startGeneration(compositionId, description);
// Return { streamUrl } (and optionally the host) to the browser.

In the browser (uses a publishable key for read/playback):

for await (const event of client.subscribeToGeneration(streamUrl, compositionId)) {
  switch (event.type) {
    case "thinking":
      console.log(event.content);
      break;
    case "progress":
      console.log(event.content);
      break;
    case "ready":
      await event.synth?.play();
      break;
    case "error":
      console.error(event.error);
      break;
    case "raw":
      /* unmapped server event, inspect event.raw */ break;
  }
}

The streamUrl contains an unguessable jobId that acts as a capability token, so the browser can subscribe without any API key.

Generation (trusted-environment one-shot)

client.generate(compositionId, description) chains both halves in a single call. It requires BOTH a usable secret key AND an EventSource global, so it's only safe in trusted environments like a Node CLI with an EventSource polyfill, an Electron app, or a local dev page. Do not use it in production browser apps -- use the backend-proxy pattern above.

for await (const event of client.generate("cmp_...", "warm analog pad")) {
  // same event shape as subscribeToGeneration
}

Error Handling

import { ApiError, AudioError, SynthError, ValidationError } from "@underscore-audio/sdk";

try {
  await client.loadSynth("invalid");
} catch (error) {
  if (error instanceof ApiError) {
    /* HTTP error */
  }
  if (error instanceof ValidationError) {
    /* Schema mismatch */
  }
  if (error instanceof AudioError) {
    /* WASM/WebAudio error */
  }
  if (error instanceof SynthError) {
    /* Playback error */
  }
}

Browser Support

| Browser | Version | | ---------- | ------- | | Chrome | 80+ | | Firefox | 79+ | | Safari | 15.4+ | | Edge | 80+ | | iOS Safari | 15.4+ |

Requires: SharedArrayBuffer, AudioWorklet, WebAssembly

Troubleshooting

| Issue | Solution | | ----------------------- | -------------------------------------------------------------- | | "Audio not initialized" | Call client.init() from a click handler | | WASM not loading | Run npx underscore-sdk ./public/supersonic and check headers | | No sound | Check client.isInitialized() and synth.isPlaying() | | "Composition not found" | Verify ID format (cmp_...) and visibility settings |

Development

npm install     # Install dependencies
npm run build   # Build
npm test        # Run fast mocked tests (no network)
npm run test:live # Run tests against a real Underscore API
npm run lint    # Lint

See CONTRIBUTING.md for the env vars the live suite reads; the same harness works against local (http://localhost:3333) and production (https://underscore.audio).

API Compatibility

Compatible with Underscore API v1.

Contributing / local testing

Testing the wizard locally

cd packages/wizard
npm install
npm run build
npm test                              # unit tests (vitest)
node dist/cli.js --help               # invoke the CLI locally

Full install-with-AI e2e coverage lives in the sibling underscore repo. From there:

make e2e-tarballs          # pack this repo's SDK + wizard
make test-install-wizard   # L2 + L3 Playwright suite

make e2e-tarballs expects underscore-web-sdk to be checked out next to underscore on disk.

Publishing the wizard

CI auto-publishes @underscore-audio/wizard to npm when a tag matching wizard-vX.Y.Z is pushed. One-time setup:

  1. Add NPM_TOKEN to this repo's GitHub Actions secrets (automation token with publish rights to the @underscore scope).

  2. Bump packages/wizard/package.json version.

  3. Tag + push:

    git tag wizard-v0.1.0
    git push origin wizard-v0.1.0

The publish-wizard job in .github/workflows/ci.yml handles the rest.

Known limitations / launch checklist

Tracked in Linear under the "Install with AI launch" parent. Summary:

  • Production blockers (P0): publish @underscore-audio/[email protected] to npm (the README above advertises npx @underscore-audio/wizard@latest which 404s until this ships).
  • Should-do before announcement (P1): dedicated "Install with AI" docs page with screenshots, wizard install telemetry, L4 audio smoke in the main repo's e2e suite.
  • Post-launch (P2): dynamic supersonic-scsynth peer-dep lookup instead of the hardcoded ^0.14.0 pin in packages/wizard/src/install.ts, AST-safe Next.js config patcher (today patch.ts returns manual-required for Next.js), wizard cancel/restart UX, and cross-repo CI for tarball consumption.

License

MIT