Write Max for Live devices in JavaScript. Compile to .amxd and drag into Ableton Live.
js2max is a Node.js compiler that takes a JavaScript file written for the Max 9 v8 engine and produces a ready-to-use .amxd device. No visual patching required.
The Max/MSP visual editor is great for experimentation but painful for serious development:
- No version control (binary/JSON diffs are unreadable)
- No code review, no linting, no refactoring tools
- AI coding assistants can't help you patch
- Copy-paste logic between devices is manual and error-prone
Max 9 introduced the v8 object — a modern V8 JavaScript engine with full ES6+ support and access to the Live Object Model. Your logic is pure JavaScript; you just need a minimal patch to wire it up.
js2max generates that patch for you.
npx js2max compile effect.js # → effect.amxd (drag into Ableton Live)
npx js2max compile effect.js -o effect.maxpat # → JSON for Max 9npm install -g js2maxOr use directly with npx:
npx js2max compile <input.js> [options]// @device midi-effect
inlets = 2;
outlets = 1;
function msg_int(value) {
if (inlet === 0) {
// Control message
post("Received:", value, "\n");
return;
}
// MIDI byte — pass through doubled
outlet(0, value);
}
function bang() {
outlet(0, "hello", "world");
}# MIDI effect (default) → .amxd
js2max compile effect.js
# Audio effect
js2max compile delay.js --type audio-effect
# Instrument
js2max compile synth.js --type instrument
# Output as .maxpat instead
js2max compile effect.js -o effect.maxpat| Option | Default | Description |
|---|---|---|
-o, --output <path> |
<input>.amxd |
Output file path (.amxd or .maxpat) |
-t, --type <type> |
midi-effect |
Device type: midi-effect, audio-effect, instrument |
--no-embed |
(embeds by default) | Reference external .js file instead of embedding |
-w, --device-width <px> |
400 |
Max for Live device strip width |
.amxd(default) — Binary Max for Live device. Opens directly in Ableton Live (any version with Max for Live support)..maxpat— JSON Max patch. Opens in Max 9. Use-o file.maxpatto select this format.
The .amxd binary format was reverse-engineered from Ableton's own maxdevtools and validated against working devices. It uses a chunk-based container (ampf → meta → ptch with mx@c header and dlst directory listing).
Use comments at the top of your file to configure compilation:
// @device midi-effect — sets device type (overridden by --type flag)
// @inlet 0 "MIDI input" — help text for inlet 0
// @inlet 1 "Control" — help text for inlet 1
// @outlet 0 "Processed MIDI" — help text for outlet 0Add interactive controls to your device's presentation view with @ui decorators. The compiler generates Max UI objects, wires them to your v8 inlets/outlets, and auto-layouts them in the device strip.
// @ui live.text "Fire" trigger inlet=0 — button (sends bang on click)
// @ui live.dial "Delay" inlet=1 min=0 max=1000 — rotary dial
// @ui live.slider "Volume" inlet=2 min=0 max=127 — vertical slider
// @ui live.toggle "Active" outlet=0 — LED toggle (driven by code)Format: // @ui <maxclass> "<label>" [trigger] inlet=N|outlet=N [min=X] [max=Y]
| Parameter | Description |
|---|---|
inlet=N |
Wires UI output to v8 inlet N (user controls the code) |
outlet=N |
Wires v8 outlet N to UI input (code controls the display) |
trigger |
For live.text — button mode (bang on click) instead of toggle |
min/max |
Value range for dials and sliders |
Example — MIDI echo with UI:
// @device midi-effect
// @ui live.dial "Delay" inlet=0 min=0 max=1000
// @ui live.dial "Feedback" inlet=2 min=0 max=100
// @ui live.toggle "Activity" outlet=1
inlets = 3;
outlets = 2;
function msg_int(value) {
if (inlet === 0) {
delayTime = value;
return;
}
if (inlet === 2) {
feedback = value / 100;
return;
}
outlet(0, value);
}Elements are arranged in a row below the title and wrap automatically when they exceed the device width.
- Parses your
.jsfile to extractinlets,outlets, handler functions, and decorator comments - Selects a template based on device type (MIDI effect, audio effect, or instrument)
- Generates the patch JSON with the
v8object wired to the appropriate Max for Live infrastructure (midiin/midiout,plugin~/plugout~,live.thisdevice) - Embeds your JavaScript source directly in the patch (single-file distribution)
- Wraps in the
.amxdbinary container (chunk-based:ampf+meta+ptchwithmx@cheader anddlstdirectory)
See the examples/ directory:
- hello-world.js — Simplest possible device with a "Say Hello" button
- midi-echo.js — MIDI delay/echo with Delay/Feedback dials and Activity toggle
- clip-launcher.js — Live Object Model with Track/Slot dials and Fire/Stop buttons
The v8 object gives you access to these globals:
// Inlets and outlets (must be in global scope)
inlets = 2;
outlets = 3;
inlet; // read-only: which inlet triggered the current function
// Output
outlet(n, ...args); // send message out outlet n
outlet_array(n, arr); // send JS array (v8 only)
outlet_dictionary(n, dict); // send dictionary (v8 only)
// Message handlers (define as global functions)
function bang() {}
function msg_int(n) {}
function msg_float(f) {}
function list(...args) {}
function anything(msg, ...args) {} // catch-all
// Live Object Model
var api = new LiveAPI(callback, "live_set tracks 0");
api.get("name");
api.set("mute", 1);
api.call("fire");
// Utilities
post("debug message\n"); // print to Max consoleFull reference: Max JavaScript User Guide | v8 Reference | Live API
- No audio DSP: JavaScript runs in Max's low-priority thread. Use
v8for MIDI processing, generative tools, control logic, and Live Object Model access — not for real-time audio processing.
git clone https://github.com/ktamas77/js2max.git
cd js2max
npm install
npm run build
npm testPre-commit hooks (via husky) automatically run on every commit:
- TypeScript typecheck (
tsc --noEmit) - Prettier formatting
- ESLint linting
npm run typecheck # type check
npm run lint # eslint
npm run format # prettier --writeSee CHANGELOG.md for release history.
MIT