# WAX Web helper (WaxWeb)

**Canonical AI / author doc for Web Audio apps inside WAX.**  
Give models **this URL only** for the web path (not WordPress HTML, not contributor internals).

| | |
|--|--|
| Helper script | `https://szfpro.github.io/CodeEditorHTML/wax-web.js` |
| This doc | `https://szfpro.github.io/CodeEditorHTML/wax/docs/WEB.md` |
| Native / hybrid graphs | [`NATIVE.md`](NATIVE.md) |

**Requires:** WAX runtime ≥ 1.0.0, plugin ≥ 1.20.0 (helper **0.2.2+**).

Inside the WAX plugin WebView, `WaxWeb` is usually **injected** — no `<script src>` needed. Outside WAX, load the hosted URL above. **Do not invent other script URLs** (especially not under `wp.audiofusion.com/docs/...`).

### Custom Pages / code-editor preview

| Do | Don’t |
|----|--------|
| Prefer **no** `wax-web.js` tag inside WAX (inject already provides `WaxWeb`) | Assume a CDN tag is required in Custom Pages |
| If you include the CDN for standalone/browser testing, put it at the **end of `<body>`** (after your markup) | Put `wax-web.js` early in `<head>` before other boot (older helpers could miss DataTree in the preview iframe) |
| Use `WaxWeb.create({ appName })` + DataTree boot below | Invent a second DataTree API |

Helper **0.2.2+**: `wax.data.*` resolves `WAX_DataTree` on this window, or on `parent` when the page runs in the Custom Pages preview iframe. Still prefer inject / omit redundant CDN tags.

---

## If you are an AI generating a page

You **must**:

1. Use only APIs in this doc: `WaxWeb.create` → `wax.audio` / `wax.host` / `wax.midi` / `wax.data` / `wax.playhead` / `wax.transport` / `wax.scheduler`.
2. Include the **DataTree boot block** (adapt `APP_NAME` + `collect` / `apply` only).
3. Route audio with `wax.audio.output` / `connect` / `input` — not a homemade mic loop labeled “VST”.
4. **Never** invent: host backends, `wax.data.subscribe`, `push(key, value)`, JUCE / ValueTree sync, random `WaxWeb.js` CDNs.
5. For **Custom Pages inside WAX**: omit `<script src="…/wax-web.js">` when possible (inject provides `WaxWeb`). If you must include the CDN, put it at the **end of `<body>`**, not early in `<head>`.

Omitting `await wax.data.pull()` or `setProvider` is **incorrect**.

**Prompt tip:** “Follow WEB.md exactly. Do not invent APIs. Inside WAX Custom Pages, do not put wax-web.js in `<head>`.”

---

## What you can build (Web path)

| Experience | Pattern |
|------------|---------|
| Instrument / synth | Oscillators / samples → gain → `wax.audio.connect` / `output`; MIDI via `wax.midi` |
| Audio effect | `wax.audio.input(ctx)` → process → `wax.audio.output(ctx)` |
| Step sequencer / arpeggiator | `wax.scheduler.createStepScheduler` + `bufferSource.start(whenSec)` |
| Transport-locked UI | `wax.transport` + `wax.playhead` |
| Presets / DAW session recall | `wax.data` (DataTree) |
| DAW automation | MIDI CC in/out with `fromMIDI` guard |

Audio runs in the page **Web Audio** graph (tied to host sample rate / block). For plugin-native DSP graphs, use [`NATIVE.md`](NATIVE.md).

---

## Quick start

**Outside WAX** (browser / standalone test):

```html
<script src="https://szfpro.github.io/CodeEditorHTML/wax-web.js"></script>
<script>
  const wax = WaxWeb.create({ appName: "my-app-id" });
  // appName required, stable, unique per page — never rename after release
</script>
```

**Inside WAX / Custom Pages:** skip the `<script src>` — use injected `WaxWeb`. If a CDN tag is present for dual-use HTML, place it at the **end of `<body>`**.

| Surface | Role |
|---------|------|
| `wax.audio` | Shared `AudioContext`, DAW input/output |
| `wax.host` | Channels, sample rate, block size |
| `wax.midi` | DAW MIDI in/out |
| `wax.playhead` | PPQ / tempo / timing |
| `wax.transport` | Play / stop / BPM |
| `wax.scheduler` | Step sequencer helper |
| `wax.data` | DataTree presets / recall |
| `wax.isWax()` / `wax.version` | Host detection |

---

## Audio

Inside WAX, audio is already activated (no click-to-start). Prefer `wax.audio.*` over raw assumptions.

### Instrument output

```js
const wax = WaxWeb.create({ appName: "my-synth" });
const ctx = wax.audio.context(); // default shared context
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
wax.audio.connect(gain, ctx); // or: gain.connect(wax.audio.output(ctx))
gain.gain.value = 0.1;
osc.start();
```

`wax.audio.context({ shared: false })` creates a separate context when needed.

### Effect input (DAW track)

```js
const wax = WaxWeb.create({ appName: "my-effect" });
const ctx = wax.audio.context();
const input = await wax.audio.input(ctx);
input.connect(/* your nodes */ wax.audio.output(ctx));
```

Inside WAX, `getUserMedia({ audio: true })` is the plugin input path. Prefer `wax.audio.input(ctx)`.

```js
const stream = await wax.audio.inputStream(); // advanced: raw MediaStream
```

---

## Host

```js
wax.host.inputChannels();   // 1 or 2
wax.host.outputChannels();
wax.host.sampleRate();
wax.host.blockSize();
```

Use for mono/stereo routing and labels.

---

## MIDI

```js
const wax = WaxWeb.create({ appName: "my-midi-app" });
await wax.midi.ready();

const unsub = wax.midi.onMessage((msg) => {
  // msg.type: noteon | noteoff | cc | pitchbend | programchange
  // msg.note, msg.velocity, msg.channel, msg.controller, msg.value, raw bytes
  if (msg.type === "noteon" && msg.velocity > 0) { /* start voice */ }
  if (msg.type === "noteoff" || (msg.type === "noteon" && msg.velocity === 0)) { /* stop */ }
});

wax.midi.noteOn(60, 100);      // note, velocity [, channel 1–16]
wax.midi.noteOff(60);
wax.midi.cc(1, 64);
wax.midi.send([0x90, 60, 100]); // raw
```

### Automation (CC) — avoid feedback

```js
let fromMIDI = false;
slider.addEventListener("input", () => {
  if (!fromMIDI) wax.midi.cc(1, Number(slider.value));
});
wax.midi.onMessage((msg) => {
  if (msg.type !== "cc" || msg.controller !== 1) return;
  fromMIDI = true;
  slider.value = msg.value;
  applyCutoff(msg.value);
  fromMIDI = false;
});
```

Only send CC when the **user** moved the control.

---

## Transport & playhead

```js
wax.transport.onPlay(() => { /* start sequencer */ });
wax.transport.onStop(() => { /* stop / panic notes */ });
wax.transport.onBpm((bpm) => { /* … */ });

wax.playhead.start(8); // update interval ms
wax.playhead.subscribe(() => {
  const t = wax.playhead.getTiming();
  // t.isPlaying, t.bpm, t.ppq, t.ppqExtrapolated, t.ppqBarRelative,
  // t.timeInSeconds, t.timeInSamples, t.timeSigNumerator/Denominator
});
// wax.playhead.stop();
// await wax.playhead.request(); // one-shot host snapshot
```

Accessors: `isPlaying()`, `bpm()`, `ppq()`, `ppqBarRelative()`, `stepIndex(stepsPerQuarter, steps)`.

**Schedule audio on `audioContext.currentTime`**, not `setInterval` / `requestAnimationFrame` alone — the UI thread can stall when the editor closes.

---

## Scheduler (tempo grid)

```js
const ctx = wax.audio.context();
const scheduler = wax.scheduler.createStepScheduler({
  audioContext: ctx,
  steps: 16,
  stepsPerQuarter: 4,
  lookaheadMs: 25,
  scheduleAheadSec: 0.1,
  // barRelative: true,  // 16 steps per bar
  onStep(step, whenSec, info) {
    // schedule sound at whenSec — e.g. source.start(whenSec)
  },
});
scheduler.start();
// scheduler.stop(); scheduler.reset(); scheduler.setBpm(128); scheduler.dispose();
```

When the DAW plays, the scheduler follows host playhead (PPQ extrapolation). When stopped, it uses a local BPM clock.

---

## DataTree (REQUIRED for presets / session recall)

Stores **one JSON snapshot per `appName`**. Not a live key/value DB, not automation, not per-knob paths.

### Signatures

```js
wax.data.push(data);                 // whole object
await wax.data.pull();               // Promise → snapshot
wax.data.cached();                   // sync cache if any
wax.data.onHydrated((data) => {});
wax.data.setProvider(() => data);    // host save → fresh state
```

`appName` comes from `WaxWeb.create({ appName })`. Optional second args are **appName overrides**, never property paths.

### Wrong vs right

| Wrong | Right |
|-------|--------|
| `wax.data.push("cutoff", 1200)` | `wax.data.push({ schema: 1, params: { cutoff: 1200 } })` |
| `await wax.data.pull("cutoff")` | `await wax.data.pull()` |
| `wax.data.subscribe(...)` | **does not exist** |
| only `onHydrated` | always `await pull()` (+ cached / onHydrated) |
| push on page load | push only after `canPush` + user change |
| invent backend / JUCE | only `wax.data.*` |
| fake script under `/docs/.../WaxWeb.js` | hosted `wax-web.js` URL above |
| `wax-web.js` early in `<head>` on Custom Pages | omit CDN in WAX, or place at end of `<body>` (helper 0.2.2+ also falls back to `parent.WAX_DataTree`) |

### REQUIRED boot block — copy this

```js
const APP_NAME = "my-app-id";
const wax = WaxWeb.create({ appName: APP_NAME });

let params = { cutoff: 1200, resonance: 0.5, gain: 0.8 };
let canPush = false;
let applying = false;
let pushTimer = 0;

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 collect() {
  return { schema: 1, params: { ...params } };
}

function apply(raw) {
  const data = unwrap(raw);
  if (!data) return;
  applying = true;
  try {
    const p = data.params || data;
    Object.assign(params, p);
    // 1) update UI from params
    // 2) update DSP from the SAME JS vars (not only DOM)
  } finally {
    applying = false;
  }
}

function pushSoon() {
  if (!canPush || applying) return;
  clearTimeout(pushTimer);
  pushTimer = setTimeout(() => {
    try { wax.data.push(collect()); } catch (_) {}
  }, 250);
}

async function initDataTree() {
  wax.data.setProvider(() => collect());
  wax.data.onHydrated((d) => { if (d) apply(d); });
  const cached = wax.data.cached();
  if (cached) apply(cached);
  try {
    const saved = await wax.data.pull();
    if (saved) apply(saved);
  } catch (_) {}
  canPush = true;
}

initDataTree();
// on knob input: update params[…] then pushSoon();
```

Recommended shape: `{ schema: 1, params: { /* knobs only — not meters/scopes */ } }`.

| Control | When to push |
|---------|----------------|
| Knobs / selects | Debounced ~250 ms |
| XY pad | Pointer **up** only |
| Meters / scopes / playhead | **Never** |

---

## Recipe: polyphonic Web Audio synth (sketch)

```js
const APP_NAME = "web-poly-sketch";
const wax = WaxWeb.create({ appName: APP_NAME });
const ctx = wax.audio.context();
const master = ctx.createGain();
master.gain.value = 0.3;
wax.audio.connect(master, ctx);

const voices = new Map();
function noteOn(note, vel = 100) {
  if (ctx.state === "suspended") ctx.resume();
  noteOff(note);
  const osc = ctx.createOscillator();
  const g = ctx.createGain();
  osc.type = "sawtooth";
  osc.frequency.value = 440 * 2 ** ((note - 69) / 12);
  const now = ctx.currentTime;
  g.gain.setValueAtTime(0.0001, now);
  g.gain.exponentialRampToValueAtTime(vel / 127, now + 0.02);
  osc.connect(g); g.connect(master);
  osc.start(now);
  voices.set(note, { osc, g });
}
function noteOff(note) {
  const v = voices.get(note);
  if (!v) return;
  const now = ctx.currentTime;
  v.g.gain.cancelScheduledValues(now);
  v.g.gain.setValueAtTime(v.g.gain.value, now);
  v.g.gain.exponentialRampToValueAtTime(0.0001, now + 0.3);
  v.osc.stop(now + 0.35);
  voices.delete(note);
}

await wax.midi.ready().catch(() => {});
wax.midi.onMessage((m) => {
  if (m.type === "noteon" && m.velocity > 0) noteOn(m.note, m.velocity);
  else if (m.type === "noteoff" || (m.type === "noteon" && m.velocity === 0)) noteOff(m.note);
});
wax.transport.onStop(() => { [...voices.keys()].forEach(noteOff); });

// + DataTree boot block with params for wave/cutoff/etc.
```

---

## Recipe: effect (sketch)

```js
const wax = WaxWeb.create({ appName: "web-fx-sketch" });
const ctx = wax.audio.context();
const input = await wax.audio.input(ctx);
const filter = ctx.createBiquadFilter();
filter.type = "lowpass";
filter.frequency.value = 2000;
input.connect(filter);
filter.connect(wax.audio.output(ctx));
// + DataTree for cutoff/Q; MIDI CC optional
```

---

## Recipe: step sequencer (sketch)

```js
const wax = WaxWeb.create({ appName: "web-seq-sketch" });
const ctx = wax.audio.context();
const steps = Array(16).fill(0).map((_, i) => (i % 4 === 0 ? 1 : 0));

const scheduler = wax.scheduler.createStepScheduler({
  audioContext: ctx,
  steps: 16,
  stepsPerQuarter: 4,
  onStep(step, whenSec) {
    if (!steps[step]) return;
    const osc = ctx.createOscillator();
    const g = ctx.createGain();
    osc.frequency.value = 220;
    g.gain.setValueAtTime(0.2, whenSec);
    g.gain.exponentialRampToValueAtTime(0.0001, whenSec + 0.1);
    osc.connect(g);
    wax.audio.connect(g, ctx);
    osc.start(whenSec);
    osc.stop(whenSec + 0.12);
  },
});
wax.transport.onPlay(() => scheduler.start());
wax.transport.onStop(() => scheduler.stop());
// + DataTree for `steps` pattern
```

---

## Minimal full skeleton

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>My WAX Web App</title>
</head>
<body>
<!-- Outside WAX: include CDN. Inside Custom Pages: omit (inject). If dual-use, keep at end of body. -->
<script src="https://szfpro.github.io/CodeEditorHTML/wax-web.js"></script>
<script>
(async function () {
  const APP_NAME = "my-app-id";
  const wax = WaxWeb.create({ appName: APP_NAME });
  const ctx = wax.audio.context();

  let params = { gain: 0.2 };
  let canPush = false;
  function collect() { return { schema: 1, params: { ...params } }; }
  function apply(raw) {
    const d = raw && (raw.params || raw.data || raw);
    if (d && typeof d === "object") Object.assign(params, d);
  }
  wax.data.setProvider(() => collect());
  const cached = wax.data.cached();
  if (cached) apply(cached);
  try { const s = await wax.data.pull(); if (s) apply(s); } catch (_) {}
  canPush = true;

  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  gain.gain.value = params.gain;
  osc.connect(gain);
  wax.audio.connect(gain, ctx);
  osc.start();

  await wax.midi.ready().catch(() => {});
})();
</script>
</body>
</html>
```

---

## Raw globals (optional)

Prefer WaxWeb. If you skip the helper:

```js
window.WAX_DataTree.push(data, appName); // appName required
await window.WAX_DataTree.pull(appName);
window.WAX_Play / window.WAX_Stop / window.WAX_BPM
window.WAX_RequestPlayheadInfo();
window.PlayheadInfo;
```

Same rules: whole-blob DataTree, pull before push.

---

## AI checklist before finishing a page

- [ ] `WaxWeb.create({ appName })` with unique stable id  
- [ ] Correct script URL or plugin inject (no invented CDN); Custom Pages: omit CDN or end of `<body>`  
- [ ] Audio via `wax.audio.input` / `output` / `connect`  
- [ ] DataTree: `setProvider` + `await pull()` + `canPush` gate + debounced `push(collect())`  
- [ ] No `subscribe` / `push(key,value)` / JUCE backends  
- [ ] MIDI automation uses `fromMIDI` guard  
- [ ] Sequencer sounds scheduled on AudioContext time  
- [ ] If you need native plugin DSP / `midinotein` → switch to [`NATIVE.md`](NATIVE.md)

---

## Do not

- Call low-level bridge / emit APIs from page code  
- Share one `appName` across different devices  
- Label a Web Audio page “WaxNative” — use [`NATIVE.md`](NATIVE.md) for native graphs  
- Store meter/scope animation in DataTree  
- Put `wax-web.js` early in `<head>` for Custom Pages when inject already provides `WaxWeb` (omit it, or load at end of `<body>`)  
