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:
- pull existing state
- apply restored values
- 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.emitProtectedEventwindow.WAXTreeDestination.emitwindow.__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.