pole

pole dspa language for audio, made by patchhaus

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

Pole compiles a DSP patch to native code: an object file and a C header. Link them and the plugin you ship carries no JIT, no compiler, and no Pole library: the compiled code only calls the C math library. The same compiler also 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();
        }
    }
}

the output

what it compiles to.

polec 1.0reverb.pole
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

native

An object file and a C header. The header says how big the state block is, and 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.

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.

native

how it runs in a plugin.

The browser is where Pole is easiest to try. The native object is what ends up in your plugin, and this is how it behaves there.

terminalzsh
$ polec home-modules.pole --emit=export --out=home-modulespolec: wrote home-modules.o (7 functions, target arm64-apple-darwin25.0.0)polec: wrote home-modules.h$ wc -c home-modules.o   24904 home-modules.o$ nm -u home-modules.o_expf_sinf_tanf_tanhf$ export-check home-modules.pole  OK    identity: 1500 blocks of 64 frames, 3 side channels, 33 MIDI events, 24 parameter writes -- 0 block(s) differ, largest difference 0export-check: the plugin sounds like the editor

a real session, 2026-09-17 · export-check trimmed to its last two lines

compiled before it ships

polec turns the patch into machine code for the processor it runs on (Apple M1 and later, by default, on Apple silicon) before the plugin is built. Nothing compiles while it plays.

--emit=export → an object file and a C header

nothing but math

The compiled code calls a few math functions and nothing else. It never calls a memory allocator, takes a lock, or asks the operating system for anything while audio runs.

all 30 examples: libm only, three import nothing

state in one block

The generated code says how big its state is. The plugin allocates that once, when it loads, and hands the same block to every render call, so nothing grows while it plays.

reverb.h: POLE_REVERB_STATE = 10253 floats

what you heard is what ships

On every graph patch we test, the exported plugin plays the editor's samples bit for bit. export-check drives both with chords, retriggers, releases, and parameter moves, with wavetables bound, and compares every block.

30 graph patches, 0 blocks differ

measured, not estimated

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, from the C math library the operating system already has. The compiled code needs nothing else at run time.

21–27 ms

to compile a patch.

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

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

the browser against the compiler

24 of 29

examples render bit-identically in the browser runtime.

check_wasm.py4,096 frames · 48 kHz
  • 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.

platforms

where it runs.

system info 1.0
 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.