# WAX Native helper (WaxNative)

**Canonical AI / author doc for native-graph apps inside WAX.**  
Audio runs in the **plugin** (instruction batch), not in the page `AudioContext`.  
Give models **this URL** for native / hybrid instruments & FX.

| | |
|--|--|
| Helper script | `https://szfpro.github.io/CodeEditorHTML/wax-native.js` |
| Browser graph runtime | `https://szfpro.github.io/CodeEditorHTML/wax-web-audio.js` (auto-fetched by `create()` outside WAX, **0.1.6+**; stereo in defaults **0.1.1+**) |
| This doc | `https://szfpro.github.io/CodeEditorHTML/wax/docs/NATIVE.md` |
| Web Audio–only apps | [`WEB.md`](WEB.md) |

Inside the plugin, `WaxNative` is usually **injected**. Outside WAX, load `wax-native.js` from the URL above. **Do not invent other script URLs. Do not import graph libraries from third-party CDNs.**

---

## If you are an AI generating a page

You **must**:

1. Use `await WaxNative.create()` and build on that **`wax`** instance (`wax.cycle`, `wax.svf`, `wax.midinotein`, …).
2. Emit with `wax.render(L, R?)` + `ensureEmit()` / poke retries — never hand-roll emit chains.
3. Instruments: **`midinotein` + `adsr` in the graph**; UI keys via **`wax.midi.noteOn` / `noteOff`**. Do **not** drive amp with JS envelopes into `wax.const` every frame.
4. DataTree: pull-first, defer init ~**600 ms**, gate apply on **`wax.didEmit()`**.
5. **Never** invent JUCE backends, `DataTree.subscribe`, or `push(key, value)`.
6. A page that only uses `AudioContext` oscillators is **Web Audio** → [`WEB.md`](WEB.md), even if titled “Native”.
7. Meters / scopes: put **`wax.scope`** on rendered outputs; listen via **`receiveWaxNativeEvent("scope")`** + **`WAX_NativeEvent`** (see [Meter / scope UI](#meter--scope-ui)). **Never** invent DOM events (`waxMeter`, `waxNativeMeter`) or `WAXTreeDestination.on("meter")`.
8. Browser FX: after `create()`, call **`await wax.connectMic()`** on a user gesture so `wax.in` hears the mic (no-op in the plugin).

**Prompt tip:** “Follow NATIVE.md exactly. Use WaxNative graph nodes only.”

---

## What you can build (Native path)

| Experience | Pattern |
|------------|---------|
| Instrument | `midinotein` → allocate/unpack → osc → filter → `adsr` → `render(L,R)`; UI MIDI via `wax.midi` |
| Audio FX | `wax.in({ channel })` → process → `render` (dual mono or stereo) |
| Sequencer | `train` / `seq2` + transport Play/Stop freeze; sync **emit** on transport (not RAF) |
| Hybrid preview | Same `buildOut(wax)` in plugin and browser (`create()` auto web fallback) |
| Presets / recall | DataTree blob + apply → **re-render graph** |
| Meter / scope | [`wax.scope` + event hooks](#meter--scope-ui) (native); Web path may use `AnalyserNode` |

Editor closed: host MIDI + native graph keep sounding. JS-only gates go silent when the WebView stalls.

---

## Quick start

```html
<script src="https://szfpro.github.io/CodeEditorHTML/wax-native.js"></script>
<script>
  let wax = null;

  async function boot() {
    WaxNative.keepAlive();
    wax = await WaxNative.create();
    renderGraph();
  }

  function renderGraph() {
    const tone = wax.mul(wax.cycle(440), 0.15);
    wax.render(tone, tone); // dual mono
    ensureEmit();
  }

  function ensureEmit() {
    if (wax.didEmit()) return true;
    if (typeof wax.reemitLast === "function") wax.reemitLast("retry");
    return wax.didEmit();
  }

  boot();
</script>
```

```js
if (!WaxNative.hasBridge()) {
  // browser / preview — create() still works (web fallback)
}
```

---

## Core rules

| Goal | Do | Avoid |
|------|----|--------|
| Host MIDI → sound | `midinotein` → allocate / unpack + gates **in graph** | JS-only gates + Web MIDI alone |
| UI keyboard | `wax.midi.noteOn` / `noteOff` | Rebuilding graph every note |
| FX from track | `wax.in({ channel: 0/1 })` → process → `render` | Assuming page stays visible |
| Survive editor close | Graph + host MIDI buffer | RAF / JS envelope → `const` amp |
| Emit | `render` + `ensureEmit` + poke schedule | Copying low-level emit backends into the page |
| Params | `wax.const({ key, value })` stable keys; re-render on change | Unstable keys / huge graphs |

```js
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(() => { renderGraph(); ensureEmit(); }, 40);
}
[50, 150, 400, 800, 1500, 2500, 4000].forEach((ms) => setTimeout(poke, ms));
addEventListener("load", () => {
  [0, 100, 300, 600, 1200, 2000].forEach((ms) => setTimeout(poke, ms));
});
```

**Never** call `render()` synchronously at the end of a top-level inline script without deferred poke (Project URL resets can eat the first emit).

Do **not** pass `onBatch` to `create()` unless you emit yourself — otherwise `didEmit()` stays false.

---

## Static API

| API | Purpose |
|-----|---------|
| `await WaxNative.create(options?)` | Native in WAX; Promise + web graph outside WAX |
| `WaxNative.createAsync(options?)` | Always a Promise |
| `WaxNative.hasBridge()` | Real host inject present |
| `WaxNative.keepAlive()` | Keep WebView foreground for timers / MIDI UI |
| `WaxNative.midi` | Same as instance `wax.midi` |
| `WaxNative.host()` | `{ outputChannels(), inputChannels(), blockSize(), sampleRate() }` |
| `WaxNative.version` / `minPlugin` | Semver helpers |
| `WaxNative.emitEvent(name, payload)` | Rare: `waxNativeEnabled`, `waxNativeRequestParamSync` |

Web instance extras (no-ops on native): `wax.connectMic()`, `wax.connectInput(streamOrNode)`, `wax.disconnectInput()`, `wax.resumeAudio()`, `wax.attachOutputAnalyser()`, `wax.pollOutputScope(name?)`.

---

## Instance API

| API | Purpose |
|-----|---------|
| `wax.render(L, R?)` | Set roots and emit batch |
| `wax.renderWithOptions(opts, …)` | Advanced render |
| `wax.in({ channel })` | Host audio input (0 = L/mono, 1 = R) |
| `wax.didEmit()` | Last render reached plugin |
| `wax.reemitLast(reason?)` | Resend last batch |
| `wax.midi.*` | UI → host MIDI for `midinotein` |
| `wax.host` | Channel / rate info |
| Graph nodes | See catalog below |

### Output modes

```js
wax.render(mono);           // ch 0 only
wax.render(mono, mono);     // dual mono (safe default for synths)
wax.render(left, right);    // true stereo
```

```js
function renderOutputs(outL, outR) {
  const outCh = wax.host?.outputChannels?.() ?? 2;
  if (outCh <= 1) wax.render(outL, outL);
  else wax.render(outL, outR);
}
```

On mono tracks, `in({ channel: 1 })` may be silent. FX: add a conservative post gain (~0.2–0.85) — bare `in → svf` can be very quiet.

---

## Graph node catalog (author-facing)

All nodes hang off **`wax`**. Numbers are often accepted where a const signal is needed; prefer **`wax.const({ key, value })`** for knobs you will change (stable `key` → re-render updates param).

### Generators

| Node | Role |
|------|------|
| `wax.cycle(freq)` | Sine |
| `wax.saw(freq)` / `triangle` / `square` | Basic shapes |
| `wax.blepsaw(freq)` / `blepsquare` / `bleptriangle` | Band-limited (prefer for synths) |
| `wax.train(rate)` | Impulse train / clock (sequencers) |
| `wax.noise()` | Noise (if available on build) |

### MIDI voice

| Node | Role |
|------|------|
| `wax.midinotein()` | Host + virtual UI MIDI |
| `wax.midinoteallocate({ voices: N }, midiIn)` | Voice allocator |
| `wax.midinoteunpack({ channel: i }, allocated)` | → `[freq, vel]` for voice `i` |

### Envelopes / control

| Node | Role |
|------|------|
| `wax.adsr(a, d, s, r, gate)` | Amp / filter envelopes in-graph |
| `wax.const({ key, value })` | Keyed param (re-render to change) |
| `wax.smooth(pole, sig)` / `wax.tau2pole(tau)` | Smoothing |
| `wax.latch(gate, sig)` | Hold value while gated |
| `wax.ge(a, b)` / `min` / `max` | Compare / select |

### Filters / math / FX

| Node | Role |
|------|------|
| `wax.svf({ mode: "lowpass"\|"highpass"\|"bandpass"\|… }, cutoff, q, input)` | State-variable filter |
| `wax.mul(a, b, …)` / `add` / `sub` / `div` / `pow` | Math |
| `wax.tanh(sig)` | Soft clip |
| `wax.delay(…)` | Delay (signature per build — mirror reference pages) |
| `wax.ms2samps(ms)` | Time helper |

### Metering / sequencing helpers

| Node | Role |
|------|------|
| `wax.scope({ name, size, channels }, …sigs)` | Native scope events → UI meters / waveform |
| `wax.seq2(…)` | Sequencer helper (see ginger / seq demos) |

`wax.meter` exists in Elementary but WAX demos standardize on **`wax.scope`**. Prefer that.

If unsure of an exotic node signature, mirror patterns from shipping demos (`elem-poly-synth`, `elem-ginger`, `elem-svf-lowpass`, `elem-la2a`, `elem-emt140`) — **always** via `wax.*`, never CDN graph libs.

---

## Meter / scope UI

Native analyzer events come from the **plugin**, not from DOM CustomEvents or DataTree.

1. Wrap rendered outputs with **`wax.scope({ name, size, channels }, …)`** (keep them in the graph whenever the UI should be live — including bypass).
2. Hook **both** delivery paths (top-level page vs Custom Page preview iframe).

```js
const SCOPE_NAME = "main";
const SCOPE_SIZE = 512;

function scoped(node) {
  return wax.scope({ name: SCOPE_NAME, size: SCOPE_SIZE, channels: 1 }, node);
}

function renderGraph() {
  const out = /* … */;
  wax.render(scoped(out), scoped(out));
  ensureEmit();
}

function handleScopePayload(payload) {
  const data = typeof payload === "string" ? JSON.parse(payload) : payload;
  if (!data) return;
  const src = data.source || data.name; // matches wax.scope({ name })
  if (src && src !== SCOPE_NAME) return;
  const ch0 = Array.isArray(data.data) ? data.data[0] : null;
  // peak / draw from ch0 sample array; paint on RAF
}

window.WAX = window.WAX || {};
window.WAX._internal = window.WAX._internal || {};
window.WAX._internal.receiveWaxNativeEvent = function (name, payload) {
  if (name === "scope") handleScopePayload(payload);
};

addEventListener("message", (e) => {
  const d = e.data;
  // Check type AND name — do not regex on (type || name); type is always "WAX_NativeEvent".
  if (d?.type === "WAX_NativeEvent" && d.name === "scope") {
    handleScopePayload(d.payload);
  }
});
```

| Path | How events arrive |
|------|-------------------|
| Top-level / Project URL | `WAX._internal.receiveWaxNativeEvent("scope", payloadJson)` |
| Custom Page preview iframe | `postMessage({ type: "WAX_NativeEvent", name: "scope", payload })` |

Payload: `{ source: "<name>", data: [Float32Array, …] }` (one array per `channels`).

| Wrong | Right |
|-------|--------|
| `addEventListener("waxMeter" / "meter")` | `receiveWaxNativeEvent` + `WAX_NativeEvent` |
| `WAXTreeDestination.on("meter")` | Scope is not DataTree |
| `/meter/.test(d.type \|\| d.name)` | `d.type === "WAX_NativeEvent" && d.name === "scope"` |
| Scope only when effect ON | Keep `wax.scope` on bypass outs if meters must stay live |
| Web `AnalyserNode` in native mode | `wax.scope` in the graph |

Browser / Chrome: call `await wax.connectMic()` on a user gesture so `wax.in` receives mic audio. Scope events are forwarded when the web-renderer emits them; otherwise `wax.attachOutputAnalyser()` + `wax.pollOutputScope(name)` fans out plugin-shaped scope payloads (see Hybrid §).

---

## Recipe: effect (SVF lowpass)

```js
let wax = null;
let cutoffHz = 1200;
let q = 0.8;
let gain = 0.85;

async function boot() {
  WaxNative.keepAlive();
  wax = await WaxNative.create();
  pokeSchedule();
}

function renderGraph() {
  const mic = wax.in({ channel: 0 });
  const out = wax.mul(
    wax.const({ key: "gain", value: gain }),
    wax.svf(
      { mode: "lowpass" },
      wax.const({ key: "cutoff", value: cutoffHz }),
      wax.const({ key: "q", value: q }),
      mic,
    ),
  );
  wax.render(out, out);
  ensureEmit();
}

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(() => { renderGraph(); ensureEmit(); }, 40);
}
function pokeSchedule() {
  [50, 150, 400, 800, 1500, 2500, 4000].forEach((ms) => setTimeout(poke, ms));
}

// knobs → update cutoffHz/q/gain → renderGraph() (debounce ~16–50ms)
boot();
```

---

## Recipe: polyphonic instrument (midinotein)

```js
const NUM_VOICES = 8;
let wax = null;
let attack = 0.01, decay = 0.2, sustain = 0.6, release = 0.3;
let cutoffHz = 2500, q = 1.2, master = 0.35;

function voice(i) {
  const midiIn = wax.midinotein();
  const allocated = wax.midinoteallocate({ voices: NUM_VOICES }, midiIn);
  const [freq, vel] = wax.midinoteunpack({ channel: i }, allocated);
  const gate = wax.ge(vel, wax.const({ key: `v${i}gateEps`, value: 0.001 }));
  const freqHold = wax.latch(gate, freq);
  const velHold = wax.latch(gate, vel);
  const ampEnv = wax.adsr(
    wax.const({ key: "ampA", value: attack }),
    wax.const({ key: "ampD", value: decay }),
    wax.const({ key: "ampS", value: sustain }),
    wax.const({ key: "ampR", value: release }),
    gate,
  );
  const osc = wax.blepsaw(freqHold);
  const filt = wax.svf(
    { mode: "lowpass" },
    wax.const({ key: "cutoff", value: cutoffHz }),
    wax.const({ key: "q", value: q }),
    osc,
  );
  return wax.mul(filt, ampEnv, velHold, wax.const({ key: `v${i}gain`, value: 0.8 }));
}

function renderGraph() {
  let mix = voice(0);
  for (let i = 1; i < NUM_VOICES; i++) mix = wax.add(mix, voice(i));
  const out = wax.mul(mix, wax.const({ key: "master", value: master }));
  wax.render(out, out);
  ensureEmit();
}

async function boot() {
  WaxNative.keepAlive();
  wax = await WaxNative.create();
  try { await wax.midi.ready(); } catch (_) {}
  pokeSchedule();
}

// UI keys:
function noteOn(midi, velocity = 1) {
  wax.midi.noteOn(midi, Math.round(velocity * 127));
}
function noteOff(midi) {
  wax.midi.noteOff(midi);
}
// DAW MIDI → midinotein automatically (no page API)
```

**Avoid:** `wax.const({ key: "amp", value: jsEnvelope })` updated on a timer — fails when the editor closes.

---

## Hybrid (plugin + browser)

From **0.1.6**, one script is enough for **instruments** (virtual `midinotein`).  
From **0.1.8**, the same path also covers **FX** (`wax.in` + meters):

```js
let wax = null;

function buildOut(w) {
  const tone = w.mul(w.cycle(440), 0.15);
  return { outL: tone, outR: tone };
}

async function renderGraph() {
  if (!wax) wax = await WaxNative.create();
  const { outL, outR } = buildOut(wax);
  await Promise.resolve(wax.render(outL, outR));
}
```

`create()` is sync inside WAX and returns a Promise outside (loads `wax-web-audio.js`).  
`wax.midi.noteOn` / `noteOff` drive virtual `midinotein` in the browser.

### Browser FX input (required for `wax.in`)

Outside WAX, call on a **user gesture** (click / power):

```js
await wax.connectMic();           // getUserMedia → Elementary worklet
// or: wax.connectInput(mediaStreamOrAudioNode)
await wax.resumeAudio();          // unlock AudioContext if needed
```

On the plugin path these are **no-ops** (`connectMic` resolves `null`) — safe to call unconditionally.

Default web init is stereo in+out (`numberOfInputs: 1`, `inputChannelCount: [2]`). Override with `WaxNative.create({ initialize: { … } })` if needed.

### Browser meters

Plugin scope events are forwarded from the web-renderer when available. Same page hooks as native:

```js
WAX._internal.receiveWaxNativeEvent = (name, payload) => {
  if (name === "scope") handleScopePayload(payload);
};
```

Fallback Analyser (downmixed stereo) if graph scope callbacks are quiet:

```js
wax.attachOutputAnalyser();
requestAnimationFrame(function tick() {
  if (!WaxNative.hasBridge()) wax.pollOutputScope("main");
  requestAnimationFrame(tick);
});
```

Advanced: `WaxWebAudio.createWeb({ audioContext })` returns the same `wax.*` vocabulary for explicit browser control — still **no CDN graph imports**.

---

## DataTree with Native (REQUIRED for presets)

Same blob rules as [`WEB.md`](WEB.md): **one JSON per `appName`**, `push(data)` / `pull()`, no keyed KV API, no `subscribe`.

Extra native rules:

1. Defer DataTree init ~**600 ms** (not 80 ms).  
2. Gate apply on **`wax.didEmit()`** (or retry ~150 ms).  
3. Apply must set **JS vars** used by `renderGraph()`, then **re-render**.  
4. Boot HTML defaults + poke **first**, then pull/apply.

### Wrong vs right

| Wrong | Right |
|-------|--------|
| Web Audio oscs labeled Native | `WaxNative.create` + `wax.render` |
| `push("cutoff", 1)` | `push({ schema: 1, params: { cutoff: 1 } })` |
| `initDataTree` at 80 ms | `setTimeout(initDataTree, 600)` + `didEmit` gate |
| Apply only updates DOM | Apply updates vars **and** `renderGraph()` |
| JS envelope → `const` amp | `midinotein` + `adsr` in graph |

### Boot sketch

```js
const APP_NAME = "my-native-device";
let canPush = false;

function collect() {
  return { schema: 1, params: { cutoffHz, q, master, attack, decay, sustain, release } };
}
function unwrap(raw) {
  if (!raw || typeof raw !== "object") return null;
  if (raw.params || raw.state) return raw;
  if (raw.data && typeof raw.data === "object") return raw.data;
  return raw;
}
function apply(raw) {
  const data = unwrap(raw);
  if (!data) return;
  const p = data.params || data;
  Object.assign(/* your vars */, p);
  syncUIFromVars();
  renderGraph();
}

async function initDataTree() {
  const helper = window.WaxWeb
    ? WaxWeb.create({ appName: APP_NAME })
    : null;
  const dt = helper ? helper.data : window.WAX_DataTree;
  if (!dt) return;

  const tryApply = (raw) => {
    if (!raw) return;
    if (wax.didEmit()) apply(raw);
    else setTimeout(() => apply(raw), 150);
  };

  if (helper) {
    dt.setProvider(() => collect());
    dt.onHydrated((d) => tryApply(d));
    const cached = dt.cached && dt.cached();
    if (cached) tryApply(cached);
    try {
      const saved = await dt.pull();
      tryApply(saved);
    } catch (_) {}
  } else {
    if (typeof dt.onPull === "function") {
      dt.onPull(() => dt.push(collect(), APP_NAME));
    }
    if (typeof dt.getCached === "function") {
      const c = dt.getCached();
      if (c) tryApply(c);
    }
    try {
      const saved = await dt.pull(APP_NAME);
      tryApply(saved);
    } catch (_) {}
  }
  canPush = true;
}

setTimeout(initDataTree, 600);
// knobs: update vars → renderGraph(); if (canPush) debounced push(collect())
```

---

## Transport

```js
window.WAX_Play = () => {
  // unfreeze trains / bump seq epoch; call renderGraph() SYNC — not RAF
};
window.WAX_Stop = () => {
  // freeze: train rate 0 / master 0; optional note panic via midi
};
window.WAX_BPM = (bpm) => { /* … */ };
```

Or use `WaxWeb` transport helpers from [`WEB.md`](WEB.md) alongside Native.

---

## UI MIDI

```js
await wax.midi.ready().catch(() => {});
wax.midi.noteOn(60, 100);
wax.midi.noteOff(60);
wax.midi.send([0x90, 60, 100]);
```

Feeds the same buffer as host MIDI for `midinotein`.

---

## Minimal deferred skeleton

```html
<!DOCTYPE html>
<html>
<body>
  <script src="https://szfpro.github.io/CodeEditorHTML/wax-native.js"></script>
  <script>
  (function () {
    var wax = null;

    async function boot() {
      WaxNative.keepAlive();
      wax = await WaxNative.create();
      [50, 150, 400, 800, 1500, 2500, 4000].forEach(function (ms) {
        setTimeout(poke, ms);
      });
      setTimeout(initDataTree, 600);
    }

    function renderGraph() {
      var mic = wax.in({ channel: 0 });
      var out = wax.mul(0.2, wax.svf({ mode: "lowpass" }, 1200, 0.8, mic));
      wax.render(out, out);
      ensureEmit();
    }

    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);
      }
    }

    async function initDataTree() { /* see boot sketch above */ }

    boot();
  })();
  </script>
</body>
</html>
```

---

## AI checklist before finishing a page

- [ ] `await WaxNative.create()` + deferred poke schedule  
- [ ] Correct `wax-native.js` URL (or inject) — no invented CDN  
- [ ] `render` + `ensureEmit` / `reemitLast`  
- [ ] Instruments: `midinotein` + `adsr` + `wax.midi` for UI keys  
- [ ] No JS envelope driving `const` amp  
- [ ] FX: `in` → process → gain → dual mono or stereo `render`  
- [ ] Knobs use stable `wax.const({ key, value })`  
- [ ] DataTree: 600 ms defer, `didEmit` gate, pull then push, blob shape  
- [ ] Transport changes emit **synchronously**  
- [ ] No third-party graph CDN / no JUCE backends  
- [ ] Meters: `wax.scope` in graph + `receiveWaxNativeEvent("scope")` **and** `WAX_NativeEvent` message hook  
- [ ] No invented `waxMeter` / `WAXTreeDestination` meter listeners  
- [ ] Browser FX: `await wax.connectMic()` on gesture (safe no-op in plugin)  

---

## Do not

- Import third-party graph libraries / CDNs in page code — vocabulary is on `wax` only  

- Hand-copy low-level emit fallback chains into the page  
- Skip DataTree pull / provider / `didEmit` gating on preset devices  
- Build “native” pages that are only Web Audio oscillators — use [`WEB.md`](WEB.md)  
- Invent meter DOM events or DataTree subscriptions for scope data — use [Meter / scope UI](#meter--scope-ui)  
- Expect `wax.in` to hear anything in Chrome **without** `connectMic` / `connectInput`  

