Skip to content

Colahall/lv2-wasm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lv2-wasm32

lv2-wasm32 is a work-in-progress runtime bridge for running LV2 plugin code compiled as WebAssembly (wasm32-wasip1) while preserving the familiar LV2 API.

The current repository is a proof of concept, not a finished DAW integration layer. Think of this like a sketch before the final painting.

Today it demonstrates that:

  1. A native C host can load an LV2 plugin compiled to WASM by using the wasmtime C API.
  2. Audio and control data can move between the native host and the guest module through a simple, explicit linear-memory model.

Quick Start: Build and Run the Demo

This project uses just (a command runner) to execute a simple demo. The build script automatically fetches required dependencies from GitHub and places them in the local project directory.

1. Install required packages (Ubuntu/Debian example):

sudo apt install build-essential lv2-dev just pkg-config

2. Fetch dependencies:

just fetch-deps

3. Build and run the demo:

just run

Project Goal

The long-term goal is to extend LV2 plugins to be more portable and sandboxable by moving plugin implementation code into WASM, while still fitting seamlessly into LV2 host expectations.

To achieve this, the architecture adheres to the following design pillars:

1. Seamless Host Integration

  • The LV2 API contract must remain strictly intact.
  • From the host's perspective, ports should behave exactly like normal LV2 ports.
  • The native side owns all VM setup, memory movement, and any proxying required for host services.

2. Future-Proofing and Architecture Independence

  • Plugin developers have an option to support WASM to extend the life of a plugin, ensuring its DSP logic does not rely on a specific OS or CPU architecture.
  • A single LV2 module can support both a traditional native shared library (for bare-metal execution on known targets) and a WASM module (as a portable, sandboxed, or long-term fallback).

Example manifest.ttl illustrating a hybrid binary declaration:

<http://example.org/plugin>
    a lv2:Plugin ;
    lv2:binary <plugin.so> , <plugin.dylib> , <plugin.dll> , <plugin.wasm> ;
    rdfs:seeAlso <plugin.ttl> .

3. A Strict Runtime Boundary

This implies a cleanly split architecture where responsibilities are explicitly divided:

  • Native side: LV2-facing wrapper, VM runtime, memory bridge, extension proxies, and UI integration.
  • WASM side: Actual plugin DSP logic, internal state, and guest-resident heap allocations.

Current Status

The repository currently proves the core bridge, but it does not yet implement a complete LV2 packaging and interoperability story.

What is implemented and validated today:

  • Wasmtime Integration: Loading and instantiation of a WASM LV2 module via the Wasmtime C API.
  • Guest Discovery: Resolution of guest exports including lv2_descriptor, linear memory, and guest allocator helpers.
  • Lifecycle Forwarding: Full native-to-guest routing for instantiate, connect_port, activate, run, deactivate, and cleanup.
  • Memory Bridge: A simple, explicit host-to-guest memory model for audio, CV, and scalar control data.
  • End-to-End Validation: A standalone demo host that successfully loads build/plugin.lv2/plugin.wasm, runs a sample passthrough plugin, and round-trips input samples to output samples.

What is not implemented yet:

  • extension_data() bridging: Required to support advanced LV2 features, state saving, and worker threads across the WASM boundary.
  • TTL-driven discovery: Port classification is currently manual; the host does not yet parse TTL metadata to configure the bridge.
  • Real DAW Integration: We lack the native LV2 wrapper/proxy plugin (.so/.dylib) that a real DAW needs to discover and load the WASM bundle.
  • Custom UI Support: The bridge currently relies entirely on the DAW's generic parameter view. It will also be low in priority, due to its complexity.

Architecture Overview

flowchart LR
    subgraph Native["Native Environment"]
        NativeHost["Native Caller / LV2 Host Wrapper"]
        TtlMetadata["TTL Metadata"] -.->|Planned| HostAPI
        ExtProxies["Extension Proxies"] -.->|Planned| HostAPI
        UiBridge["UI Bridge"] -.->|Planned| HostAPI
        NativeHost -->|C API| HostAPI["lv2_wasm32_host API"]
    end

    subgraph VM["Wasmtime Boundary"]
        HostAPI -->|Manages| WasmEngine["Engine + Store + Instance"]
    end

    subgraph Guest["WASM Sandbox (Guest)"]
        WasmEngine -->|Reads/Writes| GuestMemory[("Guest Linear Memory")]
        WasmEngine -->|Invokes| GuestExports["Guest Exported Functions\n(run, connect_port, etc.)"]
        WasmEngine -->|Invokes| GuestAllocators["Guest Allocator Helpers"]
        GuestAllocators -->|Allocates| GuestMemory
        GuestExports -->|Executes| WasmPlugin{"WASM LV2 Plugin\n(DSP & State)"}
        GuestMemory <-->|Audio/CV/Control Data| WasmPlugin
    end

    classDef native fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
    classDef vm fill:#fff3e0,stroke:#f57c00,stroke-width:2px;
    classDef guest fill:#e8f5e9,stroke:#388e3c,stroke-width:2px;

    class Native native;
    class VM vm;
    class Guest guest;
Loading

Repository Map

  • include/lv2_wasm32/lv2_wasm32.h

    • WASM-specific export helpers, will need to be merged with lv2.h in the future.
    • Rebinds LV2_SYMBOL_EXPORT under __wasm32__ so the guest exports lv2_descriptor with the expected symbol name.
  • include/lv2_wasm32/lv2_wasm32_host.h

    • Public native API for creating and driving a WASM-backed LV2 instance.
    • Defines the port classification flags and the host-side port map contract.
  • host/lv2_wasm32_host.c

    • Main runtime bridge.
  • host/main.c

    • Standalone demo harness.
    • Shows the intended lifecycle sequence and validates the sample passthrough plugin.
  • lib/wasm/lv2_wasm32_lib.c

    • Guest-side helper library compiled into the WASM module.
    • Exports allocator functions the host can call to reserve space inside guest memory.
    • Without this, malloc() and free() are not automatically exported into the WASM module.
  • plugin.lv2/plugin.c

    • Minimal sample LV2 plugin compiled into WASM.
    • Implements a two-port passthrough processor.
  • plugin.lv2/manifest.ttl and plugin.lv2/plugin.ttl

    • Metadata examples copied into the build bundle.
    • Not consumed by the current standalone host path.

Build-Time Architecture

Guest build

The guest plugin is compiled with wasi-sdk for wasm32-wasip1 in reactor mode:

  • Reactor mode is important because the module behaves like a library with exported functions instead of a standalone command.
  • The build exports the indirect function table so the host can resolve function references stored in the guest's LV2_Descriptor.
  • The WASM build includes both the sample plugin and the small helper library that exposes guest allocation APIs.

Native build

The native host side is compiled against:

  • LV2 headers for the API types.
  • wasmtime C headers and libraries for VM management.
  • Standard C runtime plus sanitizers in the main build target.

Runtime Architecture

Native runtime object

Each live WASM-backed plugin instance is represented by LV2_WASM32_Plugin. It owns:

  • A wasmtime engine and store.
  • The instantiated WASM module.
  • The exported linear memory handle.
  • Guest function handles for the LV2 lifecycle.
  • Guest allocator function handles.
  • The guest-side plugin instance handle returned by instantiate.
  • A host-managed port map that ties native pointers to guest offsets.
  • The configured maximum block size used to size guest buffers.

This is the central state object for the bridge.

Guest ABI assumptions

The current bridge assumes a guest ABI with three important properties:

  1. The guest exports lv2_descriptor.
  2. The guest exports linear memory as memory.
  3. The guest exports helper functions:
    • lv2_wasm32_aligned_alloc
    • lv2_wasm32_alloc
    • lv2_wasm32_free

The host reads a guest-side mirror of LV2_Descriptor from guest memory. That mirror stores:

  • A guest offset to the URI string.
  • Function table indices for LV2 lifecycle callbacks.

The host then resolves those indices through __indirect_function_table and stores the resulting callable wasmtime function references.

This is the core architectural trick that makes an ordinary LV2-style descriptor usable inside WASM.

Execution Flow

1. Create

lv2_wasm32_plugin_create() performs VM bring-up and bridge initialization:

  1. Allocate the native LV2_WASM32_Plugin.
  2. Copy caller-supplied port class information into the host port map.
  3. Create the wasmtime engine and store.
  4. Read and compile the WASM module from disk.
  5. Configure WASI and instantiate the module.
  6. Call _initialize when present.
  7. Discover guest allocator exports.
  8. Discover guest memory.
  9. Call guest lv2_descriptor(0).
  10. Read the descriptor struct out of guest linear memory.
  11. Resolve lifecycle function references from the indirect function table.
  12. Pre-allocate guest memory for every declared port.

At the end of create, the VM exists and all bridge pointers are resolved, but the guest plugin instance itself has not yet been created. That happens during instantiate.

2. Instantiate

lv2_wasm32_plugin_instantiate() forwards the LV2 instantiate call into the guest and stores the returned guest instance handle.

Current behavior:

  • sample_rate is passed through.
  • bundle_path is not yet serialized into guest memory.
  • features are not yet serialized into guest memory [TODO]

This means the prototype only supports plugins that do not depend on bundle paths or host-provided features.

3. Connect Port

lv2_wasm32_plugin_connect_port() does two things:

  1. It tells the guest plugin which guest-memory offset corresponds to that port.
  2. It stores the native host pointer locally in the host-side port map.

The guest never receives a native pointer. It only sees an offset into its own linear memory. The native pointer remains host-owned.

4. Activate / Deactivate / Cleanup

These functions are simple lifecycle forwarding calls into the guest. The host does not inject special policy here yet.

5. Run

lv2_wasm32_plugin_run() is the main data bridge:

  1. For every connected input port, copy native data into the corresponding guest buffer.
  2. Call the guest run() function with the guest instance handle and frame count.
  3. Refresh the linear-memory base pointer in case the guest reallocated memory during execution.
  4. For every connected output port, copy guest data back into native host buffers.

This design is explicit and simple. It is also intentionally not zero-copy.

Memory Model

The current memory model is the second milestone already achieved by the project.

Design principles

  • The guest owns all memory visible to guest code.
  • The host owns all native audio/control pointers.
  • Port connections are modeled as offset mappings, not shared raw pointers.
  • Audio/CV data is block-copied at run() time.
  • Scalar control values are copied as a single float.

Buffer allocation strategy

During create, the host allocates guest memory for ports in advance:

  • Audio and CV ports get a dedicated aligned block sized as:
    • max_block_size * sizeof(float)
  • Control ports are packed into a single scalar area:
    • one float per port

Current assumptions:

  • Audio, CV, and control values are a block of float.
  • A single max_block_size chosen at instance creation time is sufficient for all future run() calls, so memory is never reallocated.
  • The guest allocator exports remain valid for the entire lifetime of the module instance.

Extension Data and Feature Support

This is the largest missing architectural piece.

There are two related but distinct problems:

LV2 features passed into instantiate()

LV2 features are native structs, often containing:

  • host-owned pointers
  • callbacks
  • nested data
  • extension-specific ABI rules

Those cannot simply be reinterpreted inside WASM. They need either:

  • Serialization into guest memory, or
  • Native proxy objects that expose a WASM-safe ABI.

extension_data(uri)

The guest may advertise extension entry points or interface structs. In native LV2, those are returned as pointers to structs with function pointers. Inside WASM, that pointer is only meaningful relative to guest memory and guest function tables.

The native side therefore needs a translation layer that can:

  1. Ask the guest for the extension.
  2. Read the guest-side representation.
  3. Construct a native proxy object matching the expected LV2 extension ABI.
  4. Route native calls back into guest code safely.

This is why lv2_wasm32_plugin_extension_data() is still unimplemented today.

Key Constraints and Risks

  • This is currently a standalone demo host, not a complete DAW-facing LV2 wrapper.
  • Real-time behavior has not been formally validated.
  • The runtime depends on wasmtime, so deployment and packaging must account for the native wasmtime C library.
  • The guest ABI currently assumes 32-bit offsets and a stable descriptor layout.
  • The bridge is copy-based, which is simple and safe, but not the final word on performance.
  • The metadata path is not authoritative yet.
  • Feature and extension support will require explicit ABI design, not just more plumbing.

Milestones

Milestone 1: wasmtime loader and C runtime integration

Status: complete

Delivered:

  • Compile LV2 plugin code into WASM.
  • Load the WASM module in a native C host using wasmtime.
  • Resolve exported memory and lifecycle entry points.
  • Forward core LV2 calls into the guest.

Milestone 2: Simple host-to-VM memory model

Status: complete

Delivered:

  • Host-side port map from native buffers to guest offsets.
  • Guest-side allocation helpers for buffer reservation.
  • Input copy into guest memory before run().
  • Output copy back to host memory after run().
  • Validation through the passthrough sample plugin.

Milestone 3: Integrate into lilv project

Status: proposed

Goals:

  • Extend lilv's plugin loading logic to recognize .wasm binaries declared in LV2 TTL metadata.
  • Embed the lv2_wasm32_host bridge within lilv's instantiation pipeline so it can manage the Wasmtime engine and lifecycle.
  • Abstract the Wasm-to-native boundary completely, ensuring that host applications using lilv receive a standard LilvInstance and do not need to manage the VM themselves.
  • Replace the bridge's current manual port classification with lilv's authoritative, TTL-driven port mapping.

Definition of done:

  • A standard lilv-based tool (such as jalv or lv2info) can successfully discover, inspect, and execute a WASM-backed LV2 plugin without modification.
  • A DAW using lilv can load a hybrid bundle (containing both a .so and .wasm file) and route audio through the WASM guest seamlessly.

Milestone 4: Native UI bridge for WASM plugins

Status: proposed

Goals:

  • Expose a real LV2 UI strategy beyond the generic DAW control surface.
  • Define message transport between native UI code and guest state.
  • Choose a host-compatible first UI path, likely a native proxy UI.

Definition of done:

  • At least one WASM-backed plugin can expose a custom UI in an LV2 host environment.

Recommended Next Steps

If development continues from the current codebase, the most sensible order of operations is:

  1. Gather feedback and requirements: Present this proof-of-concept to the broader LV2 and DAW development communities to validate the proposed host-guest boundary and memory model assumptions.
  2. Start integrating into the lilv project: Shift from a custom standalone host to a lilv-embedded bridge. This immediately solves the metadata discovery gap and allows testing with standard ecosystem tools like jalv.
  3. Design the feature and extension ABI: Before adding more complex plugin DSP, define how essential LV2 features (specifically urid#map and atom events) will be serialized or proxied safely across the WASM boundary.
  4. Tackle the UI architecture: Once DSP, metadata, and control events are bridging successfully through lilv, establish a host-compatible native proxy UI model.

Current Bottom Line

The project already demonstrates the essential core idea: an LV2 plugin can be compiled to WASM, loaded with wasmtime, and executed through a native C bridge that preserves LV2 lifecycle calls and moves port data through guest linear memory.

That is the right foundation. The remaining work is no longer about proving basic feasibility. It is about turning the bridge into a real LV2 interoperability layer by solving metadata, features, extension data, UI, and host compatibility.

AI Use Transparency

Large Language Models (LLMs) were utilized in the creation and modification of this document. All architectural concepts, technical claims, and final phrasing have been strictly reviewed and validated by human contributors.

About

[WIP] A runtime bridge for running LV2 audio plugins compiled as WebAssembly (WASM).

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors