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()andnoteOff()for on-screen keys - Put envelopes and musical timing inside the native graph
- Never use
requestAnimationFramefor 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.