Skip to content

Add native Max for Live (.amxd) device support#9

Merged
santolucito merged 7 commits into
mainfrom
Max-For-Live-Support
Mar 30, 2026
Merged

Add native Max for Live (.amxd) device support#9
santolucito merged 7 commits into
mainfrom
Max-For-Live-Support

Conversation

@chrishyoroklee
Copy link
Copy Markdown
Collaborator

@chrishyoroklee chrishyoroklee commented Mar 28, 2026

Summary

Adds native Max for Live (.amxd) device support to MaxPyLang — save and load patches as Ableton Live devices directly from Python.

Supported device types

device_type Input Output Ableton track
"instrument" notein plugout~ MIDI track
"audio_effect" plugin~ plugout~ Audio track
"midi_effect" midiinmidiparse midiformatmidiout MIDI track

Usage

Instrument (MIDI → audio):

import maxpylang as mp

patch = mp.MaxPatch()
notein = patch.place("notein")[0]
mtof = patch.place("mtof")[0]
osc = patch.place("cycle~")[0]
clip = patch.place("clip~ -1. 1.")[0]
plugout = patch.place("plugout~")[0]

patch.connect(
    [notein.outs[0], mtof.ins[0]],
    [mtof.outs[0], osc.ins[0]],
    [osc.outs[0], clip.ins[0]],
    [clip.outs[0], plugout.ins[0]],
    [clip.outs[0], plugout.ins[1]],
)

patch.save("my_synth.amxd", device_type="instrument")

Audio effect (audio → audio):

patch = mp.MaxPatch()
plugin = patch.place("plugin~")[0]
filt = patch.place("lores~ 2000 0.5")[0]
clip = patch.place("clip~ -1. 1.")[0]
plugout = patch.place("plugout~")[0]

patch.connect(
    [plugin.outs[0], filt.ins[0]],
    [filt.outs[0], clip.ins[0]],
    [clip.outs[0], plugout.ins[0]],
    [clip.outs[0], plugout.ins[1]],
)

patch.save("my_filter.amxd", device_type="audio_effect")

Standalone functions for advanced use:

from maxpylang import save_amxd, load_amxd

json_dict = patch.get_json()
save_amxd(json_dict, "device.amxd", device_type="instrument")

json_dict = load_amxd("device.amxd")

Other fixes

  • Fix notein args: mark port-channel and channel as optional so notein can be placed without arguments
  • Harden load_amxd: use rstrip for multiple trailing nulls, compare chunk tags as raw bytes, include filename in error messages

CI

  • Runs 5 standalone examples in CI: hello_world, m4l_instrument, m4l_audio_effect, attributes, random_pitch_generator
  • Verifies each example produces its expected output file
  • Runs 81 pytest tests covering amxd binary format, round-trips, extension handling, M4L I/O objects, and backward compatibility

Test plan

  • patch.save("test.amxd", device_type="instrument") produces a valid .amxd file openable in Ableton
  • load_amxd("test.amxd") round-trips correctly
  • patch.place("notein")[0] works without errors
  • Saving .amxd without device_type raises ValueError
  • Existing .maxpat save behavior is unchanged

chrishyoroklee and others added 5 commits March 28, 2026 13:04
Integrate .amxd binary save/load directly into the maxpylang package so
users can create M4L devices with a single explicit flag:

  patch.save("synth.amxd", device_type="instrument")

- Add maxpylang/amxd.py with save_amxd/load_amxd functions
- Extend patch.save() with device_type parameter (instrument/audio_effect/midi_effect)
- Auto-detect .amxd from extension, require device_type, force extension when set
- Support loading .amxd files via MaxPatch(load_file="device.amxd")
- Export save_amxd, load_amxd, DEVICE_TYPES from package
- Add M4L instrument and audio effect examples
- Add class_transition.md and edge_cases.md planning docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
notein works without arguments in Max (listens on all ports/channels).
The reference file incorrectly marked these as required, causing
UnknownObjectWarning when placing notein without args.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add bounds check in load_amxd to catch truncated/corrupt .amxd files
- Use removesuffix(b"\x00") instead of rstrip for precise null handling
- Add TypeError guard in save_amxd for non-dict input
- Fix truthiness check to explicit `is not None` in save() verbose path
- Update load_file docstring to mention .amxd support
- Fix misleading comment in extension handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use rstrip(b"\x00") instead of removesuffix to handle multiple trailing nulls
- Compare chunk tags as raw bytes instead of decoding to ASCII
- Wrap JSONDecodeError with filename context for easier debugging
- Include filename in "no ptch chunk" error message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@santolucito
Copy link
Copy Markdown
Member

Can we add something in CI that tests this?

@santolucito
Copy link
Copy Markdown
Member

like maybe we shoudl at least just run maxpy on the example folder in ci to make sure we dont break these at least from a maxpy pass. i dont know that there is any good way to have full integration tests

Expand CI to run M4L examples (m4l_instrument, m4l_audio_effect),
attributes, and random_pitch_generator alongside hello_world.
Add pytest step with 81 tests covering amxd support and backward compat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@chrishyoroklee
Copy link
Copy Markdown
Collaborator Author

@santolucito
I had Claude set these up in CI:

  • 5 standalone examples run in CI (hello_world, m4l_instrument, m4l_audio_effect, attributes, random_pitch_generator)
  • 81 pytest tests (amxd + backward compat)
  • Output file verification for each example

Comment thread maxpylang/tools/patchfuncs/instantiation.py Outdated
Comment thread tests/test_backward_compat.py Outdated
- load_file: change else to elif .maxpat with ValueError on unknown extensions
- Remove test_backward_compat.py per reviewer request (one-time verification)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@santolucito santolucito merged commit 19c1b31 into main Mar 30, 2026
5 checks passed
@santolucito santolucito deleted the Max-For-Live-Support branch March 30, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants