WAX Native Developer Guide

Canonical Markdown:
NATIVE.md

Introducation

Build with deeper integration for low-latency, real-time audio and MIDI processing using the WAX Native audio engine.

WAX Native is designed to go beyond the limitations of browser-based audio with sample-accurate MIDI, tight host synchronization, and native performance built for professional music production.


What WAX Native Is

A WAX Native page has two distinct parts:

Your HTML and JavaScript build the user interface and define how the audio and MIDI components work together.

The WAX plugin uses this configuration to process audio and MIDI natively on its real-time audio thread.

Your page is therefore not processing the final audio through a browser AudioContext.

Instead, JavaScript builds a graph:

Input
  ↓
Filter
  ↓
Gain
  ↓
Output

WAX converts that graph into an instruction batch and sends it to the native runtime.

Once installed, the graph continues processing independently of the WebView.

That distinction is the core idea behind WAX Native.


1. Creating a Native Graph

WAX automatically injects WaxNative into pages running inside the plugin.

Create an instance:

const wax = WaxNative.create();

The returned wax object contains the native graph nodes:

wax.cycle()
wax.svf()
wax.mul()
wax.const()
wax.adsr()
wax.midinotein()
wax.in()

To activate the graph, call:

wax.render(left, right);

For example, a simple oscillator:

const wax = WaxNative.create();

const tone = wax.mul(
  0.15,
  wax.cycle(440)
);

wax.render(tone, tone);

This creates a 440 Hz oscillator and sends the same signal to the left and right plugin outputs.


2. Native Audio Effects

Effects receive audio directly from the host.

Use:

wax.in({ channel: 0 })

for the first plugin input.

A simple low-pass filter:

const wax = WaxNative.create();

function renderGraph() {
  const input = wax.in({ channel: 0 });

  const filtered = wax.svf(
    { mode: "lowpass" },
    1200,
    0.8,
    input
  );

  const output = wax.mul(0.85, filtered);

  wax.render(output, output);
}

The audio path is:

DAW Track
   ↓
WAX Plugin Input
   ↓
Native Filter
   ↓
Native Gain
   ↓
Plugin Output

No browser audio processing is involved.


3. Parameters

Parameters that come from your interface should use stable graph keys.

let cutoff = 1200;
let resonance = 0.8;

function renderGraph() {
  const input = wax.in({ channel: 0 });

  const cutoffNode = wax.const({
    key: "cutoff",
    value: cutoff
  });

  const resonanceNode = wax.const({
    key: "resonance",
    value: resonance
  });

  const output = wax.svf(
    { mode: "lowpass" },
    cutoffNode,
    resonanceNode,
    input
  );

  wax.render(output, output);
}

When a control changes:

cutoffSlider.addEventListener("input", function () {
  cutoff = Number(this.value);
  renderGraph();
});

Use stable keys such as:

cutoff
resonance
gain
attack
release
voice0Gate
voice0Frequency

Do not generate random parameter keys during each render.


4. Rendering Mono and Stereo

wax.render() defines the plugin outputs.

Mono

wax.render(signal);

The signal is sent to output channel 0.

Dual mono

wax.render(signal, signal);

The same signal goes to left and right.

This is usually the safest choice for synthesizers.

Stereo

wax.render(left, right);

For a stereo effect:

const left = wax.in({ channel: 0 });
const right = wax.in({ channel: 1 });

wax.render(
  processLeft(left),
  processRight(right)
);

Be aware that input channel 1 may be silent when the plugin is inserted on a mono track.


5. Native MIDI

For instruments, MIDI should live inside the native graph.

Do not build an instrument whose envelopes depend on continuous JavaScript updates.

Use:

wax.midinotein()

The host’s MIDI buffer is delivered directly to the native graph every audio block.

A typical polyphonic voice begins with:

const midi = wax.midinotein();

const allocated = wax.midinoteallocate(
  { voices: 8 },
  midi
);

const [frequency, velocity] = wax.midinoteunpack(
  { channel: 0 },
  allocated
);

From there you can build a gate:

const gate = wax.ge(
  velocity,
  wax.const({
    key: "gateThreshold",
    value: 0.001
  })
);

And feed it into an envelope:

const envelope = wax.adsr(
  attack,
  decay,
  sustain,
  release,
  gate
);

This matters because the envelope is now running inside the native audio graph.


6. On-Screen Keyboard MIDI

DAW MIDI reaches wax.midinotein() automatically.

For an on-screen keyboard, send MIDI into WAX using the MIDI helper on the same native instance.

const wax = WaxNative.create();

async function boot() {
  try {
    await wax.midi.ready();
  } catch (_) {}

  renderGraph();
}

Then:

function noteOn(note, velocity = 1) {
  wax.midi.noteOn(
    note,
    Math.round(velocity * 127)
  );
}

function noteOff(note) {
  wax.midi.noteOff(note);
}

Example:

noteOn(60, 0.8);
noteOff(60);

The path becomes:

UI Keyboard
   ↓
wax.midi
   ↓
WAX MIDI Buffer
   ↓
wax.midinotein()
   ↓
Native Synth Graph

7. Why Native MIDI Matters

Browser-based MIDI handling can introduce timing inconsistencies because JavaScript scheduling is not designed for real-time DAW audio.

WAX Native processes MIDI directly within the plugin’s real-time engine, providing lower latency, precise timing, and reliable synchronization with the DAW transport.

With native MIDI:

DAW MIDI
   ↓
midinotein
   ↓
native allocation
   ↓
native ADSR
   ↓
native oscillator

the audio thread remains responsible for musical timing.

That is the architecture you want for production instruments.


8. Booting WAX Native Correctly

One of the most important WAX Native rules is:

Do not assume your first graph emit will survive page navigation.

When a top-level WAX page loads, the plugin resets the previous native graph.

If your script emits too early, this can happen:

Your page emits graph
        ↓
WAX finishes navigation
        ↓
WAX clears graph
        ↓
Silence

For top-level Project pages, use deferred boot attempts.

const wax = WaxNative.create();

function renderGraph() {
  const input = wax.in({ channel: 0 });

  const output = wax.mul(
    0.85,
    wax.svf(
      { mode: "lowpass" },
      1200,
      0.8,
      input
    )
  );

  wax.render(output, output);
}

Then create a boot poke:

function poke() {
  WaxNative.keepAlive();
  renderGraph();

  if (!wax.didEmit()) {
    setTimeout(function () {
      renderGraph();

      if (typeof wax.reemitLast === "function") {
        wax.reemitLast("retry");
      }
    }, 40);
  }
}

Schedule several attempts:

[
  50,
  150,
  400,
  800,
  1500,
  2500,
  4000
].forEach(function (ms) {
  setTimeout(poke, ms);
});

This makes top-level Project pages much more reliable.


9. Never Use RAF for Audio-Critical Work

Do not schedule graph activation with:

requestAnimationFrame(renderGraph);

An out of focus or hidden WAX plugin window may stop delivering animation frames.

Use:

setTimeout(renderGraph, 0);

or:

renderGraph();

when synchronous execution is appropriate.

requestAnimationFrame is fine for visual animation:

function drawMeter() {
  // canvas drawing
  requestAnimationFrame(drawMeter);
}

It should not control whether your audio graph exists.


10. Keep the WAX Page Alive

For long-lived audio interfaces:

WaxNative.keepAlive();

This wraps WAX’s foreground behavior and helps keep WebView timers and host integration healthy when the editor is parked.

A common boot sequence is:

function poke() {
  WaxNative.keepAlive();
  renderGraph();
}

The native graph itself does not require JavaScript to process every audio block, but timers, transport listeners, UI synchronization, and state management still benefit from the page remaining active.


11. Detecting WAX

Use:

WaxNative.hasBridge()

to determine whether the page is actually running inside a native WAX host.

if (WaxNative.hasBridge()) {
  console.log("Native WAX available");
}

12. Browser Preview

A WAX Native graph can optionally share its graph-building code with a normal browser.

Use WaxWebAudio for this.

Load:

<script src="https://szfpro.github.io/CodeEditorHTML/wax-native.js"></script>
<script src="https://szfpro.github.io/CodeEditorHTML/wax-web-audio.js"></script>

Then write one graph builder:

function buildGraph(wax) {
  const tone = wax.mul(
    0.15,
    wax.cycle(440)
  );

  return {
    left: tone,
    right: tone
  };
}

Inside WAX:

const nativeWax = WaxNative.create();

const graph = buildGraph(nativeWax);

nativeWax.render(
  graph.left,
  graph.right
);

In a browser:

const ctx = new AudioContext();

const webWax = await WaxWebAudio.createWeb({
  audioContext: ctx
});

const graph = buildGraph(webWax);

await webWax.render(
  graph.left,
  graph.right
);

This allows one UI and one graph description to support both environments.

For plugin-only devices, WaxWebAudio is unnecessary.


13. DataTree Presets and Session Recall

WAX DataTree stores application state.

For example:

{
  schema: 1,
  params: {
    cutoff: 1200,
    resonance: 0.8,
    drive: 0.25
  }
}

On recall, the process is:

DAW restores WAX
      ↓
WAX restores page URL
      ↓
Page boots
      ↓
Native graph is created
      ↓
DataTree state is loaded
      ↓
JS parameter values are restored
      ↓
Graph is rendered again

The graph must be recreated after recall.
It does not store the compiled native graph.


14. Stable Application IDs

Give every device its own DataTree application name.

const APP_NAME = "wax-native-filter";

Examples:

wax-native-filter
wax-native-poly
wax-native-compressor
wax-native-sequencer

Do not reuse one DataTree ID across unrelated devices.

Otherwise their saved state values can overwrite each other if switching between 2 web apps on the same web domain.


15. Applying Recalled State

Suppose the graph reads:

let cutoff = 1200;
let resonance = 0.8;

When state returns, update the variables:

function applyState(state) {
  const params = state?.params || {};

  cutoff = params.cutoff ?? cutoff;
  resonance = params.resonance ?? resonance;

  cutoffSlider.value = cutoff;
  resonanceSlider.value = resonance;

  renderGraph();
}

Do not only update:

cutoffSlider.value = params.cutoff;

If renderGraph() reads the JavaScript variable cutoff, the interface may show the recalled preset while the audio continues using the old value.


16. DataTree Boot Timing

Do not restore presets at the very beginning of page parsing.

First let the native graph successfully emit.

Then restore the DataTree state.

A useful pattern is:

setTimeout(initDataTree, 600);

Before applying recalled state:

if (wax.didEmit()) {
  applyState(savedState);
} else {
  setTimeout(function () {
    applyState(savedState);
  }, 150);
}

This prevents state restoration from fighting the navigation/reset period.


17. Transport

WAX exposes DAW transport hooks.

window.WAX_Play = function () {
  setTransport(true);
};

window.WAX_Stop = function () {
  setTransport(false);
};

window.WAX_BPM = function (bpm) {
  setTempo(bpm);
};

For transport-sensitive native graphs, render immediately when the transport edge occurs.

window.WAX_Play = function () {
  playing = true;
  renderGraph();
};

window.WAX_Stop = function () {
  playing = false;
  renderGraph();
};

Do not queue these changes through requestAnimationFrame.


18. Native Sequencers

Whenever possible, timing should happen inside the native graph.

For example, graph nodes such as native trains, sequences, gates, and envelopes can continue running when the browser UI is parked.

Prefer:

DAW transport
     ↓
native timing nodes
     ↓
native sequence
     ↓
native synth/effect

over:

setInterval()
     ↓
JavaScript step
     ↓
graph rebuild

for anything timing-critical.

The UI can still visualize the sequence with JavaScript, but audio timing should not depend on the visual loop.


19. Re-Rendering

Rebuilding a graph is appropriate when a user changes parameters.

For example:

let renderPending = false;

function requestGraphRender() {
  if (renderPending) return;

  renderPending = true;

  setTimeout(function () {
    renderPending = false;
    renderGraph();
  }, 16);
}

Then:

cutoffSlider.addEventListener("input", function () {
  cutoff = Number(this.value);
  requestGraphRender();
});

This limits graph rebuilding to approximately 60 updates per second.

Immediate renders are still appropriate for:

  • initial boot
  • transport play
  • transport stop
  • important routing changes
  • explicit graph activation

20. Do Not Build Audio Envelopes in JavaScript

Avoid patterns such as:

let envelopeValue = 0;

setInterval(function () {
  envelopeValue += 0.01;
  renderGraph();
}, 10);

followed by:

wax.const({
  key: "amp",
  value: envelopeValue
});

This makes the audio dependent on JavaScript timing.

Instead:

const envelope = wax.adsr(
  attack,
  decay,
  sustain,
  release,
  gate
);

The envelope then runs natively.

General rule:

JavaScript chooses musical parameters. The native graph performs audio-rate behavior.


21. Native Scope and Metering

For native devices, meter and scope information should originate from the native graph rather than a separate browser audio analyzer.

Use native scope nodes where appropriate.

Native events can then update your interface.

The important distinction remains:

Native audio graph
      ↓
meter/scope data
      ↓
JavaScript visualization

The interface observes the graph.

It should not become part of the graph’s audio timing.


22. Common Failure Modes

Works with the editor open, stops when closed

Usually caused by:

  • JavaScript-driven envelopes
  • JavaScript MIDI gates
  • requestAnimationFrame
  • browser timers controlling musical timing

Move that behavior into the native graph.


Page loads but there is no audio

The graph may have emitted before WAX finished resetting the previous page.

Use deferred boot pokes and check:

wax.didEmit()

Interface keyboard does nothing

DAW MIDI may still work because it reaches midinotein() directly.

For UI keyboard notes, use:

await wax.midi.ready();
wax.midi.noteOn(...);
wax.midi.noteOff(...);

Preset looks correct but sounds wrong

Your recall code probably updated HTML controls but not the JavaScript variables used by renderGraph().

Restore both.


Device is silent after loading a DAW session

Make sure session recall returns to the actual device page.

If WAX restores a Project hub page that does not emit a graph, there is no native device to process audio.


First graph works sometimes

You are probably emitting during the navigation reset window.

Use deferred boot attempts.


23. Recommended Native Device Structure

A production page can follow this structure:

const APP_NAME = "my-wax-device";

const wax = WaxNative.create();

let cutoff = 1200;
let resonance = 0.8;
let gain = 0.85;

function buildGraph() {
  const input = wax.in({
    channel: 0
  });

  const filtered = wax.svf(
    { mode: "lowpass" },

    wax.const({
      key: "cutoff",
      value: cutoff
    }),

    wax.const({
      key: "resonance",
      value: resonance
    }),

    input
  );

  return wax.mul(
    wax.const({
      key: "gain",
      value: gain
    }),

    filtered
  );
}

function renderGraph() {
  const output = buildGraph();

  wax.render(
    output,
    output
  );
}

function ensureEmit() {
  if (wax.didEmit()) {
    return true;
  }

  if (typeof wax.reemitLast === "function") {
    wax.reemitLast("retry");
  }

  return wax.didEmit();
}

function poke() {
  WaxNative.keepAlive();

  renderGraph();

  if (!wax.didEmit()) {
    setTimeout(function () {
      renderGraph();
      ensureEmit();
    }, 40);
  }
}

[
  50,
  150,
  400,
  800,
  1500,
  2500,
  4000
].forEach(function (ms) {
  setTimeout(poke, ms);
});

setTimeout(initDataTree, 600);

This gives you a strong starting point for WAX Native effects.


24. Recommended Native Instrument Architecture

A native instrument should generally look like:

DAW MIDI
   ↓
midinotein
   ↓
midinoteallocate
   ↓
midinoteunpack
   ↓
gate + frequency + velocity
   ↓
native oscillator
   ↓
native ADSR
   ↓
filter
   ↓
voice mix
   ↓
wax.render(L, R)

The interface controls:

Oscillator settings
Filter cutoff
Resonance
Attack
Decay
Sustain
Release
Voice count
Effects
Preset state

The native graph controls:

Audio-rate oscillator generation
MIDI note handling
Voice gates
Envelope timing
Audio filtering
Signal mixing

That separation is what allows the instrument to behave like a conventional DAW plugin.


25. WAX Native API Quick Reference

Create a graph:

const wax = WaxNative.create();

Check for WAX:

WaxNative.hasBridge();

Keep the host integration active:

WaxNative.keepAlive();

Host input:

wax.in({ channel: 0 });

Render:

wax.render(left, right);

Check emit:

wax.didEmit();

Retry:

wax.reemitLast("retry");

Host information:

wax.host.outputChannels();
wax.host.inputChannels();
wax.host.blockSize();
wax.host.sampleRate();

UI MIDI:

await wax.midi.ready();

wax.midi.noteOn(60, 100);
wax.midi.noteOff(60);

Native MIDI graph:

wax.midinotein();

Send a WAX side event:

WaxNative.emitEvent(
  "waxNativeRequestParamSync",
  {}
);

26. Before Shipping

  • Build the graph using WaxNative.create()
  • Emit using wax.render()
  • Use stable parameter keys
  • Explicitly choose mono, dual mono, or stereo output
  • Use wax.midinotein() for DAW-driven instruments
  • Use wax.midi.noteOn() and noteOff() for on-screen keys
  • Put envelopes and musical timing inside the native graph
  • Never use requestAnimationFrame for audio-critical work
  • Use WaxNative.keepAlive()
  • Defer top-level Project boot
  • Check wax.didEmit()
  • Retry with wax.reemitLast() when necessary
  • Give every device a unique DataTree appName
  • Restore both DOM controls and JavaScript parameter variables
  • Re-render after DataTree recall
  • Test session recall with the editor closed
  • Test DAW MIDI with the editor closed
  • Test transport without opening the plugin window
  • Confirm WAX restores the actual device URL rather than a non-audio Project hub

The Core WAX Native Rule

The easiest way to design WAX Native devices correctly is to keep one rule in mind:

The page defines the device. The plugin runs the device.

JavaScript should handle the interface, parameter changes, presets, and high-level control.

The native graph should handle anything that must remain musically reliable:

  • audio generation
  • audio processing
  • MIDI voices
  • envelopes
  • gates
  • timing
  • signal routing

If a device only works while its editor is visible, too much of the instrument probably still lives in JavaScript.

A properly designed WAX Native device should load with the DAW session, rebuild its graph, restore its state, receive host MIDI and transport, and continue processing audio whether the editor is open or not.

Was this article helpful?
Dislike