livepilot 1.9.23 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +119 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +144 -13
- package/bin/livepilot.js +87 -0
- package/installer/codex.js +147 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +21 -4
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
- package/livepilot/skills/livepilot-core/references/overview.md +13 -9
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
- package/livepilot/skills/livepilot-devices/SKILL.md +16 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +19 -5
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +15 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +2 -2
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +357 -0
- package/mcp_server/atlas/device_atlas.json +44067 -0
- package/mcp_server/atlas/enrichments/__init__.py +111 -0
- package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
- package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
- package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
- package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
- package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
- package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
- package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
- package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
- package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
- package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
- package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
- package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
- package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
- package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
- package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
- package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
- package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
- package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
- package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
- package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
- package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
- package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
- package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
- package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
- package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
- package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
- package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
- package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
- package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
- package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
- package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
- package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
- package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
- package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
- package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
- package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
- package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
- package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
- package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
- package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
- package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
- package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
- package/mcp_server/atlas/scanner.py +236 -0
- package/mcp_server/atlas/tools.py +224 -0
- package/mcp_server/composer/__init__.py +1 -0
- package/mcp_server/composer/engine.py +452 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/tools.py +201 -0
- package/mcp_server/connection.py +53 -8
- package/mcp_server/corpus/__init__.py +377 -0
- package/mcp_server/device_forge/__init__.py +1 -0
- package/mcp_server/device_forge/builder.py +377 -0
- package/mcp_server/device_forge/models.py +142 -0
- package/mcp_server/device_forge/templates.py +483 -0
- package/mcp_server/device_forge/tools.py +162 -0
- package/mcp_server/hook_hunter/analyzer.py +23 -0
- package/mcp_server/hook_hunter/models.py +1 -0
- package/mcp_server/hook_hunter/tools.py +4 -2
- package/mcp_server/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_graph.py +68 -1
- package/mcp_server/memory/tools.py +15 -4
- package/mcp_server/musical_intelligence/detectors.py +14 -1
- package/mcp_server/musical_intelligence/tools.py +11 -8
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -0
- package/mcp_server/preview_studio/models.py +1 -0
- package/mcp_server/preview_studio/tools.py +56 -13
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +137 -0
- package/mcp_server/runtime/execution_router.py +143 -0
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/remote_commands.py +87 -0
- package/mcp_server/runtime/tools.py +18 -4
- package/mcp_server/sample_engine/__init__.py +1 -0
- package/mcp_server/sample_engine/analyzer.py +216 -0
- package/mcp_server/sample_engine/critics.py +390 -0
- package/mcp_server/sample_engine/models.py +193 -0
- package/mcp_server/sample_engine/moves.py +127 -0
- package/mcp_server/sample_engine/planner.py +186 -0
- package/mcp_server/sample_engine/sources.py +540 -0
- package/mcp_server/sample_engine/techniques.py +908 -0
- package/mcp_server/sample_engine/tools.py +442 -0
- package/mcp_server/semantic_moves/__init__.py +3 -0
- package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
- package/mcp_server/semantic_moves/mix_moves.py +41 -41
- package/mcp_server/semantic_moves/performance_moves.py +13 -13
- package/mcp_server/semantic_moves/sample_compilers.py +372 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
- package/mcp_server/semantic_moves/tools.py +18 -17
- package/mcp_server/semantic_moves/transition_moves.py +16 -16
- package/mcp_server/server.py +51 -0
- package/mcp_server/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -0
- package/mcp_server/session_continuity/tracker.py +29 -1
- package/mcp_server/song_brain/builder.py +28 -1
- package/mcp_server/song_brain/models.py +4 -0
- package/mcp_server/song_brain/tools.py +20 -2
- package/mcp_server/sound_design/critics.py +89 -1
- package/mcp_server/splice_client/__init__.py +1 -0
- package/mcp_server/splice_client/client.py +347 -0
- package/mcp_server/splice_client/models.py +96 -0
- package/mcp_server/splice_client/protos/__init__.py +1 -0
- package/mcp_server/splice_client/protos/app_pb2.py +319 -0
- package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
- package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
- package/mcp_server/tools/arrangement.py +69 -0
- package/mcp_server/tools/automation.py +15 -2
- package/mcp_server/tools/devices.py +117 -6
- package/mcp_server/tools/notes.py +37 -4
- package/mcp_server/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +85 -1
- package/mcp_server/wonder_mode/tools.py +6 -1
- package/package.json +12 -2
- package/remote_script/LivePilot/__init__.py +8 -1
- package/remote_script/LivePilot/arrangement.py +114 -0
- package/remote_script/LivePilot/browser.py +56 -1
- package/remote_script/LivePilot/devices.py +236 -6
- package/remote_script/LivePilot/mixing.py +8 -3
- package/remote_script/LivePilot/server.py +5 -1
- package/remote_script/LivePilot/transport.py +3 -0
- package/remote_script/LivePilot/version_detect.py +78 -0
- package/scripts/sync_metadata.py +132 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Build .amxd binary files from DeviceSpec — pure Python, no dependencies.
|
|
2
|
+
|
|
3
|
+
Binary format (reverse-engineered from LivePilot_Analyzer.amxd):
|
|
4
|
+
Offset 0x00: "ampf" + uint32_LE(4) + device_marker (4 bytes)
|
|
5
|
+
Offset 0x0C: "meta" + uint32_LE(4) + uint32_LE(meta_value)
|
|
6
|
+
Offset 0x18: "ptch" + uint32_LE(content_size)
|
|
7
|
+
Offset 0x20: "mx@c" + uint32_BE(16) + uint32_BE(0) + uint32_BE(json_size)
|
|
8
|
+
Offset 0x30: JSON patcher (UTF-8)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import struct
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .models import DeviceSpec, DeviceType, GenExprParam
|
|
18
|
+
|
|
19
|
+
ABLETON_USER_LIBRARY = Path.home() / "Music" / "Ableton" / "User Library"
|
|
20
|
+
|
|
21
|
+
_SUBDIR_MAP = {
|
|
22
|
+
DeviceType.AUDIO_EFFECT: "Presets/Audio Effects/Max Audio Effect",
|
|
23
|
+
DeviceType.MIDI_EFFECT: "Presets/MIDI Effects/Max MIDI Effect",
|
|
24
|
+
DeviceType.INSTRUMENT: "Presets/Instruments/Max Instrument",
|
|
25
|
+
DeviceType.MIDI_GENERATOR: "Presets/MIDI Effects/Max MIDI Effect",
|
|
26
|
+
DeviceType.MIDI_TRANSFORMATION: "Presets/MIDI Effects/Max MIDI Effect",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── JSON Patcher Generation ─────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _make_box(obj_id: str, maxclass: str, text: str,
|
|
34
|
+
numinlets: int, numoutlets: int,
|
|
35
|
+
outlettype: list[str], rect: list[float],
|
|
36
|
+
**extra) -> dict:
|
|
37
|
+
"""Create a single Max box dict."""
|
|
38
|
+
box: dict = {
|
|
39
|
+
"id": obj_id,
|
|
40
|
+
"maxclass": maxclass,
|
|
41
|
+
"text": text,
|
|
42
|
+
"numinlets": numinlets,
|
|
43
|
+
"numoutlets": numoutlets,
|
|
44
|
+
"outlettype": outlettype,
|
|
45
|
+
"patching_rect": rect,
|
|
46
|
+
}
|
|
47
|
+
box.update(extra)
|
|
48
|
+
return {"box": box}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _make_line(src_id: str, src_outlet: int,
|
|
52
|
+
dst_id: str, dst_inlet: int) -> dict:
|
|
53
|
+
return {"patchline": {"source": [src_id, src_outlet],
|
|
54
|
+
"destination": [dst_id, dst_inlet]}}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ensure_safety_clip(code: str) -> str:
|
|
58
|
+
"""Append safety clipping to gen~ code to prevent speaker damage."""
|
|
59
|
+
safe = code.rstrip()
|
|
60
|
+
if "clip(" in safe.lower():
|
|
61
|
+
return safe
|
|
62
|
+
|
|
63
|
+
# Find output assignments and wrap them
|
|
64
|
+
lines = safe.split("\n")
|
|
65
|
+
new_lines = []
|
|
66
|
+
for line in lines:
|
|
67
|
+
stripped = line.strip()
|
|
68
|
+
if stripped.startswith("out") and "=" in stripped:
|
|
69
|
+
var = stripped.split("=")[0].strip()
|
|
70
|
+
new_lines.append(line)
|
|
71
|
+
new_lines.append(f"{var} = clip({var}, -1, 1);")
|
|
72
|
+
else:
|
|
73
|
+
new_lines.append(line)
|
|
74
|
+
return "\n".join(new_lines)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_gen_patcher(spec: DeviceSpec) -> dict:
|
|
78
|
+
"""Build the gen~ sub-patcher with codebox containing user's GenExpr code."""
|
|
79
|
+
safe_code = _ensure_safety_clip(spec.gen_code)
|
|
80
|
+
|
|
81
|
+
boxes = []
|
|
82
|
+
lines = []
|
|
83
|
+
|
|
84
|
+
# Codebox — maxclass MUST be "codebox" (not "newobj" with text "codebox")
|
|
85
|
+
# Canonical format verified against 18 factory codebox objects
|
|
86
|
+
boxes.append({
|
|
87
|
+
"box": {
|
|
88
|
+
"id": "obj-codebox",
|
|
89
|
+
"maxclass": "codebox",
|
|
90
|
+
"numinlets": 1,
|
|
91
|
+
"numoutlets": 1,
|
|
92
|
+
"outlettype": [""],
|
|
93
|
+
"patching_rect": [50.0, 100.0, 400.0, 200.0],
|
|
94
|
+
"fontface": 0,
|
|
95
|
+
"fontname": "<Monospaced>",
|
|
96
|
+
"fontsize": 12.0,
|
|
97
|
+
"code": safe_code,
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
# in 1 (audio or data input)
|
|
102
|
+
boxes.append(_make_box("obj-in1", "newobj", "in 1", 0, 1, [""], [50.0, 30.0, 30.0, 22.0]))
|
|
103
|
+
lines.append(_make_line("obj-in1", 0, "obj-codebox", 0))
|
|
104
|
+
|
|
105
|
+
# out 1
|
|
106
|
+
boxes.append(_make_box("obj-out1", "newobj", "out 1", 1, 0, [], [50.0, 350.0, 35.0, 22.0]))
|
|
107
|
+
lines.append(_make_line("obj-codebox", 0, "obj-out1", 0))
|
|
108
|
+
|
|
109
|
+
# Param objects for each parameter
|
|
110
|
+
for i, param in enumerate(spec.params):
|
|
111
|
+
param_id = f"obj-param{i}"
|
|
112
|
+
boxes.append(_make_box(
|
|
113
|
+
param_id, "newobj",
|
|
114
|
+
f"param {param.name} @default {param.default} @min {param.min_val} @max {param.max_val}",
|
|
115
|
+
0, 1, [""], [200.0 + i * 120, 30.0, 150.0, 22.0],
|
|
116
|
+
))
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"fileversion": 1,
|
|
120
|
+
"appversion": {"major": 9, "minor": 0, "revision": 5,
|
|
121
|
+
"architecture": "x64", "modernui": 1},
|
|
122
|
+
"classnamespace": "dsp.gen",
|
|
123
|
+
"rect": [100.0, 100.0, 600.0, 450.0],
|
|
124
|
+
"boxes": boxes,
|
|
125
|
+
"lines": lines,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _patcher_boilerplate(spec: DeviceSpec) -> dict:
|
|
130
|
+
"""Common patcher-level fields for all device types."""
|
|
131
|
+
return {
|
|
132
|
+
"fileversion": 1,
|
|
133
|
+
"appversion": {"major": 9, "minor": 0, "revision": 5,
|
|
134
|
+
"architecture": "x64", "modernui": 1},
|
|
135
|
+
"classnamespace": "box",
|
|
136
|
+
"rect": [100.0, 100.0, 800.0, 600.0],
|
|
137
|
+
"openinpresentation": 1,
|
|
138
|
+
"default_fontsize": 12.0,
|
|
139
|
+
"default_fontface": 0,
|
|
140
|
+
"default_fontname": "Arial",
|
|
141
|
+
"gridonopen": 1,
|
|
142
|
+
"gridsize": [15.0, 15.0],
|
|
143
|
+
"gridsnaponopen": 1,
|
|
144
|
+
"objectsnaponopen": 1,
|
|
145
|
+
"statusbarvisible": 2,
|
|
146
|
+
"toolbarvisible": 1,
|
|
147
|
+
"lefttoolbarpinned": 0,
|
|
148
|
+
"toptoolbarpinned": 0,
|
|
149
|
+
"righttoolbarpinned": 0,
|
|
150
|
+
"bottomtoolbarpinned": 0,
|
|
151
|
+
"toolbars_unpinned_last_save": 0,
|
|
152
|
+
"tallnewobj": 0,
|
|
153
|
+
"boxanimatetime": 200,
|
|
154
|
+
"enablehscroll": 1,
|
|
155
|
+
"enablevscroll": 1,
|
|
156
|
+
"devicewidth": float(spec.width),
|
|
157
|
+
"description": spec.description,
|
|
158
|
+
"digest": spec.description,
|
|
159
|
+
"tags": spec.tags,
|
|
160
|
+
"style": "",
|
|
161
|
+
"subpatcher_template": "",
|
|
162
|
+
"assistshowspatchername": 0,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _build_audio_effect_patcher(spec: DeviceSpec) -> dict:
|
|
167
|
+
"""Build patcher for an audio effect: plugin~ -> gen~ -> plugout~."""
|
|
168
|
+
boxes = []
|
|
169
|
+
lines = []
|
|
170
|
+
_counter = [0]
|
|
171
|
+
|
|
172
|
+
def nid():
|
|
173
|
+
_counter[0] += 1
|
|
174
|
+
return f"obj-{_counter[0]}"
|
|
175
|
+
|
|
176
|
+
# Background panel — background=1 sends it behind all other UI elements
|
|
177
|
+
pid = nid()
|
|
178
|
+
boxes.append({
|
|
179
|
+
"box": {
|
|
180
|
+
"id": pid, "maxclass": "panel", "numinlets": 1, "numoutlets": 0,
|
|
181
|
+
"patching_rect": [0.0, 0.0, float(spec.width), float(spec.height)],
|
|
182
|
+
"presentation": 1,
|
|
183
|
+
"presentation_rect": [0.0, 0.0, float(spec.width), float(spec.height)],
|
|
184
|
+
"bgcolor": [0.12, 0.12, 0.12, 1.0],
|
|
185
|
+
"background": 1,
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
# plugin~ — numinlets=2 so Live can feed audio into the device
|
|
190
|
+
plugin_id = nid()
|
|
191
|
+
boxes.append(_make_box(plugin_id, "newobj", "plugin~", 2, 2,
|
|
192
|
+
["signal", "signal"], [50.0, 30.0, 65.0, 22.0]))
|
|
193
|
+
|
|
194
|
+
# plugout~
|
|
195
|
+
plugout_id = nid()
|
|
196
|
+
boxes.append(_make_box(plugout_id, "newobj", "plugout~", 2, 2,
|
|
197
|
+
["signal", "signal"], [50.0, 400.0, 70.0, 22.0]))
|
|
198
|
+
|
|
199
|
+
# gen~ with embedded patcher
|
|
200
|
+
gen_id = nid()
|
|
201
|
+
boxes.append({
|
|
202
|
+
"box": {
|
|
203
|
+
"id": gen_id, "maxclass": "newobj", "text": "gen~",
|
|
204
|
+
"numinlets": 1, "numoutlets": 1, "outlettype": ["signal"],
|
|
205
|
+
"patching_rect": [50.0, 200.0, 300.0, 22.0],
|
|
206
|
+
"patcher": _build_gen_patcher(spec),
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
# Signal path: L channel through gen~, R channel direct passthrough
|
|
211
|
+
# plugin~ L -> gen~ -> plugout~ L
|
|
212
|
+
lines.append(_make_line(plugin_id, 0, gen_id, 0))
|
|
213
|
+
lines.append(_make_line(gen_id, 0, plugout_id, 0))
|
|
214
|
+
# plugin~ R -> plugout~ R (direct passthrough)
|
|
215
|
+
lines.append(_make_line(plugin_id, 1, plugout_id, 1))
|
|
216
|
+
|
|
217
|
+
# live.dial for each parameter
|
|
218
|
+
for i, param in enumerate(spec.params):
|
|
219
|
+
did = nid()
|
|
220
|
+
x = 10.0 + i * 54.0
|
|
221
|
+
boxes.append(param.to_live_dial_json(did, [x, 10.0, 44.0, 48.0]))
|
|
222
|
+
|
|
223
|
+
# Title comment
|
|
224
|
+
tid = nid()
|
|
225
|
+
boxes.append({
|
|
226
|
+
"box": {
|
|
227
|
+
"id": tid, "maxclass": "comment", "text": spec.name,
|
|
228
|
+
"numinlets": 1, "numoutlets": 0,
|
|
229
|
+
"patching_rect": [50.0, 440.0, 200.0, 20.0],
|
|
230
|
+
"presentation": 1,
|
|
231
|
+
"presentation_rect": [10.0, float(spec.height - 20), 200.0, 18.0],
|
|
232
|
+
"textcolor": [0.7, 0.7, 0.7, 1.0], "fontsize": 10.0,
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
p = _patcher_boilerplate(spec)
|
|
237
|
+
p["boxes"] = boxes
|
|
238
|
+
p["lines"] = lines
|
|
239
|
+
return {"patcher": p}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _build_midi_effect_patcher(spec: DeviceSpec) -> dict:
|
|
243
|
+
boxes = [
|
|
244
|
+
_make_box("obj-1", "newobj", "midiin", 1, 1, ["int"], [50.0, 30.0, 50.0, 22.0]),
|
|
245
|
+
_make_box("obj-2", "newobj", "midiout", 1, 0, [], [50.0, 300.0, 55.0, 22.0]),
|
|
246
|
+
]
|
|
247
|
+
lines = [_make_line("obj-1", 0, "obj-2", 0)]
|
|
248
|
+
p = _patcher_boilerplate(spec)
|
|
249
|
+
p["boxes"] = boxes
|
|
250
|
+
p["lines"] = lines
|
|
251
|
+
return {"patcher": p}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _build_instrument_patcher(spec: DeviceSpec) -> dict:
|
|
255
|
+
boxes = [
|
|
256
|
+
_make_box("obj-mi", "newobj", "midiin", 1, 1, ["int"], [50.0, 30.0, 50.0, 22.0]),
|
|
257
|
+
_make_box("obj-mp", "newobj", "midiparse", 1, 8,
|
|
258
|
+
["", "", "", "", "", "", "", ""], [50.0, 70.0, 100.0, 22.0]),
|
|
259
|
+
_make_box("obj-mtof", "newobj", "mtof", 1, 1, [""], [50.0, 110.0, 40.0, 22.0]),
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
gen_patcher = _build_gen_patcher(spec)
|
|
263
|
+
boxes.append({
|
|
264
|
+
"box": {
|
|
265
|
+
"id": "obj-gen", "maxclass": "newobj", "text": "gen~",
|
|
266
|
+
"numinlets": 1, "numoutlets": 1, "outlettype": ["signal"],
|
|
267
|
+
"patching_rect": [50.0, 200.0, 300.0, 22.0],
|
|
268
|
+
"patcher": gen_patcher,
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
boxes.append(_make_box("obj-po", "newobj", "plugout~", 2, 2,
|
|
272
|
+
["signal", "signal"], [50.0, 300.0, 70.0, 22.0]))
|
|
273
|
+
|
|
274
|
+
lines = [
|
|
275
|
+
_make_line("obj-mi", 0, "obj-mp", 0),
|
|
276
|
+
_make_line("obj-mp", 0, "obj-mtof", 0),
|
|
277
|
+
_make_line("obj-mtof", 0, "obj-gen", 0),
|
|
278
|
+
_make_line("obj-gen", 0, "obj-po", 0),
|
|
279
|
+
_make_line("obj-gen", 0, "obj-po", 1),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
p = _patcher_boilerplate(spec)
|
|
283
|
+
p["boxes"] = boxes
|
|
284
|
+
p["lines"] = lines
|
|
285
|
+
return {"patcher": p}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
_PATCHER_BUILDERS = {
|
|
289
|
+
DeviceType.AUDIO_EFFECT: _build_audio_effect_patcher,
|
|
290
|
+
DeviceType.MIDI_EFFECT: _build_midi_effect_patcher,
|
|
291
|
+
DeviceType.INSTRUMENT: _build_instrument_patcher,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def build_patcher_json(spec: DeviceSpec) -> dict:
|
|
296
|
+
"""Build the complete .maxpat JSON patcher dict for a device spec."""
|
|
297
|
+
return _PATCHER_BUILDERS[spec.device_type](spec)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def build_amxd_binary(spec: DeviceSpec) -> bytes:
|
|
301
|
+
"""Build the complete .amxd binary from a device spec.
|
|
302
|
+
|
|
303
|
+
Unfrozen .amxd format (32-byte header + JSON):
|
|
304
|
+
ampf(4) + uint32_LE(4) + device_marker(4) = 12 bytes
|
|
305
|
+
meta(4) + uint32_LE(4) + uint32_LE(0) = 12 bytes
|
|
306
|
+
ptch(4) + uint32_LE(json_size) = 8 bytes
|
|
307
|
+
JSON patcher (UTF-8)
|
|
308
|
+
|
|
309
|
+
Note: The mx@c wrapper is only used for FROZEN devices with embedded
|
|
310
|
+
dependencies. Unfrozen devices put JSON directly after the ptch header.
|
|
311
|
+
"""
|
|
312
|
+
patcher = build_patcher_json(spec)
|
|
313
|
+
json_bytes = json.dumps(patcher, indent="\t", separators=(",", " : "),
|
|
314
|
+
ensure_ascii=False).encode("utf-8")
|
|
315
|
+
|
|
316
|
+
dt = spec.device_type
|
|
317
|
+
|
|
318
|
+
# ampf header (12 bytes)
|
|
319
|
+
header = b"ampf"
|
|
320
|
+
header += struct.pack("<I", 4)
|
|
321
|
+
header += dt.ampf_marker
|
|
322
|
+
|
|
323
|
+
# meta chunk (12 bytes) — meta_value=0 for unfrozen devices
|
|
324
|
+
header += b"meta"
|
|
325
|
+
header += struct.pack("<I", 4)
|
|
326
|
+
header += struct.pack("<I", 0)
|
|
327
|
+
|
|
328
|
+
# ptch chunk (8 bytes) — size = JSON byte length
|
|
329
|
+
header += b"ptch"
|
|
330
|
+
header += struct.pack("<I", len(json_bytes))
|
|
331
|
+
|
|
332
|
+
return header + json_bytes
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def parse_amxd_header(data: bytes) -> dict:
|
|
336
|
+
"""Parse an .amxd binary header. Returns dict with metadata."""
|
|
337
|
+
if len(data) < 32 or data[:4] != b"ampf":
|
|
338
|
+
raise ValueError("Not a valid .amxd file")
|
|
339
|
+
|
|
340
|
+
marker = data[8:12]
|
|
341
|
+
type_map = {
|
|
342
|
+
b"aaaa": "audio_effect", b"mmmm": "midi_effect", b"iiii": "instrument",
|
|
343
|
+
b"nagg": "midi_generator", b"natt": "midi_transformation",
|
|
344
|
+
}
|
|
345
|
+
ptch_size = struct.unpack("<I", data[28:32])[0]
|
|
346
|
+
|
|
347
|
+
# Detect frozen vs unfrozen: frozen has mx@c at offset 32
|
|
348
|
+
frozen = data[32:36] == b"mx@c"
|
|
349
|
+
json_offset = 48 if frozen else 32
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
"device_type": type_map.get(marker, "unknown"),
|
|
353
|
+
"meta_value": struct.unpack("<I", data[20:24])[0],
|
|
354
|
+
"ptch_size": ptch_size,
|
|
355
|
+
"json_offset": json_offset,
|
|
356
|
+
"frozen": frozen,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def build_device(spec: DeviceSpec, output_dir: str | Path | None = None) -> Path:
|
|
361
|
+
"""Build an .amxd file and write it to disk.
|
|
362
|
+
|
|
363
|
+
If output_dir is None, writes to the Ableton User Library.
|
|
364
|
+
Returns the path to the created file.
|
|
365
|
+
"""
|
|
366
|
+
data = build_amxd_binary(spec)
|
|
367
|
+
|
|
368
|
+
if output_dir is None:
|
|
369
|
+
subdir = _SUBDIR_MAP[spec.device_type]
|
|
370
|
+
output_dir = ABLETON_USER_LIBRARY / subdir
|
|
371
|
+
|
|
372
|
+
output_dir = Path(output_dir)
|
|
373
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
374
|
+
|
|
375
|
+
path = output_dir / spec.safe_filename
|
|
376
|
+
path.write_bytes(data)
|
|
377
|
+
return path
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Device Forge data models — specs for generated M4L devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeviceType(Enum):
|
|
11
|
+
"""M4L device types with binary format metadata."""
|
|
12
|
+
|
|
13
|
+
AUDIO_EFFECT = ("aaaa", 7, "Max Audio Effect", ("plugin~", "plugout~"))
|
|
14
|
+
MIDI_EFFECT = ("mmmm", 1, "Max MIDI Effect", ("midiin", "midiout"))
|
|
15
|
+
INSTRUMENT = ("iiii", 2, "Max Instrument", ("midiin", "plugout~"))
|
|
16
|
+
MIDI_GENERATOR = ("nagg", 3, "Max MIDI Generator", ("midiout",))
|
|
17
|
+
MIDI_TRANSFORMATION = ("natt", 4, "Max MIDI Transformation", ("midiin", "midiout"))
|
|
18
|
+
|
|
19
|
+
def __init__(self, ampf: str, meta: int, title: str, io: tuple):
|
|
20
|
+
self._ampf = ampf.encode("ascii")
|
|
21
|
+
self._meta = meta
|
|
22
|
+
self._title = title
|
|
23
|
+
self._io = io
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def ampf_marker(self) -> bytes:
|
|
27
|
+
return self._ampf
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def meta_value(self) -> int:
|
|
31
|
+
return self._meta
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def title(self) -> str:
|
|
35
|
+
return self._title
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def required_io(self) -> tuple:
|
|
39
|
+
return self._io
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Parameter unit styles matching Live's enum
|
|
43
|
+
UNIT_STYLE_INT = 0
|
|
44
|
+
UNIT_STYLE_FLOAT = 1
|
|
45
|
+
UNIT_STYLE_TIME = 2
|
|
46
|
+
UNIT_STYLE_HERTZ = 3
|
|
47
|
+
UNIT_STYLE_DB = 4
|
|
48
|
+
UNIT_STYLE_PERCENT = 5
|
|
49
|
+
UNIT_STYLE_PAN = 6
|
|
50
|
+
UNIT_STYLE_SEMITONES = 7
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class GenExprParam:
|
|
55
|
+
"""A gen~ parameter exposed to Ableton as a live.dial."""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
default: float = 0.5
|
|
59
|
+
min_val: float = 0.0
|
|
60
|
+
max_val: float = 1.0
|
|
61
|
+
unit_style: int = UNIT_STYLE_FLOAT
|
|
62
|
+
exponent: float = 1.0 # 1.0 = linear
|
|
63
|
+
|
|
64
|
+
def to_genexpr(self) -> str:
|
|
65
|
+
"""Generate the Param declaration for gen~ codebox."""
|
|
66
|
+
return f"Param {self.name}({self.default});"
|
|
67
|
+
|
|
68
|
+
def to_live_dial_json(self, obj_id: str, rect: list[float]) -> dict:
|
|
69
|
+
"""Generate the JSON for a live.dial box wired to this parameter."""
|
|
70
|
+
return {
|
|
71
|
+
"box": {
|
|
72
|
+
"id": obj_id,
|
|
73
|
+
"maxclass": "live.dial",
|
|
74
|
+
"numinlets": 1,
|
|
75
|
+
"numoutlets": 2,
|
|
76
|
+
"outlettype": ["", "float"],
|
|
77
|
+
"parameter_enable": 1,
|
|
78
|
+
"patching_rect": rect,
|
|
79
|
+
"presentation": 1,
|
|
80
|
+
"presentation_rect": rect,
|
|
81
|
+
"saved_attribute_attributes": {
|
|
82
|
+
"valueof": {
|
|
83
|
+
"parameter_longname": self.name,
|
|
84
|
+
"parameter_shortname": self.name[:7],
|
|
85
|
+
"parameter_type": 0,
|
|
86
|
+
"parameter_mmin": self.min_val,
|
|
87
|
+
"parameter_mmax": self.max_val,
|
|
88
|
+
"parameter_unitstyle": self.unit_style,
|
|
89
|
+
"parameter_initial_enable": 1,
|
|
90
|
+
"parameter_initial": [self.default],
|
|
91
|
+
"parameter_exponent": self.exponent,
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"varname": self.name,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class GenExprTemplate:
|
|
101
|
+
"""A reusable gen~ DSP building block."""
|
|
102
|
+
|
|
103
|
+
template_id: str
|
|
104
|
+
name: str
|
|
105
|
+
description: str
|
|
106
|
+
category: str # chaos, delay, distortion, filter, modulation, synthesis, texture, utility
|
|
107
|
+
code: str # GenExpr source code
|
|
108
|
+
params: list[GenExprParam] = field(default_factory=list)
|
|
109
|
+
num_inputs: int = 1
|
|
110
|
+
num_outputs: int = 1
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict:
|
|
113
|
+
"""Summary dict — does NOT expose raw code."""
|
|
114
|
+
return {
|
|
115
|
+
"template_id": self.template_id,
|
|
116
|
+
"name": self.name,
|
|
117
|
+
"description": self.description,
|
|
118
|
+
"category": self.category,
|
|
119
|
+
"params": [p.name for p in self.params],
|
|
120
|
+
"num_inputs": self.num_inputs,
|
|
121
|
+
"num_outputs": self.num_outputs,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class DeviceSpec:
|
|
127
|
+
"""Complete specification for a generated M4L device."""
|
|
128
|
+
|
|
129
|
+
name: str
|
|
130
|
+
device_type: DeviceType
|
|
131
|
+
gen_code: str # GenExpr source for the gen~ codebox
|
|
132
|
+
description: str = ""
|
|
133
|
+
params: list[GenExprParam] = field(default_factory=list)
|
|
134
|
+
width: int = 300
|
|
135
|
+
height: int = 100
|
|
136
|
+
tags: str = "livepilot generated"
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def safe_filename(self) -> str:
|
|
140
|
+
"""Filesystem-safe .amxd filename."""
|
|
141
|
+
clean = re.sub(r"[^a-zA-Z0-9_ ]", "", self.name)
|
|
142
|
+
return clean.strip().replace(" ", "_") + ".amxd"
|