Dev Helper Doc

Overview

Using standard HTML, CSS, JavaScript, Web Audio, and Web MIDI APIs, developers can build instruments, effects, sequencers, visualizers, educational tools, and interactive audio experiences directly inside professional production environments.

At the center of this workflow is WaxWeb.js — a lightweight helper that simplifies transport synchronization, MIDI handling, scheduling, playhead access, automation, and state management.

Helper version: 0.2.1
Requires: WAX runtime ≥ 1.0.0, WAX plugin ≥ 1.20.0

No special SDK is required.
No plugin framework experience is required.
If you can build a website, you can build a WAX app.

New to WAX? Start here. For raw global APIs (window.WAX_Play, window.PlayheadInfo, etc.), see WAX Developer Documentation.


Getting Started

Include WaxWeb.js at the top of the script.

<script src="wax-web.js"></script>

Create a WAX instance:

const wax = WaxWeb.create({
appName: "my-first-app"
});

appName is required. Use a stable, unique string per page. It identifies your app’s DataTree storage and must not change after release.

This gives you access to:

wax.audio
wax.host
wax.midi
wax.playhead
wax.transport
wax.scheduler
wax.data
wax.bridge

Check that you are running inside WAX:

if (wax.isWax()) {
console.log("Running inside WAX", wax.version);
}

Audio

Use the Web Audio API exactly as you normally would.

Inside WAX, audio is already activated by the host.

You do not need “Click to Start Audio” buttons.

The AudioContext runs tied to the DAW’s sample rate and processing block size.


Output (Instruments & Effects)

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

Or connect directly to the destination:

gain.connect(wax.audio.output(ctx));

wax.audio.context() returns a shared AudioContext by default. Pass { shared: false } to create a separate one.


Input (Effects)

To receive audio from the DAW:

const wax = WaxWeb.create({ appName: "my-effect" });
const ctx = wax.audio.context();
wax.audio.input(ctx).then((input) => {
input.connect(wax.audio.output(ctx));
});

wax.audio.input(ctx) requests the audio input and returns a ready-to-connect MediaStreamAudioSourceNode.

Inside WAX, getUserMedia({ audio: true }) represents the plugin input path. In a normal browser, this is regular microphone input.

For advanced cases, the raw stream is still available:

const stream = await wax.audio.inputStream();
const tracks = stream.getAudioTracks();

Host

Read host audio configuration for routing and UI labels.

const inCh  = wax.host.inputChannels();   // e.g. 1 or 2
const outCh = wax.host.outputChannels(); // e.g. 1 or 2
const sr = wax.host.sampleRate(); // e.g. 48000
const block = wax.host.blockSize(); // e.g. 512 or 1024

Use these when building stereo/mono routing, channel meters, or host info displays.


MIDI

WAX uses the standard Web MIDI API.

MIDI input comes from the DAW track.
MIDI output is sent back into the DAW.


Initialize MIDI

const wax = WaxWeb.create({ appName: "my-midi-app" });
wax.midi.ready().then(() => {
console.log("MIDI Ready");
});

Receive MIDI

wax.midi.onMessage((msg) => {
console.log(msg.type);
console.log(msg.note);
console.log(msg.velocity);
});

Example message types:

  • noteon
  • noteoff
  • cc
  • pitchbend
  • programchange

Each message also includes channel, controller, value, and raw bytes.

onMessage() returns an unsubscribe function.


Send MIDI

wax.midi.noteOn(60, 127);       // note, velocity
wax.midi.noteOff(60); // note
wax.midi.cc(1, 64); // controller, value

Optional MIDI channel (1–16):

wax.midi.noteOn(60, 100, 1);
wax.midi.cc(1, 64, 1);

Raw bytes are still available:

wax.midi.send([0x90, 60, 100]);

Transport

Respond to DAW playback and BPM changes.

The helper lets multiple modules register handlers without overwriting each other. Existing window.WAX_Play, window.WAX_Stop, and window.WAX_BPM handlers are preserved and chained.


Playback Events

const wax = WaxWeb.create({ appName: "my-app" });
const unsubPlay = wax.transport.onPlay(() => {
console.log("DAW Started");
});
const unsubStop = wax.transport.onStop(() => {
console.log("DAW Stopped");
});

Each handler returns an unsubscribe function.


BPM Updates

wax.transport.onBpm((bpm) => {
console.log("Host BPM:", bpm);
});

Playhead

The playhead provides access to:

  • transport position
  • PPQ timing
  • tempo
  • looping
  • time signature
  • playback state

This is the timing authority for DAW synchronization.


Start Playhead Updates

const wax = WaxWeb.create({ appName: "my-app" });
wax.playhead.start(8); // 8 = update interval in milliseconds

Stop updates when done:

wax.playhead.stop();

Read Timing

const timing = wax.playhead.getTiming();
console.log(timing.ppq);
console.log(timing.bpm);
console.log(timing.isPlaying);

getTiming() returns a flattened view for convenience. ppq is the same value as timing.ppqPosition on the raw PlayheadInfo object returned by request() or window.PlayheadInfo.


Useful Timing Values

timing.ppq
timing.ppqBarRelative
timing.ppqExtrapolated
timing.timeInSeconds
timing.timeInSamples
timing.bpm
timing.timeSigNumerator
timing.timeSigDenominator

ppqExtrapolated and ppqBarRelative advance between host snapshots so UI and sequencers stay smooth while the DAW is playing.

Disable extrapolation:

wax.playhead.getTiming({ extrapolate: false });

Individual Accessors

wax.playhead.isPlaying()
wax.playhead.isRecording()
wax.playhead.isLooping()
wax.playhead.bpm()
wax.playhead.ppq()
wax.playhead.ppqBarStart()
wax.playhead.ppqBarRelative()
wax.playhead.timeInSeconds()
wax.playhead.timeInSamples()
wax.playhead.timeSig() // { numerator, denominator }
wax.playhead.loop() // { ppqStart, ppqEnd, isLooping }
wax.playhead.stepIndex(4, 16) // 16th-note step index 0–15

Subscribe to Updates

const unsub = wax.playhead.subscribe(() => {
const t = wax.playhead.getTiming();
console.log(t.isPlaying, t.bpm, t.ppqExtrapolated);
});

One-Shot Request

const info = await wax.playhead.request();
console.log(info.timing.ppqPosition);

PlayheadInfo Shape

The object returned by request() (and window.PlayheadInfo) uses this structure:

info.state.isPlaying       // boolean — transport playing
info.state.isRecording // boolean — transport recording
info.state.isLooping // boolean — loop enabled
info.tempo.bpm // number — host tempo (e.g. 120)
info.tempo.timeSigNumerator
info.tempo.timeSigDenominator
info.timing.timeInSamples
info.timing.timeInSeconds
info.timing.ppqPosition
info.timing.ppqPositionOfLastBarStart
info.loop.ppqLoopStart
info.loop.ppqLoopEnd
info.loop.isLooping

Scheduling

JavaScript timers are not reliable for audio playback.

Functions like:

setInterval()
requestAnimationFrame()

can stall when:

  • the editor closes
  • the tab is hidden
  • the UI thread slows down

Professional timing must happen on the audio thread.


Correct Scheduling

Instead of:

setInterval(() => {
playSound();
}, 125);

Schedule directly on the Web Audio timeline:

const when = audioContext.currentTime + 0.05;
source.start(when);

The audio engine guarantees accurate playback.


Tempo-Based Scheduling

WAX provides a scheduler system for DAW-synced sequencing.


Step Sequencer Example

const wax = WaxWeb.create({ appName: "my-sequencer" });
const ctx = wax.audio.context();
const scheduler = wax.scheduler.createStepScheduler({
audioContext: ctx,
steps: 16,
stepsPerQuarter: 4,
lookaheadMs: 25,
scheduleAheadSec: 0.1,
onStep(step, whenSec, info) {
playStep(step, whenSec);
}
});
scheduler.start();

When the DAW is playing, the scheduler follows host PlayheadInfo with PPQ extrapolation between host snapshots.

When the DAW is not playing, it falls back to a local BPM clock so you can still preview.

For bar-aligned grids (16 steps per bar):

barRelative: true

Scheduler controls:

scheduler.stop();
scheduler.reset();
scheduler.setBpm(128);
scheduler.isRunning();
scheduler.dispose();

Understanding PPQ

PPQ = Pulses Per Quarter Note.

This is the most important timing system in DAWs.

Examples:

ppq = 0      // beginning of song
ppq = 1 // one quarter note later
ppq = 2.5 // halfway through beat 3

Converting PPQ to Steps

16th-note sequencer:

step = Math.floor(ppq * 4) % 16;

Or use the helper:

wax.playhead.stepIndex(4, 16);

DataTree

DataTree handles persistent state.

Use it for:

  • presets
  • sequencer patterns
  • UI state
  • project recall
  • saved parameters

Save State

const wax = WaxWeb.create({
appName: "my-synth"
});
wax.data.push({
cutoff: 1200,
resonance: 0.4,
waveform: "saw"
});

Load State

wax.data.pull().then((data) => {
console.log(data);
});

Hydration Events

wax.data.onHydrated((data) => {
console.log("Restored", data);
});

Cached State

const cached = wax.data.cached();

Provider Hook

wax.data.setProvider(() => {
return getCurrentState();
});

Best Practice

Always attempt a pull before pushing data.

Incorrect:

wax.data.push(defaultState);

on startup can overwrite the user’s saved preset.

Correct flow:

  1. pull existing state
  2. apply restored values
  3. only push after user changes

Pick one stable appName per page and never change it after release.


Automation

WAX automation is MIDI-based.

The recommended workflow:

  • send CC when user moves controls
  • receive CC from host automation
  • update UI + audio engine together

Avoiding Feedback Loops

Only send MIDI when the change originated from the user.

Do not retransmit incoming automation back to the host.


Example

let fromMIDI = false;
slider.addEventListener("input", () => {
if (!fromMIDI) {
wax.midi.cc(1, slider.value);
}
});
wax.midi.onMessage((msg) => {
if (msg.type === "cc") {
fromMIDI = true;
slider.value = msg.value;
fromMIDI = false;
}
});

Bridge

Most apps should use the helpers above. For advanced or new native events, use the bridge directly.

wax.bridge.emit("waxRequestPlayheadInfo", {});

wax.bridge.emit() tries the best available path:

  • window.WAX._internal.emitProtectedEvent
  • window.WAXTreeDestination.emit
  • window.__JUCE__.backend.emitEvent
  • parent-frame __JUCE__
  • postMessage relay for iframe/custom-page contexts

Static helper:

WaxWeb.emit("waxRequestPlayheadInfo", {});

Background Execution

Audio scheduling should never depend entirely on UI timing.

The UI thread may:

  • slow down
  • sleep
  • pause
  • stall

The audio thread continues independently.

Use:

  • AudioContext.currentTime
  • scheduled playback
  • lookahead scheduling
  • transport synchronization

instead of relying on visual timers alone.


Quick Reference

Create App

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

Audio

wax.audio.context()
wax.audio.input()
wax.audio.inputStream()
wax.audio.output()
wax.audio.connect(node, ctx)

Host

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

MIDI

wax.midi.ready()
wax.midi.onMessage()
wax.midi.send()
wax.midi.noteOn()
wax.midi.noteOff()
wax.midi.cc()

Transport

wax.transport.onPlay()
wax.transport.onStop()
wax.transport.onBpm()

Playhead

wax.playhead.start()
wax.playhead.stop()
wax.playhead.request()
wax.playhead.get()
wax.playhead.subscribe()
wax.playhead.getTiming()
wax.playhead.ppq()
wax.playhead.bpm()
wax.playhead.isPlaying()
wax.playhead.stepIndex(stepsPerQuarter, steps)

Scheduler

wax.scheduler.createStepScheduler({
audioContext,
steps,
stepsPerQuarter,
barRelative,
onStep(step, whenSec, info)
})

DataTree

wax.data.push()
wax.data.pull()
wax.data.cached()
wax.data.onHydrated()
wax.data.setProvider()

Bridge

wax.bridge.emit(eventName, payload)
wax.isWax()
wax.version

Raw APIs Still Work

The helper is additive. Existing pages can continue using:

new AudioContext();
navigator.mediaDevices.getUserMedia({ audio: true });
navigator.requestMIDIAccess();
window.WAX_DataTree.push(data, appName);
window.WAX_Play / window.WAX_Stop / window.WAX_BPM;
window.WAX_RequestPlayheadInfo();
window.PlayheadInfo;
window.__JUCE__.backend.emitEvent(eventName, payload);

Use WaxWeb when you want common WAX app patterns to be shorter, consistent, and easier to teach. For full raw API details, see WAX Developer Documentation.


Final Notes

WAX allows web applications to become deeply integrated audio tools inside professional production environments.

By combining modern browser technologies with DAW synchronization, transport awareness, MIDI routing, scheduling, and persistent state management, WAX dramatically lowers the barrier to creating powerful music software.

The web is no longer separate from audio production.

With WAX, the browser becomes part of the studio.

Was this article helpful?
Dislike