A language for audio, with a compiler that ships nothing.

Pole compiles a DSP patch to an object file and a C header. Link them and there is no JIT, no runtime and no compiler inside the plugin you ship. The same compiler targets WebAssembly — which is why the patch below plays in this page.

A plucked string — press Run, then the keysPlayground
processor Pluck
{
    input  stream float pitch;
    input  stream float gate;
    output stream float out;

    param  float decay = 0.4f [0.05f, 2.0f];
    param  float tone  = 0.4f [0.05f, 0.95f];

    float phase = 0.0f;
    float env   = 0.0f;
    float lp    = 0.0f;

    void main()
    {
        loop
        {
            if (gate > 0.5f) { env = 1.0f; }
            else { env = env * (1.0f - 1.0f / (decay * 48000.0f)); }

            let saw = phase * 2.0f - 1.0f;
            lp = lp + tone * (saw - lp);

            out <- lp * env * 0.3f;

            phase = phase + pitch / 48000.0f;
            if (phase >= 1.0f) { phase = phase - 1.0f; }
            advance();
        }
    }
}

What it compiles to

reverb.pole
92 lines of Pole
polec
--emit=export
reverb.o
1,944 bytes
reverb.h
4,318 bytes
linked into your plugin
--emit=web
reverb.o
2,273 bytes, wasm32
reverb.json
2,468 bytes
wasm-ld
reverb.wasm
2,760 bytes
run by a browser
examples/reverb.pole, compiled for a plugin and for a browser. The sizes are of the files polec writes.

Native

An object file and a C header. The header says how big the state block is; Pole never allocates. Link it into a plugin and the plugin carries no compiler.

WebAssembly

An object and a JSON manifest that tells a browser everything the C header would have told C. Nothing on that side parses the header — a runtime that parsed C would break when its formatting changed.

The same code

The exported object is the optimised module a JIT session would have run, so an exported plugin is not a port of what you were hearing. A bit-identity gate holds it to that.

Measured, not estimated

Every figure here was produced by running the thing that produces it, on the commit this page was written from.

1,944 bytes
a reverb, compiled

polec --emit=export on examples/reverb.pole. The header beside it is 4.3 KB.

one
undefined symbol in it

_expf. That is the whole run-time dependency: no compiler, no runtime, libm.

21–27 ms
to compile a patch

Median of eleven runs each for gain, filter, synth and reverb, to an object and a header.

24 of 29
examples render bit-identically in the browser runtime
  • adsr
  • bounded_search
  • delay
  • filter
  • fuzz
  • gain
  • modmatrix
  • octave_up
  • poly_graph
  • poly_simple
  • poly_synth
  • poly_voices
  • reverb
  • saw_gate
  • shaper
  • sine
  • soft_clip
  • stereo_chorus
  • stereo_width
  • synth
  • wavefolder
  • wavetable
  • wt_load
  • wt_synth
  • fft93 of 4,096 samples differ, by −150.5 dBFS at most
  • fx_chain2 of 4,096 samples differ, by −156.5 dBFS at most
  • spectral923 of 4,096 samples differ, by −138.5 dBFS at most
  • wt_osc532 of 4,096 samples differ, by −138.5 dBFS at most
  • feedback_lfoa feedback graph, which cannot run in a browser yet
  • poly_eventsneeds notes, which the check does not play
Every example rendered by the compiler and by the browser runtime, then compared float for float by scripts/check_wasm.py. The four that differ all call sin, cos, exp, pow or log, which the compiler takes from macOS’s maths library and the browser from musl’s.

Where it runs

 macOSLinuxWindows
compile, render, exportverifiedverifiednot built
WebAssembly outputverifiedverifiednot built
live audio and MIDIverifiedno backendno backend

“Not built” means exactly that: the compiler core has no platform headers and no reason not to build on Windows, but nobody has built it, so this table does not say it works. The platforms page has the detail.

Start anywhere

Write a patch in the browser

The playground compiles Pole with the same compiler and plays it in the page. There is nothing to install.

Open the playground