Use Pure Data patches as audio engines in browser apps.
libpd-wasm runs libpd as a WebAssembly
AudioWorklet. You give it a .pd file, it runs in the browser, and you control
it with normal Pd messages: bang, float, symbol, list, or arbitrary selectors.
Build whatever UI or app you want around it in JavaScript or TypeScript.
Try the demo · hyrfilm/libpd-wasm
Why This Exists
I was working on a browser-based music project and used RNBO to build a sequencer. RNBO is awesome, but it's not open-souce, and what you ship is one or more patches that are already compiled. That specific aspect is usually not a big deal but in my case I wanted to also be able to generate patches on-the-fly. I started looking at similar browser targets for Faust, Pd, and other audio tools.
For Pd, there was not really anything that fit. So I built it.
Unlike fixed patch exporters, this runtime loads patches at runtime, including patches edited or generated while the page is running. You can try that yourself by dropping a .pd file onto the patch-area in the playground.
Install
The package is browser-focused. It is intended for apps built with a browser bundler such as Vite.
Quick Start
Vite TypeScript projects need the standard Vite client types for ?url asset
imports. Put this in src/vite-env.d.ts if your project does not already have
it:
/// <reference types="vite/client" />Then import one of the packaged worklet assets:
import { createPd } from "libpd-wasm"; import workletUrl from "libpd-wasm/assets/libpd-worklet.js?url"; const pd = await createPd({ packages: ["vanilla"], files: { "patch.pd": patchSource, }, entry: "patch.pd", workletUrl, }); pd.connect(); pd.sendBang("start"); pd.sendFloat("cutoff", 1200);
For patches that need externals, import the matching worklet file and request the matching package list:
| Worklet asset | Packages |
|---|---|
libpd-worklet.js |
["vanilla"] |
libpd-worklet-cyclone.js |
["vanilla", "cyclone"] |
libpd-worklet-else.js |
["vanilla", "else"] |
libpd-worklet-full.js |
["vanilla", "cyclone", "else"] |
Example:
import { createPd } from "libpd-wasm"; import workletUrl from "libpd-wasm/assets/libpd-worklet-cyclone.js?url"; const pd = await createPd({ packages: ["vanilla", "cyclone"], files: { "patch.pd": patchSource }, entry: "patch.pd", workletUrl, });
Public API
import { checkPatch, createPd, getControls, manifest } from "libpd-wasm";
createPd(options)
Creates a Pd runtime, loads the entry patch, and returns a Pd instance.
const pd = await createPd({ packages: ["vanilla"], files: { "main.pd": source }, entry: "main.pd", workletUrl, });
The files object is a virtual filesystem. Keys are relative paths and values
are .pd source strings. entry is the patch to open.
The returned pd object exposes:
pd.node; pd.audioContext; pd.connect(destination?); pd.disconnect(); pd.close(); pd.loadPatch({ files, entry }); pd.sendBang(name); pd.sendFloat(name, value); pd.sendSymbol(name, value); pd.sendList(name, values); pd.sendMessage(name, selector, values); pd.subscribe(name, callback); pd.onPrint(callback); pd.onError(callback);
checkPatch(options)
Checks whether a patch appears compatible with the packages you selected.
const check = checkPatch({ packages: ["vanilla"], files: { "main.pd": source }, entry: "main.pd", }); if (!check.ok) { console.log(check.messages); }
This can be used, for example, when loading a patch at runtime before you actually attempt to load it.
getControls(options)
Reads browser-addressable GUI controls from a patch.
const controls = getControls({ files: { "main.pd": source }, entry: "main.pd", });
Controls are returned in the following shape:
{ name: string; kind: string; range: number[]; initial: Array<number | string>; }
Unaddressable controls are ignored.
Runtime Model
The browser runtime has two parts:
- JavaScript/TypeScript helpers imported by your app.
- AudioWorklet processor files loaded by URL with
audioContext.audioWorklet.addModule(...).
That second part matters. You can import functions from an npm package, but an
AudioWorklet still needs a real URL served by your app. libpd-wasm packages
the built worklet files under assets/ so Vite can serve them from
node_modules via ?url.
The runtime opens a virtual filesystem in the worklet. App code provides files
as { "path/to/file.pd": source } and identifies the entry file. There are no
Node filesystem APIs, host-specific search paths, or hidden network fetches in
the core runtime.
External Library Support
Beyond vanilla Pd, the current build includes large subsets of:
Many patches using these libraries should run, but not every object is included. Objects that depend on pthreads, networking, OpenGL, ffmpeg, aubio, libsamplerate, opus, or other native/browser-incompatible pieces are skipped for now.
Build variants let an app choose what to include:
| Variant | Includes |
|---|---|
basic |
libpd + Pd's own extra/ |
cyclone |
basic + cyclone |
else |
basic + ELSE |
full |
basic + cyclone + ELSE |
Browser WebAssembly does not load Pd externals the way a local Pd install loads
dynamic libraries, so cyclone and ELSE are currently statically linked into
separate runtime bundles. That is why the else and full worklet assets are
larger than the vanilla one.
Building From Source
git clone --recurse-submodules https://github.com/hyrfilm/libpd-wasm.git
cd libpd-wasm
nix develop
./build.shUseful development commands:
build-wasm # build the browser bundle and demo worklets build-native # native debug CMake build for quick iteration serve # serve the demo at http://localhost:8000/demo/ scripts/clean # remove build directories
Main generated outputs:
build-wasm/libpd.js
build-wasm/libpd.wasm
build-wasm/pd-object-manifest.json
demo/libpd-worklet.js
demo/libpd-worklet-cyclone.js
demo/libpd-worklet-else.js
demo/libpd-worklet-full.js
The build also generates an object manifest from the compiled Pd/externals sources. The npm analyzer uses that manifest to classify objects by package.
Current Limitations
- The package is designed for browser apps, not Node runtimes.
- Recursive abstraction/path resolution is intentionally minimal. Provide the
patch files you want the runtime to see in the
filesobject. - External-library bundles are large because objects are statically linked into WASM.
- Some cyclone and ELSE objects are skipped because they depend on native browser-incompatible APIs or libraries.
- The public API is deliberately small.
Can I Use This Commercially?
Yes. libpd uses the BSD license, which permits use in open source, private, and commercial projects.
Practical obligations:
- Keep the copyright notice and BSD license text when redistributing source.
- Include the copyright notice, BSD license text, and disclaimer with binary/browser bundle distributions.
- Do not imply that libpd, Pure Data, Miller Puckette, or other contributors endorse your product without permission.
- Also: I'm just a guy on the internet so if you're considering whether something is legal or not, ehhhh maybe talk to like an actual lawyer? This is not legal advice blablabla.
License
libpd is distributed under the BSD license. See LICENSE.txt. The original libpd README is preserved in docs/libpd-readme.md.