Introduction
WaxNative is for creating apps capable of running inside WAX’s internal audio engine, synced to the host at native sample rate for lower latency and better performance. A WaxNative device is just an ordinary HTML page that builds its sound through the helper objects WAX supplies, instead of the browser’s own Web Audio API. The engine keeps running while a WaxNative page is loaded. Loading a normal Web Audio page disconnects it.
Page flow
script tag or WAX inject
wax = await WaxNative.create()renderDevice()wire
wax.* → wax.render(L, R?)renderDevice() again1. Overview
Helper
Everything centers on the helper, wax-native.js that gives you a global WaxNative object. From there you build sound as a chain of nodes similar to the Web Audio API (wax.cycle for an oscillator, wax.svf for a filter, etc.). When the chain is ready, render it to the engine with wax.render(left, right), and the engine keeps playing it until you render changes.
Nodes
WaxNative devices are made up of small audio blocks very similar to the standard Web Audio API — oscillators, filters, gains, MIDI inputs, and so on. In code you create them with wax methods (wax.cycle, wax.svf, wax.mul, …) and plug the outputs of one into the inputs of the next until you reach wax.render.
Every supported method is listed in Reference (same names as WEB.md).
2. Get started
Chapter 2 — Get started · minimal-tone demo
Load the helper
wax-native.js is always present in the plugin. Add the tag manually to support running outside the plugin (Chrome, Safari, etc.):
<script src="https://szfpro.github.io/CodeEditorHTML/wax-native.js"></script>
Create the instance
One wax instance per page. Run once on load:
wax = await WaxNative.create();
Render Node Chain
Build the chain in a dedicated function — for example renderDevice() — and commit with wax.render. Putting the chain in a dedicated function is good practice when params, presets, or other state need to update what the engine plays.
function renderDevice() { const tone = wax.mul(0.15, wax.cycle(440)); wax.render(tone, tone); }
UI and parameters
Store knob values in params and read them in renderDevice() instead of fixed numbers. Wire controls through setParam.
Sliders fire input very often while you drag. Calling renderDevice() on every event rebuilds the graph too fast and can stutter. Wait about 16 ms between commits (scheduleRender), then call flushRender() on change when the user releases the control so the final value is exact. Boot and one-shot updates can still call renderDevice() immediately. More under Sliders stutter. MIDI notes are in MIDI; host CC in Automation.
const params = { gain: 0.15, frequency: 440 }; let renderTimer = null; function scheduleRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = setTimeout(function () { renderTimer = null; renderDevice(); }, 16); } function flushRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = null; renderDevice(); } function setParam(name, value) { params[name] = value; scheduleRender(); } gainSlider.addEventListener("input", function () { setParam("gain", Number(gainSlider.value)); }); gainSlider.addEventListener("change", flushRender);
Starter page
Minimal full file: load helper, one script (create → renderDevice → slider wiring), then markup. Project / hub URLs: Troubleshooting. Host CC wiring is in the Automation starter; note-driven synths in the MIDI starter.
<script src="https://szfpro.github.io/CodeEditorHTML/wax-native.js"></script> <script> let wax = null; async function init() { wax = await WaxNative.create(); renderDevice(); } function renderDevice() { const tone = wax.mul(params.gain, wax.cycle(params.frequency)); wax.render(tone, tone); } const params = { gain: 0.15, frequency: 440 }; let renderTimer = null; function scheduleRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = setTimeout(function () { renderTimer = null; renderDevice(); }, 16); } function flushRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = null; renderDevice(); } function setParam(name, value) { params[name] = value; scheduleRender(); } document.addEventListener("DOMContentLoaded", function () { const gainSlider = document.getElementById("gain"); const freqSlider = document.getElementById("frequency"); gainSlider.addEventListener("input", function () { setParam("gain", Number(gainSlider.value)); }); gainSlider.addEventListener("change", flushRender); freqSlider.addEventListener("input", function () { setParam("frequency", Number(freqSlider.value)); }); freqSlider.addEventListener("change", flushRender); init(); }); </script> <label>Gain <input id="gain" type="range" min="0" max="1" step="0.01" value="0.15" /> </label> <label>Frequency (Hz) <input id="frequency" type="range" min="50" max="2000" step="1" value="440" /> </label>
Try it — starter oscillator
3. MIDI
Chapter 3 — MIDI · poly-synth demo
MIDI path
WaxNative handles MIDI notes from the DAW and your on-screen keyboard through the same input, wax.midinotein(). Both sources use the same voice allocation and sound generation code. The following steps show how to assign incoming notes to voices and use their pitch and velocity to drive an oscillator and envelope.
await wax.midi.ready()wax.midi.noteOn() / noteOff()wax.midinotein()midinoteallocatepitch + velocity (
midinoteunpack)MIDI voices (step by step)
Host and UI MIDI share one path: collect notes, assign voices, read pitch and level, then envelope and oscillator. Steps:
midinotein()— note stream from the host,wax.midi, or internal event.midinoteallocate({ voices: N }, midiIn)— polyphony: assign each note-on to one ofNvoices.midinoteunpack({ channel: 0 }, voices)— per voice, pitch (frequency) and strength (velocity).trigger— fromwax.ge(velocity, …): start/stop event for the ADSR. 1 while key down, 0 when up.wax.latch(trigger, frequency)— memory node. Whentriggerturns on, holds that note’sfrequencyfor the osc through release (frequencyjumps to 0 on release).wax.adsr(…, trigger)— amplitude envelope. The sametriggerstarts attack and ends sustain, then runs for release.
const midiIn = wax.midinotein(); // host / wax.midi / internal events const voices = wax.midinoteallocate({ voices: 8 }, midiIn); // assign note-ons to 8 slots const [frequency, velocity] = wax.midinoteunpack({ channel: 0 }, voices); // pitch + level (voice 0) const trigger = wax.ge(velocity, wax.const({ key: "triggerEps", value: 0.001 })); // 1 while key down, 0 default const oscFreq = wax.latch(trigger, frequency); // memory node: last note-on Hz for release const ampEnv = wax.adsr(attack, decay, sustain, release, trigger); // envelope follows trigger const tone = wax.mul(ampEnv, wax.mul(velocity, wax.cycle(oscFreq)));
On-screen keyboard
await wax.midi.ready(); wax.midi.noteOn(60, 100); wax.midi.noteOff(60);
Control change (CC)
After await wax.midi.ready(), wax.midi.access().inputs deliver ordinary MIDI bytes from the host — not only notes. Control Change uses status 0xB0 (channel in the low nibble), then controller number and value (**0–127** each). That stream is separate from midinotein(): CC does not become note voices; you handle it in JavaScript. Audio still updates only when you call setParam (or use wax.param in the graph).
Receive control change
Listen on each input and ignore non-CC status bytes. Map a controller into the same setParam / params as Get started — that rebuilds the graph (e.g. CC 1 → gain).
wax.midi.ready().then(function () { const access = wax.midi.access(); if (!access) return; access.inputs.forEach(function (input) { input.onmidimessage = function (e) { if ((e.data[0] & 0xf0) !== 0xb0) return; // not Control Change const controller = e.data[1]; const value = e.data[2]; // 0–127 if (controller === 1) setParam("gain", value / 127); }; }); });
Send UI changes to the host
When the user moves a slider, update the local parameter and send the corresponding CC value to the host. wax.midi.cc takes a controller number from 1–32 for these host controls and a MIDI value from 0–127.
Register this listener after MIDI is ready. It replaces the local-only slider listener from a minimal starter.
wax.midi.ready().then(function () { slider.addEventListener("input", function () { if (hostCcFromHost) return; const gain = Number(slider.value); setParam("gain", gain); wax.midi.cc(1, Math.round(gain * 127)); }); });
setParam updates the graph; wax.midi.cc tells the host the parameter moved. Use a hostCcFromHost flag when applying incoming host/CC updates so you do not echo CC back — see Host automation.
Starter page
Monophonic synth from the voice chain above, plus Control change: CC in → setParam, sliders out → wax.midi.cc.
<script src="https://szfpro.github.io/CodeEditorHTML/wax-native.js"></script> <script> // One wax instance for the whole page (plugin native or browser hybrid). let wax = null; async function init() { wax = await WaxNative.create(); renderDevice(); await wax.midi.ready(); // notes (wax.midi.noteOn) and CC (access.inputs) wireCc(); } // Build the note-driven chain — call again whenever params change. function renderDevice() { const midiIn = wax.midinotein(); const voices = wax.midinoteallocate({ voices: 1 }, midiIn); const [frequency, velocity] = wax.midinoteunpack({ channel: 0 }, voices); const trigger = wax.ge(velocity, wax.const({ key: "triggerEps", value: 0.001 })); const oscFreq = wax.latch(trigger, frequency); const env = wax.adsr(params.attack, 0.1, 1, params.release, trigger); const tone = wax.mul(env, wax.mul(velocity, wax.cycle(oscFreq))); const out = wax.mul(params.master, tone); wax.render(out, out); } const params = { master: 0.2, attack: 0.02, release: 0.35 }; let renderTimer = null; function scheduleRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = setTimeout(function () { renderTimer = null; renderDevice(); }, 16); } function flushRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = null; renderDevice(); } function setParam(name, value) { params[name] = value; scheduleRender(); } let masterSlider = null; let attackSlider = null; let releaseSlider = null; let hostCcFromHost = false; // CC 1–3 → params (0–127 MIDI → slider ranges below). function applyCc(controller, midiValue) { hostCcFromHost = true; const t = midiValue / 127; if (controller === 1) { params.master = t * 0.5; if (masterSlider) masterSlider.value = String(params.master); } if (controller === 2) { params.attack = 0.005 + t * (0.5 - 0.005); if (attackSlider) attackSlider.value = String(params.attack); } if (controller === 3) { params.release = 0.05 + t * (1.5 - 0.05); if (releaseSlider) releaseSlider.value = String(params.release); } hostCcFromHost = false; flushRender(); } function wireCc() { const access = wax.midi.access(); if (!access) return; access.inputs.forEach(function (input) { input.onmidimessage = function (e) { if ((e.data[0] & 0xf0) !== 0xb0) return; applyCc(e.data[1], e.data[2]); }; }); } // Wire sliders, then boot. document.addEventListener("DOMContentLoaded", function () { masterSlider = document.getElementById("master"); attackSlider = document.getElementById("attack"); releaseSlider = document.getElementById("release"); masterSlider.addEventListener("input", function () { if (hostCcFromHost) return; const v = Number(masterSlider.value); setParam("master", v); wax.midi.cc(1, Math.round((v / 0.5) * 127)); }); masterSlider.addEventListener("change", function () { if (hostCcFromHost) return; flushRender(); }); attackSlider.addEventListener("input", function () { if (hostCcFromHost) return; const v = Number(attackSlider.value); setParam("attack", v); wax.midi.cc(2, Math.round(((v - 0.005) / (0.5 - 0.005)) * 127)); }); attackSlider.addEventListener("change", function () { if (hostCcFromHost) return; flushRender(); }); releaseSlider.addEventListener("input", function () { if (hostCcFromHost) return; const v = Number(releaseSlider.value); setParam("release", v); wax.midi.cc(3, Math.round(((v - 0.05) / (1.5 - 0.05)) * 127)); }); releaseSlider.addEventListener("change", function () { if (hostCcFromHost) return; flushRender(); }); init(); }); </script> <label>Master <input id="master" type="range" min="0" max="0.5" step="0.01" value="0.2" /> </label> <label>Attack (s) <input id="attack" type="range" min="0.005" max="0.5" step="0.005" value="0.02" /> </label> <label>Release (s) <input id="release" type="range" min="0.05" max="1.5" step="0.01" value="0.35" /> </label>
Try it — mini synth (MIDI → ADSR)
4. Automation
Connect the DAW’s host parameter slots to your on-screen sliders and params so automation, project recall, and user moves all update the same audio.
Host automation
Host automation lets the DAW control your device’s parameters through cc1–cc32. Map these controls to the same params used by your UI so automation updates both the audio and the displayed values.
Walk through sync, send, receive, and range mapping below. Uses params / setParam from Get started.
Sync values on load
When the device loads inside the plugin, request the host’s current parameter values. This initializes the device and its controls from the host’s state, including values restored from a saved project.
Define a handler, in this case hostUpdate, that calls setParam to update the audio and assigns the slider’s value to update the UI. Register it when WaxNative.hasBridge() returns true, then request the initial sync. The response contains cc1–cc32 values normalized to 0–1.
let hostCcFromHost = false; function hostUpdate(host) { hostCcFromHost = true; if (host.cc1 != null) { setParam("gain", host.cc1); slider.value = host.cc1; } hostCcFromHost = false; } if (WaxNative.hasBridge()) { window.WAX._internal.receiveWaxNativeUpdate = hostUpdate; WaxNative.emitEvent("waxNativeRequestParamSync", {}); }
Send automation changes
When the user moves a slider, update the graph with setParam and notify the host with wax.midi.cc. Use controller numbers 1–32 (host parameter slots) and MIDI values 0–127. Wire listeners after await wax.midi.ready() — same MIDI API as Control change (CC).
wax.midi.ready().then(function () { gainSlider.addEventListener("input", function () { if (hostCcFromHost) return; setParam("gain", Number(gainSlider.value)); wax.midi.cc(1, Math.round(params.gain * 127)); }); });
setParam drives audio; wax.midi.cc updates host state and automation. Set hostCcFromHost while applying sync or incoming values so you do not echo CC back to the host.
Receive automation changes
The sync request retrieves the current values once. Subsequent automation changes arrive as MIDI CC messages, with values from 0–127. Live CC uses the same hostUpdate as sync; only the payload differs (MIDI bytes you normalize to 0–1 vs the bridge object from sync).
After MIDI is ready, listen for CC messages, normalize their values to 0–1, and pass them to hostUpdate. This keeps the parameter mapping and UI updates in one place.
wax.midi.ready().then(function () { const access = wax.midi.access(); if (!access) return; access.inputs.forEach(function (input) { input.onmidimessage = function (e) { if ((e.data[0] & 0xf0) !== 0xb0) return; const controller = e.data[1]; const value = e.data[2] / 127; hostUpdate({ ["cc" + controller]: value }); }; }); });
Map parameter ranges
For parameters with a different range, convert incoming normalized values into the parameter’s units and convert outgoing values back to MIDI’s 0–127 range.
For example, map cc2 to a frequency control spanning 50–2000 Hz:
// Inside hostUpdate: normalized host value → Hz. if (host.cc2 != null) { const frequency = 50 + host.cc2 * (2000 - 50); setParam("frequency", frequency); frequencySlider.value = frequency; } // Inside the frequency slider's input handler: Hz → MIDI CC. const normalized = (params.frequency - 50) / (2000 - 50); wax.midi.cc(2, Math.round(normalized * 127));
Use matching conversions in both directions so the host and UI represent the same value. To read host CC inside the graph with wax.param, see Reference — wax.param.
Wire it together
Define a method to bundle host-side wiring, in this case wireHost(), and call it from init() after WaxNative.create(). Put Sync values on load and the CC listener from Receive automation changes in that function — both call the same hostUpdate. Attach slider input handlers in DOM setup (Send automation changes). They call setParam and wax.midi.cc when the user moves a control.
function wireHost() { if (WaxNative.hasBridge()) { window.WAX._internal.receiveWaxNativeUpdate = hostUpdate; WaxNative.emitEvent("waxNativeRequestParamSync", {}); } wax.midi.ready().then(function () { const access = wax.midi.access(); if (!access) return; access.inputs.forEach(function (input) { input.onmidimessage = function (e) { if ((e.data[0] & 0xf0) !== 0xb0) return; hostUpdate({ ["cc" + e.data[1]]: e.data[2] / 127 }); }; }); }); } async function init() { wax = await WaxNative.create(); renderDevice(); wireHost(); } document.addEventListener("DOMContentLoaded", function () { // gainSlider.addEventListener("input", …) — see Send automation changes init(); });
Starter page
Same always-on oscillator as Get started, plus wireHost() so DAW automation and incoming CC stay in sync with the sliders. Project / hub URLs: Troubleshooting.
<script src="https://szfpro.github.io/CodeEditorHTML/wax-native.js"></script> <script> // One wax instance for the whole page (plugin native or browser hybrid). let wax = null; async function init() { wax = await WaxNative.create(); renderDevice(); wireHost(); // sync on load + listen for host CC (inside wireHost) } // Same always-on oscillator as Get started — rebuild when params change. function renderDevice() { const tone = wax.mul(params.gain, wax.cycle(params.frequency)); wax.render(tone, tone); } const params = { gain: 0.15, frequency: 440 }; let renderTimer = null; function scheduleRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = setTimeout(function () { renderTimer = null; renderDevice(); }, 16); } function flushRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = null; renderDevice(); } function setParam(name, value) { params[name] = value; scheduleRender(); } // Slider refs for hostUpdate; guard stops echo when host moves a knob. let gainSlider = null; let freqSlider = null; let hostCcFromHost = false; // Host → params + on-screen sliders (cc1–cc32 values are 0–1). function hostUpdate(host) { hostCcFromHost = true; if (host.cc1 != null) { params.gain = host.cc1; if (gainSlider) gainSlider.value = String(params.gain); } if (host.cc2 != null) { params.frequency = 50 + host.cc2 * (2000 - 50); // normalized → Hz if (freqSlider) freqSlider.value = String(params.frequency); } hostCcFromHost = false; flushRender(); } function wireHost() { // One-shot project/host state (see Sync values on load). if (WaxNative.hasBridge()) { window.WAX._internal.receiveWaxNativeUpdate = hostUpdate; WaxNative.emitEvent("waxNativeRequestParamSync", {}); } // Live automation as MIDI CC → same hostUpdate. wax.midi.ready().then(function () { const access = wax.midi.access(); // null in a plain browser if (!access) return; access.inputs.forEach(function (input) { input.onmidimessage = function (e) { if ((e.data[0] & 0xf0) !== 0xb0) return; hostUpdate({ ["cc" + e.data[1]]: e.data[2] / 127 }); }; }); }); } // Wire sliders, then boot. document.addEventListener("DOMContentLoaded", function () { gainSlider = document.getElementById("gain"); freqSlider = document.getElementById("frequency"); gainSlider.addEventListener("input", function () { if (hostCcFromHost) return; setParam("gain", Number(gainSlider.value)); if (wax && wax.midi) wax.midi.cc(1, Math.round(params.gain * 127)); }); gainSlider.addEventListener("change", function () { if (hostCcFromHost) return; flushRender(); }); freqSlider.addEventListener("input", function () { if (hostCcFromHost) return; setParam("frequency", Number(freqSlider.value)); if (wax && wax.midi) { const normalized = (params.frequency - 50) / (2000 - 50); wax.midi.cc(2, Math.round(normalized * 127)); } }); freqSlider.addEventListener("change", function () { if (hostCcFromHost) return; flushRender(); }); init(); }); </script> <label>Gain <input id="gain" type="range" min="0" max="1" step="0.01" value="0.15" /> </label> <label>Frequency (Hz) <input id="frequency" type="range" min="50" max="2000" step="1" value="440" /> </label>
Try it — oscillator + host CC
5. Presets
DataTree flow
Presets and session recall go through WAX’s DataTree storage. You choose the JSON snapshot; the host does not store a frozen copy of the WaxNative graph. Save only what renderDevice() reads (usually params plus any mode flags your graph needs).
collectState()DOM + JS vars → JSON
push(data, APP_NAME)WaxNative.create()pull(APP_NAME)applyState()params + HTML controlsrenderDevice() → wax.renderNot stored: the instruction batch from wax.render() — only your JSON.
- Save — debounced
pushafter user edits;onPullbefore host preset save. - Recall — first emit, then
pulland apply whenwax.didEmit()is true (see Boot order). - Sound — graph always follows JS state; recall must update the same variables
renderDevice()reads.
What gets saved
DataTree stores your JSON settings, not the instruction batch from wax.render().
Put musically meaningful knobs and modes under params:
{
"params": {
"gain": 0.15,
"frequency": 440,
"cutoff": 1200,
"resonance": 0.8
}
}
Do not store meter needles, scope buffers, transport position, or animation frames unless you explicitly want them recalled. Do not invent DataTree.subscribe or private host APIs — use window.WAX_DataTree (see below).
appName
Every push and pull needs a stable, unique string per device page. Two pages sharing the same name overwrite each other’s snapshots.
const APP_NAME = "my-wax-synth";
Pick one ID and keep it for the life of the project (renaming breaks recall for old sessions).
Save and load
The host injects window.WAX_DataTree. Typical methods:
push(data, APP_NAME)— send your JSON to the host (debounce ~250 ms after user changes).pull(APP_NAME)— returns a Promise with the last saved blob (on boot / recall).onPull(handler)— host asks for fresh state before save; callpush(collectState(), APP_NAME)inside the handler.
Apply must touch params. If renderDevice() reads JavaScript variables, updating only the range inputs leaves recall looking correct but sounding wrong. Mirror Get started: write DOM and params, then renderDevice() (or flushRender() if you debounce).
function collectState() { return { params: { gain: params.gain, frequency: params.frequency } }; } function applyState(raw) { const p = raw && raw.params ? raw.params : raw; if (!p || typeof p !== "object") return; if (p.gain != null) params.gain = Number(p.gain); if (p.frequency != null) params.frequency = Number(p.frequency); if (gainSlider) gainSlider.value = String(params.gain); if (freqSlider) freqSlider.value = String(params.frequency); renderDevice(); } let pushTimer = null; function pushSoon() { if (pushTimer) clearTimeout(pushTimer); pushTimer = setTimeout(function () { pushTimer = null; if (window.WAX_DataTree && window.WAX_DataTree.push) { window.WAX_DataTree.push(collectState(), APP_NAME); } }, 250); }
Call pushSoon() from slider change handlers (not every input tick unless you debounce). Register onPull once during init so host preset save stays in sync.
Boot order
Recall flow: host restores URL + cached DataTree → page creates wax → first renderDevice() → pull → apply → render again.
Do not run heavy apply before the bridge accepts the first emit. If apply races boot, you can get Emit: failed and silence (see Poke & didEmit).
- Wire HTML defaults and call
renderDevice()afterWaxNative.create(). - Defer DataTree setup — typically
setTimeout(initDataTree, 600), not immediately at parse time. - On
pull, callapplyStateonly whenwax.didEmit()is true; if not, retry apply once after ~150 ms.
function pullAndApply() { if (!window.WAX_DataTree || !window.WAX_DataTree.pull) return; window.WAX_DataTree.pull(APP_NAME).then(function (raw) { if (!raw) return; if (!wax.didEmit()) { setTimeout(function () { if (wax.didEmit()) applyState(raw); }, 150); return; } applyState(raw); }); } function initDataTree() { const dt = window.WAX_DataTree; if (!dt) return; if (typeof dt.onPull === "function") { dt.onPull(function () { dt.push(collectState(), APP_NAME); }); } pullAndApply(); } async function init() { wax = await WaxNative.create(); renderDevice(); setTimeout(initDataTree, 600); }
Example script
Minimal preset wiring on the always-on oscillator from Get started: collect / apply / debounced push, deferred init, gated pull.
<script src="https://szfpro.github.io/CodeEditorHTML/wax-native.js"></script> <script> const APP_NAME = "wax-doc-starter-presets"; let wax = null; const params = { gain: 0.15, frequency: 440 }; function collectState() { return { params: { gain: params.gain, frequency: params.frequency } }; } function applyState(raw) { const p = raw && raw.params ? raw.params : raw; if (!p || typeof p !== "object") return; if (p.gain != null) params.gain = Number(p.gain); if (p.frequency != null) params.frequency = Number(p.frequency); const gainSlider = document.getElementById("gain"); const freqSlider = document.getElementById("frequency"); if (gainSlider) gainSlider.value = String(params.gain); if (freqSlider) freqSlider.value = String(params.frequency); renderDevice(); } let pushTimer = null; function pushSoon() { if (pushTimer) clearTimeout(pushTimer); pushTimer = setTimeout(function () { pushTimer = null; if (window.WAX_DataTree && window.WAX_DataTree.push) { window.WAX_DataTree.push(collectState(), APP_NAME); } }, 250); } function pullAndApply() { const dt = window.WAX_DataTree; if (!dt || !dt.pull) return; dt.pull(APP_NAME).then(function (raw) { if (!raw) return; if (!wax.didEmit()) { setTimeout(function () { if (wax.didEmit()) applyState(raw); }, 150); return; } applyState(raw); }); } function initDataTree() { const dt = window.WAX_DataTree; if (!dt) return; if (typeof dt.onPull === "function") { dt.onPull(function () { dt.push(collectState(), APP_NAME); }); } pullAndApply(); } function renderDevice() { const tone = wax.mul(params.gain, wax.cycle(params.frequency)); wax.render(tone, tone); } async function init() { wax = await WaxNative.create(); renderDevice(); setTimeout(initDataTree, 600); } document.addEventListener("DOMContentLoaded", function () { const gainSlider = document.getElementById("gain"); const freqSlider = document.getElementById("frequency"); gainSlider.addEventListener("input", function () { params.gain = Number(gainSlider.value); }); gainSlider.addEventListener("change", function () { renderDevice(); pushSoon(); }); freqSlider.addEventListener("input", function () { params.frequency = Number(freqSlider.value); }); freqSlider.addEventListener("change", function () { renderDevice(); pushSoon(); }); init(); }); </script> <label>Gain <input id="gain" type="range" min="0" max="1" step="0.01" value="0.15" /> </label> <label>Frequency (Hz) <input id="frequency" type="range" min="50" max="2000" step="1" value="440" /> </label>
Try it — oscillator + DataTree push / pull
6. Instruments — audio & MIDI
Chapter 6 — Instruments · poly-synth demo
An instrument makes sound when you play notes. MIDI showed how notes arrive; Automation how sliders talk to the host. Here you stack the audio blocks inside renderDevice(): turn each note into pitch and loudness, pick a waveform, optionally filter it, then wax.render.
Signal path
Build the node chain in this order:
- Notes —
midinoteinthrough allocate / unpack (see MIDI voices). - Envelope —
trigger,latch, andadsrso keys fade in and out smoothly. - Oscillator — a wave at
oscFreq, scaled by velocity and the envelope. - Tone shaping — often
wax.svf(lowpass cutoff). - Output — master gain, then
wax.render(left, right).
On-screen keys use wax.midi.noteOn / noteOff (Keyboard); they feed the same midinotein path as the DAW.
Notes, envelope, and level
Start every instrument’s renderDevice() from the note stream. One voice is enough for a lead synth; raise voices for polyphony.
const midiIn = wax.midinotein(); const voices = wax.midinoteallocate({ voices: 1 }, midiIn); const [frequency, velocity] = wax.midinoteunpack({ channel: 0 }, voices); const trigger = wax.ge(velocity, wax.const({ key: "triggerEps", value: 0.001 })); const oscFreq = wax.latch(trigger, frequency); const env = wax.adsr( wax.const({ key: "attack", value: params.attack }), wax.const({ key: "decay", value: 0.1 }), wax.const({ key: "sustain", value: 1 }), wax.const({ key: "release", value: params.release }), trigger, ); // oscFreq + env + velocity → oscillator section next
velocity is how hard the note was played (0 when no key). env is the amplitude shape over time. You will multiply both into the oscillator output.
Oscillators
The oscillator is a steady wave whose pitch comes from oscFreq (Hz). Pick a generator in JavaScript — a dropdown or buttons set params.wave, then call the matching wax.* node:
const params = { wave: "saw", attack: 0.02, release: 0.35, master: 0.2, cutoff: 2400, }; function pickOscillator(freq) { switch (params.wave) { case "sine": return wax.cycle(freq); case "square": return wax.square(freq); case "triangle": return wax.triangle(freq); case "saw": default: return wax.saw(freq); } } // Inside renderDevice(), after the envelope block above: const raw = pickOscillator(oscFreq); const tone = wax.mul(env, wax.mul(velocity, raw));
Changing wave re-renders the graph — same as any other knob: update params.wave and call renderDevice().
| Sound | Node | Notes |
|---|---|---|
| Sine | cycle | Smooth, soft |
| Saw | saw | Bright, common for synths |
| Square | square | Hollow, retro |
| Triangle | triangle | Mellow |
| Anti-aliased saw / square / triangle | blepsaw, blepsquare, bleptriangle | Cleaner when pitch bends |
| Noise | noise, pinknoise | Drums / texture — often not tied to oscFreq |
Tone shaping
A state-variable filter tames brightness. Store cutoff in params and pass it with wax.const each render — same pattern as attack / master in the blocks above.
const filtered = wax.svf( { mode: "lowpass" }, wax.const({ key: "cutoff", value: params.cutoff }), wax.const({ key: "resonance", value: 0.7 }), tone, ); const out = wax.mul(wax.const({ key: "master", value: params.master }), filtered); wax.render(out, out);
More filter and dynamics examples live under Effects.
Host parameters
A parameter controls audio from the page UI or from the DAW. Read more in Host automation.
In this instrument, parameters are:
attack,release— envelope times (seconds)master— output levelcutoff— lowpass frequency (Hz)wave— oscillator shape (pickOscillator)voices— polyphony (1–8)
The example below maps host CC to every parameter: cc1 cutoff, cc2 master, cc3 attack, cc4 release, cc5 wave (same pattern as the Automation starter).
Example script
Full synth (1–8 voices): allocate → envelope → pickOscillator → SVF → mix → wax.render.
let wax = null; const params = { wave: "saw", master: 0.2, cutoff: 2400, attack: 0.02, release: 0.35, voices: 1, }; const WAVES = ["sine", "square", "triangle", "saw"]; let cutoffSlider = null; let masterSlider = null; let attackSlider = null; let releaseSlider = null; let waveSelect = null; let voicesInput = null; let hostCcFromHost = false; function pickOscillator(freq) { switch (params.wave) { case "sine": return wax.cycle(freq); case "square": return wax.square(freq); case "triangle": return wax.triangle(freq); case "saw": default: return wax.saw(freq); } } let renderTimer = null; function scheduleRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = setTimeout(function () { renderTimer = null; renderDevice(); }, 16); } function flushRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = null; renderDevice(); } function setParam(name, value) { params[name] = value; scheduleRender(); } function hostUpdate(host) { hostCcFromHost = true; if (host.cc1 != null) { params.cutoff = 200 + host.cc1 * (8000 - 200); if (cutoffSlider) cutoffSlider.value = String(params.cutoff); } if (host.cc2 != null) { params.master = host.cc2 * 0.5; if (masterSlider) masterSlider.value = String(params.master); } if (host.cc3 != null) { params.attack = 0.005 + host.cc3 * (0.5 - 0.005); if (attackSlider) attackSlider.value = String(params.attack); } if (host.cc4 != null) { params.release = 0.05 + host.cc4 * (1.5 - 0.05); if (releaseSlider) releaseSlider.value = String(params.release); } if (host.cc5 != null) { params.wave = WAVES[Math.min(3, Math.floor(host.cc5 * 4))]; if (waveSelect) waveSelect.value = params.wave; } hostCcFromHost = false; flushRender(); } function wireHost() { if (WaxNative.hasBridge()) { window.WAX._internal.receiveWaxNativeUpdate = hostUpdate; WaxNative.emitEvent("waxNativeRequestParamSync", {}); } wax.midi.ready().then(function () { const access = wax.midi.access(); if (!access) return; access.inputs.forEach(function (input) { input.onmidimessage = function (e) { if ((e.data[0] & 0xf0) !== 0xb0) return; hostUpdate({ ["cc" + e.data[1]]: e.data[2] / 127 }); }; }); }); } function renderDevice() { const midiIn = wax.midinotein(); const allocated = wax.midinoteallocate({ voices: params.voices }, midiIn); let mix = null; for (let i = 0; i < params.voices; i++) { const [frequency, velocity] = wax.midinoteunpack({ channel: i }, allocated); const trigger = wax.ge( velocity, wax.const({ key: "triggerEps" + i, value: 0.001 }), ); const oscFreq = wax.latch(trigger, frequency); const env = wax.adsr( wax.const({ key: "attack", value: params.attack }), wax.const({ key: "decay", value: 0.1 }), wax.const({ key: "sustain", value: 1 }), wax.const({ key: "release", value: params.release }), trigger, ); const tone = wax.mul(env, wax.mul(velocity, pickOscillator(oscFreq))); const filtered = wax.svf( { mode: "lowpass" }, wax.const({ key: "cutoff", value: params.cutoff }), wax.const({ key: "resonance", value: 0.7 }), tone, ); mix = mix ? wax.add(mix, filtered) : filtered; } const out = wax.mul(wax.const({ key: "master", value: params.master }), mix); wax.render(out, out); } async function init() { wax = await WaxNative.create(); renderDevice(); wireHost(); await wax.midi.ready(); } document.addEventListener("DOMContentLoaded", function () { cutoffSlider = document.getElementById("cutoff"); masterSlider = document.getElementById("master"); attackSlider = document.getElementById("attack"); releaseSlider = document.getElementById("release"); waveSelect = document.getElementById("wave"); voicesInput = document.getElementById("voices"); voicesInput.addEventListener("change", function () { const n = Math.min(8, Math.max(1, Number(voicesInput.value) | 0)); voicesInput.value = String(n); params.voices = n; flushRender(); }); cutoffSlider.addEventListener("input", function () { if (hostCcFromHost) return; setParam("cutoff", Number(cutoffSlider.value)); if (wax && wax.midi) { const n = (params.cutoff - 200) / (8000 - 200); wax.midi.cc(1, Math.round(n * 127)); } }); cutoffSlider.addEventListener("change", function () { if (hostCcFromHost) return; flushRender(); }); masterSlider.addEventListener("input", function () { if (hostCcFromHost) return; setParam("master", Number(masterSlider.value)); if (wax && wax.midi) wax.midi.cc(2, Math.round((params.master / 0.5) * 127)); }); masterSlider.addEventListener("change", function () { if (hostCcFromHost) return; flushRender(); }); attackSlider.addEventListener("input", function () { if (hostCcFromHost) return; setParam("attack", Number(attackSlider.value)); if (wax && wax.midi) { wax.midi.cc(3, Math.round(((params.attack - 0.005) / (0.5 - 0.005)) * 127)); } }); attackSlider.addEventListener("change", function () { if (hostCcFromHost) return; flushRender(); }); releaseSlider.addEventListener("input", function () { if (hostCcFromHost) return; setParam("release", Number(releaseSlider.value)); if (wax && wax.midi) { wax.midi.cc(4, Math.round(((params.release - 0.05) / (1.5 - 0.05)) * 127)); } }); releaseSlider.addEventListener("change", function () { if (hostCcFromHost) return; flushRender(); }); waveSelect.addEventListener("change", function () { if (hostCcFromHost) return; setParam("wave", waveSelect.value); if (wax && wax.midi) { const idx = WAVES.indexOf(params.wave); wax.midi.cc(5, Math.round((idx < 0 ? 0 : idx) / 3 * 127)); } flushRender(); }); init(); });
<label>Voices <input id="voices" type="number" min="1" max="8" step="1" value="1" /> </label>
Try it — mini synth (MIDI → ADSR → filter)
7. Effects
Chapter 7 — Effects · svf-lowpass demo
An effect processes audio that is already on the track. The only structural difference from an instrument is where the signal starts: wax.in instead of an oscillator.
Track input
On a track effect, incoming audio from the DAW enters the graph with wax.in({ channel: 0 }) (use channel 1 for the right side of a stereo input).
const input = wax.in({ channel: 0 });
Browser mic (preview only)
await wax.connectMic(); // after a user click — then wax.in hears the mic in Chrome
Filters and dynamics
wax.svf({ mode }, fc, q, x) — modes: lowpass, highpass, bandpass, notch, allpass. Also shelves, compress, delay, convolve.
Example effect
function renderDevice() { 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 ); const output = wax.mul(wax.const({ key: "gain", value: gain }), filtered); wax.render(output, output); }
Try it — lowpass on wax.in
8. Sequencing
Repeating patterns — arpeggios, step sequencers, timed gates — belong in the graph, not in JavaScript. Build the clock and seq2 once in renderDevice(); the engine keeps time after the editor closes. Notes and envelopes reuse the same blocks as Instruments; do not advance steps with setInterval.
Signal path
Wire the chain in this order:
- Clock —
train(ormetro) at step rate derived from BPM. - Steps —
seq2for pitch (hold) and gate (pulse) arrays, looped. - Envelope —
geon the gate →adsr(same idea as Notes, envelope, and level). - Oscillator —
cycleat the pitch signal fromseq2. - Output — master gain →
wax.render.
Clock and tempo
Convert host BPM to step rate in Hz (one step per quarter note at 4/4: bpm / 60). Drive train from a wax.const so transport can set rate to 0 on stop. Keep key strings stable; read params.playing and params.bpm inside renderDevice().
const stepHz = params.playing ? params.bpm / 60 : 0; const clock = wax.train(wax.const({ key: "stepHz", value: stepHz }));
Steps and gates
seq2 steps through an array of numbers, advancing one step on each clock pulse. Use two sequences driven by the same clock: one to choose the pitch, and another to trigger the envelope.
seq— the values for each step. For pitch, the example uses frequencies in Hz. For triggers, use1to play a step and0to skip it.hold: true— keep the current value until the next step. Use this for pitch so the oscillator keeps its frequency between clock pulses.hold: false— output a short pulse, then return to zero. Use this to trigger the envelope on each active step.loop: true— repeat the pattern after its last step.
Pass the clock after the options object, followed by the optional reset signal.
const reset = wax.const({ key: "seqReset", value: 0 }); const pitch = wax.seq2( { key: "pitch", seq: STEP_PITCHES, hold: true, loop: true }, clock, reset ); const gate = wax.seq2( { key: "gate", seq: [1, 0, 1, 0], hold: false, loop: true }, clock, reset ); const trigger = wax.ge(gate, wax.const({ key: "trigEps", value: 0.001 }));
Host transport
The DAW calls globals when transport or tempo changes. Update params, then call renderDevice() immediately — not on requestAnimationFrame. On stop, set params.playing to 0 so step rate goes to zero and the clock freezes.
Play and stop
window.WAX_Play = function () { params.playing = 1; renderDevice(); }; window.WAX_Stop = function () { params.playing = 0; renderDevice(); };
BPM
window.WAX_BPM = function (bpm) { params.bpm = bpm; renderDevice(); };
Wire transport
Assign the globals once after WaxNative.create(). In browser preview, also set params.playing from your Play button so the seq runs without the DAW.
function wireTransport() { window.WAX_Play = function () { params.playing = 1; flushRender(); }; window.WAX_Stop = function () { params.playing = 0; flushRender(); }; window.WAX_BPM = function (bpm) { params.bpm = bpm; flushRender(); }; }
Example script
const STEP_PITCHES = [261.63, 329.63, 392.0, 493.88]; const params = { bpm: 120, master: 0.2, playing: 0 }; let renderTimer = null; function scheduleRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = setTimeout(function () { renderTimer = null; renderDevice(); }, 16); } function flushRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = null; renderDevice(); } function setParam(name, value) { params[name] = value; scheduleRender(); } function renderDevice() { const stepHz = params.playing ? params.bpm / 60 : 0; const clock = wax.train(wax.const({ key: "stepHz", value: stepHz })); const reset = wax.const({ key: "seqReset", value: 0 }); const pitch = wax.seq2( { key: "pitch", seq: STEP_PITCHES, hold: true, loop: true }, clock, reset ); const gate = wax.seq2( { key: "gate", seq: [1, 0, 1, 0], hold: false, loop: true }, clock, reset ); const trigger = wax.ge(gate, wax.const({ key: "trigEps", value: 0.001 })); const env = wax.adsr( wax.const({ key: "attack", value: 0.005 }), wax.const({ key: "decay", value: 0.08 }), wax.const({ key: "sustain", value: 0.75 }), wax.const({ key: "release", value: 0.12 }), trigger ); const tone = wax.mul( wax.const({ key: "master", value: params.master }), wax.mul(env, wax.cycle(pitch)) ); wax.render(tone, tone); } function wireTransport() { window.WAX_Play = function () { params.playing = 1; flushRender(); }; window.WAX_Stop = function () { params.playing = 0; flushRender(); }; window.WAX_BPM = function (bpm) { params.bpm = bpm; flushRender(); }; } async function init() { wax = await WaxNative.create(); wireTransport(); renderDevice(); } document.addEventListener("DOMContentLoaded", function () { const bpmSlider = document.getElementById("bpm"); const masterSlider = document.getElementById("master"); bpmSlider.addEventListener("input", function () { setParam("bpm", Number(bpmSlider.value)); }); bpmSlider.addEventListener("change", flushRender); masterSlider.addEventListener("input", function () { setParam("master", Number(masterSlider.value)); }); masterSlider.addEventListener("change", flushRender); init(); });
<label>BPM <input id="bpm" type="range" min="60" max="180" step="1" value="120" /> </label> <label>Master <input id="master" type="range" min="0" max="0.5" step="0.01" value="0.2" /> </label>
Try it — four-step mono seq (train → seq2 → ADSR)
9. Scopes
Chapter 9 — Scopes · scope-visualizer
Three steps
Audio runs in the engine — you cannot read output samples from JavaScript directly. Tap the signal with a scope node; the host delivers blocks of samples to your page for meters and waveforms. You always do the same three things:
| Step | You | What happens |
|---|---|---|
| 1. Hook scope | wax.scope + wax.render | Engine measures the signal each audio block. |
| 2. Catch blocks | wireScopeEvents() → scopeSamples | Engine pushes the latest block; you copy it into a JS array. |
| 3. Draw | requestAnimationFrame + canvas | Paint from scopeSamples (the last block you were handed). |
Step 2 is not extra ceremony — it is “take scope values.” They arrive asynchronously (callback / message), not as a property on the graph node. Do not invent DOM events or DataTree hooks for meters; use the wiring below.
Step 1 — Hook scope (engine measures)
In renderDevice() (or whenever the graph changes), wrap the signal you want to view and render it like any output. Pick a unique name per tap; use the same name when filtering incoming blocks in step 2. size is samples per block; channels is how many arrays appear in payload.data.
const SCOPE_NAME = "main"; const SCOPE_SIZE = 512; let scopeSamples = null; // filled in step 2, read in step 3 function renderDevice() { const tone = wax.mul(params.gain, wax.cycle(params.frequency)); const out = wax.scope( { name: SCOPE_NAME, size: SCOPE_SIZE, channels: 1 }, tone ); wax.render(out, out); }
Step 2 — Catch blocks (engine hands you numbers)
Call wireScopeEvents() once at startup (same pattern as wireHost() in Automation). When a block arrives, handleScopePayload copies channel 0 into scopeSamples. Keep this handler fast — no canvas painting here.
function handleScopePayload(payload) { const data = typeof payload === "string" ? JSON.parse(payload) : payload; if (!data) return; const src = data.source || data.name || ""; if (src && src !== SCOPE_NAME) return; const ch0 = Array.isArray(data.data) ? data.data[0] : null; if (!ch0 || !ch0.length) return; scopeSamples = Float32Array.from(ch0); } function wireScopeEvents() { function onNativeEvent(name, payload) { if (name === "scope") handleScopePayload(payload); } if (WaxNative.hasBridge()) { window.WAX._internal.receiveWaxNativeEvent = onNativeEvent; } addEventListener("message", function (e) { if (e.data && e.data.type === "WAX_NativeEvent" && e.data.name === "scope") { handleScopePayload(e.data.payload); } }); }
Delivery. In the plugin, the host calls receiveWaxNativeEvent on your page. Preview iframes and hybrid Chrome get the same payload via postMessage — that is why both hooks live in wireScopeEvents(). Payload shape: { source, data: [Float32Array, …] }.
Step 3 — Draw (paint the last block)
Start a requestAnimationFrame loop that reads scopeSamples and strokes the canvas. If there is no data yet, clear the canvas and return; blocks will appear once audio is running and step 2 is wired.
function drawScope() { requestAnimationFrame(drawScope); const canvas = document.getElementById("scope"); const ctx = canvas.getContext("2d"); ctx.fillRect(0, 0, canvas.width, canvas.height); if (!scopeSamples || !scopeSamples.length) return; // stroke waveform from scopeSamples … }
Example script
Steps 1–3 together: oscillator, gain/frequency sliders (as in Get started), and a canvas. init() wires events, creates wax, renders, and starts the draw loop.
<canvas id="scope" width="320" height="80" style="width:100%;max-width:420px;height:80px;background:#0f1419"></canvas> <label>Gain <input id="gain" type="range" min="0" max="1" step="0.01" value="0.15" /> </label> <label>Frequency (Hz) <input id="frequency" type="range" min="50" max="2000" step="1" value="440" /> </label> <script src="https://szfpro.github.io/CodeEditorHTML/wax-native.js"></script> <script> const SCOPE_NAME = "main"; const SCOPE_SIZE = 512; let wax = null; const params = { gain: 0.15, frequency: 440 }; let scopeSamples = null; // Step 2 — stash each incoming block function handleScopePayload(payload) { try { const data = typeof payload === "string" ? JSON.parse(payload) : payload; if (!data) return; const src = data.source || data.name || ""; if (src && src !== SCOPE_NAME) return; const ch0 = Array.isArray(data.data) ? data.data[0] : null; if (!ch0 || !ch0.length) return; scopeSamples = Float32Array.from(ch0); } catch (_) {} } function wireScopeEvents() { function onNativeEvent(name, payload) { if (name === "scope") handleScopePayload(payload); } if (WaxNative.hasBridge()) { window.WAX._internal.receiveWaxNativeEvent = onNativeEvent; } addEventListener("message", function (e) { if (e.data && e.data.type === "WAX_NativeEvent" && e.data.name === "scope") { handleScopePayload(e.data.payload); } }); } // Step 1 — graph tap + render function renderDevice() { const tone = wax.mul(params.gain, wax.cycle(params.frequency)); const out = wax.scope({ name: SCOPE_NAME, size: SCOPE_SIZE, channels: 1 }, tone); wax.render(out, out); } // Step 3 — paint from scopeSamples function drawScope() { requestAnimationFrame(drawScope); const canvas = document.getElementById("scope"); if (!canvas) return; const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; ctx.fillStyle = "#0f1419"; ctx.fillRect(0, 0, w, h); if (!scopeSamples || !scopeSamples.length) return; ctx.strokeStyle = "#5eead4"; ctx.lineWidth = 1; ctx.beginPath(); for (let i = 0; i < scopeSamples.length; i++) { const x = (i / (scopeSamples.length - 1)) * w; const y = h * 0.5 - scopeSamples[i] * (h * 0.45); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } async function init() { wireScopeEvents(); // step 2 — before audio runs wax = await WaxNative.create(); renderDevice(); requestAnimationFrame(drawScope); } document.addEventListener("DOMContentLoaded", function () { const gainSlider = document.getElementById("gain"); const freqSlider = document.getElementById("frequency"); gainSlider.addEventListener("input", function () { params.gain = Number(gainSlider.value); renderDevice(); }); freqSlider.addEventListener("input", function () { params.frequency = Number(freqSlider.value); renderDevice(); }); init(); }); </script>
Try it — scoped oscillator waveform
10. Troubleshooting
Common issues
- Sound only when editor open — Use
adsr,midinotein, and engine sequencers — not JS timers for gates or envelopes. - No audio after load — On Project / hub URLs, poke
renderDevice()and checkwax.didEmit()(see below). - On-screen keyboard silent —
await wax.midi.ready(), thennoteOn/noteOff. - Preset wrong sound — Recall updated the slider but not
params(or the other way around). - Sliders stutter while dragging — Debounce
renderDevice()to about 16 ms oninput; call it once onchange. - Silent after session load — Session restored a hub URL, not your device page.
Project pages: poke & didEmit
When your device is opened through a hub or Project URL, WAX may reset the slot just after your script runs, which throws away your first commit — the page looks fine but is silent. On those URLs only, call WaxNative.keepAlive() once at the start of init() (keeps the page wired to the host) and add the helpers below.
async function init() { WaxNative.keepAlive(); wax = await WaxNative.create(); renderDevice(); scheduleLoadPokes(); }
Add ensureEmit() and call it at the end of renderDevice():
function ensureEmit() { if (!wax || !wax.didEmit) return true; if (wax.didEmit()) return true; if (typeof wax.reemitLast === "function") wax.reemitLast("retry"); return wax.didEmit(); }
At the end of renderDevice(), call ensureEmit(). Re-send the graph during the first seconds:
function poke() { WaxNative.keepAlive(); renderDevice(); if (!wax.didEmit()) { setTimeout(function () { renderDevice(); ensureEmit(); }, 40); } } function scheduleLoadPokes() { [50, 150, 400, 800, 1500, 2500, 4000].forEach(function (ms) { setTimeout(poke, ms); }); }
Before you ship
keepAlive(), pokes, anddidEmiton Project pages; stablewax.constkeys- Instruments:
midinotein+wax.midi; sequencers:train/seq2— notsetIntervalrebuilds - Test with editor closed, DAW MIDI, transport, session recall; unique DataTree
appName
Remember: WaxNative runs your audio in WAX’s internal engine.
11. Reference — all wax.* nodes
Index of helpers and every graph node. Full API with parameters →
WaxNative helpers & page API
| API | Purpose |
|---|---|
WaxNative.create() | One wax instance per page; Web Audio fallback in browser |
WaxNative.hasBridge() | true when the real plugin inject is present |
WaxNative.keepAlive() | Keep WebView wired on Project / hub URLs |
wax.render(outL, outR?) | Commit graph to the internal engine |
wax.didEmit() / reemitLast() | Confirm delivery; retry if first batch dropped |
wax.host.sampleRate() … | Input/output channels, block size for I/O layout |
wax.midi.ready(), noteOn, noteOff, cc | UI keyboard + host automation MIDI |
wax.connectMic() / resumeAudio() | Browser FX: mic and AudioContext unlock (no-op in plugin) |
WaxNative.emitEvent("waxNativeRequestParamSync", {}) | Pull host CC values into sliders |
WAX._internal.receiveWaxNativeEvent | Scope/meter blocks from engine → page |
window.WAX_Play / WAX_Stop / WAX_BPM | DAW transport hooks → re-render graph |
Graph nodes (wax.*)
Node index generated from @elemaudio/core@4.0.3 (bundled in wax-native.js). Same API in WAX and browser preview.
Host, time, and control
| API | Purpose |
|---|---|
wax.in({ channel: 0 }) | Host or browser input — FX entry point (`channel` index) |
wax.const({ key: "gain", value: 0.5 }) | Control value with stable `key` — knobs, automation, presets |
wax.param({ index: 1 }) | Host automation slot (`index` 1–32) as graph signal |
wax.sr(…) | Sample rate (Hz) as signal for graph math |
wax.time(…) | Elapsed time signal for modulation / sequencing |
Oscillators and noise
| API | Purpose |
|---|---|
wax.cycle(440) | Sine oscillator — frequency in Hz |
wax.saw(440) | Sawtooth oscillator |
wax.square(440) | Square wave oscillator |
wax.triangle(440) | Triangle wave oscillator |
wax.train(440) | Pulse / clock train at frequency |
wax.phasor(…) | 0→1 ramp phasor at rate |
wax.syncphasor(…) | Hard-synced phasor |
wax.blepsaw(…) | Band-limited saw (cleaner bends) |
wax.blepsquare(…) | Band-limited square |
wax.bleptriangle(…) | Band-limited triangle |
wax.noise(…) | White noise source |
wax.pinknoise(…) | Pink noise source |
Filters and tone
| API | Purpose |
|---|---|
wax.svf({ mode: "lowpass" }, fc, q, input) | State-variable filter (LP/HP/BP/notch/allpass modes) |
wax.svfshelf(…) | SVF-based shelf EQ |
wax.lowpass(…) | One-pole lowpass |
wax.highpass(…) | One-pole highpass |
wax.bandpass(…) | Bandpass filter |
wax.notch(…) | Notch filter |
wax.allpass(…) | Allpass filter |
wax.lowshelf(…) | Low shelf EQ |
wax.highshelf(…) | High shelf EQ |
wax.peak(…) | Peaking EQ band |
wax.biquad(…) | Generic biquad coefficient filter |
wax.pole(…) | Real pole smoothing |
wax.mm1p(…) | Moog-style one-pole |
wax.prewarp(…) | Bilinear prewarp for cutoff |
wax.smooth(…) | Smooth a control signal |
wax.sm(…) | Smooth (alias) |
wax.dcblock(…) | Remove DC offset |
wax.df11(…) | First-order delay / filter building block |
wax.zero(…) | Delay / zero-pole helper |
wax.pink(…) | Pinking filter on white noise |
Envelopes and dynamics
| API | Purpose |
|---|---|
wax.adsr(a, d, s, r, gate) | Amplitude envelope from gate/trigger |
wax.env(…) | Simple envelope follower |
wax.compress(…) | Dynamics compressor |
wax.skcompress(…) | Sidechain-aware compressor |
Delay, memory, and samples
| API | Purpose |
|---|---|
wax.delay(…) | Delay line — time in seconds |
wax.sdelay(…) | Sample-based delay |
wax.z(…) | Single-sample delay (unit delay) |
wax.tapIn(…) | Write delay line tap |
wax.tapOut(…) | Read delay line tap |
wax.sample(…) | Play audio sample from buffer |
wax.table(…) | Wavetable / lookup oscillator |
wax.convolve(…) | Impulse response convolution reverb |
Sequencing and logic
| API | Purpose |
|---|---|
wax.metro(…) | Metronome pulse at interval |
wax.seq(…) | Step sequencer (legacy) |
wax.seq2(…) | Multi-output step sequencer |
wax.sparseq(…) | Sparse pattern sequencer |
wax.sparseq2(…) | Sparse sequencer v2 |
wax.sampleseq(…) | Sample-triggering sequencer |
wax.sampleseq2(…) | Sample sequencer v2 |
wax.counter(…) | Increment while gate high |
wax.accum(…) | Accumulate while gate high |
wax.latch(…) | Hold value while gate high (note pitch memory) |
wax.once(…) | Fire once when trigger rises |
wax.maxhold(…) | Hold maximum of input |
wax.rand(…) | Random value on trigger |
MIDI in the graph
| API | Purpose |
|---|---|
wax.midinotein() | Merged host + UI MIDI note stream |
wax.midinoteallocate({ voices: 8 }, midiIn) | Assign note-ons to polyphony voices |
wax.midinoteunpack({ channel: 0 }, voices) | Per-voice pitch and velocity |
wax.midinoteshift(…) | Transpose MIDI note numbers |
Multichannel (wax.mc)
| API | Purpose |
|---|---|
wax.mc.sample(…) | Multichannel sample player |
wax.mc.table(…) | Multichannel wavetable |
wax.mc.sampleseq(…) | Multichannel sample sequence |
wax.mc.sampleseq2(…) | Multichannel sample sequence v2 |
wax.mc.capture(…) | Multichannel capture buffer |
Analysis and metering
| API | Purpose |
|---|---|
wax.scope({ name: "main", size: 512, channels: 1 }, signal) | Send audio blocks to page for meters/waveforms |
wax.meter(…) | Level metering tap |
wax.fft(…) | FFT analysis tap |
wax.snapshot(…) | Capture graph state snapshot |
wax.capture(…) | Record signal to buffer |
Signal utilities
| API | Purpose |
|---|---|
wax.select(…) | Switch between inputs by index |
wax.ms2samps(…) | Convert milliseconds to samples |
wax.tau2pole(…) | Time constant → filter pole |
wax.db2gain(…) | Decibels to linear gain |
wax.gain2db(…) | Linear gain to decibels |
wax.hann(…) | Hann window for overlap-add |
Math and comparisons
| API | Purpose |
|---|---|
wax.add(…) | Sum signals |
wax.sub(…) | Subtract signals |
wax.mul(…) | Multiply / gain |
wax.div(…) | Divide signals |
wax.mod(…) | Modulo |
wax.min(…) | Minimum of inputs |
wax.max(…) | Maximum of inputs |
wax.pow(…) | Power |
wax.sin(…) | Sine math |
wax.cos(…) | Cosine math |
wax.tan(…) | Tangent math |
wax.tanh(…) | Tanh soft clip |
wax.abs(…) | Absolute value |
wax.sqrt(…) | Square root |
wax.exp(…) | Exponential |
wax.ln(…) | Natural log |
wax.log(…) | Log |
wax.log2(…) | Log base 2 |
wax.floor(…) | Round down |
wax.ceil(…) | Round up |
wax.round(…) | Round to nearest |
wax.asinh(…) | Inverse hyperbolic sine |
wax.le(…) | Less than or equal (logic) |
wax.leq(…) | Less or equal |
wax.ge(…) | Greater than or equal — common for MIDI gates |
wax.geq(…) | Greater or equal |
wax.eq(…) | Equal comparison |
wax.and(…) | Logical AND |
wax.or(…) | Logical OR |
Other bundled exports
| API | Purpose |
|---|---|
wax.constant(…) | Graph node (other) — see @elemaudio/core |
wax.identity(…) | Graph node (other) — see @elemaudio/core |