livepilot 1.23.6 → 1.25.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/CHANGELOG.md +107 -0
- package/README.md +60 -14
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +17 -3
- package/mcp_server/atlas/explore_tools.py +332 -0
- package/mcp_server/atlas/tools.py +161 -0
- package/mcp_server/audit/__init__.py +6 -0
- package/mcp_server/audit/checks.py +618 -0
- package/mcp_server/audit/tools.py +232 -0
- package/mcp_server/composer/branch_producer.py +5 -2
- package/mcp_server/composer/develop/__init__.py +19 -0
- package/mcp_server/composer/develop/apply.py +217 -0
- package/mcp_server/composer/develop/brief_builder.py +269 -0
- package/mcp_server/composer/develop/seed_introspector.py +195 -0
- package/mcp_server/composer/engine.py +15 -521
- package/mcp_server/composer/fast/__init__.py +62 -0
- package/mcp_server/composer/fast/apply.py +533 -0
- package/mcp_server/composer/fast/brief_builder.py +1479 -0
- package/mcp_server/composer/fast/tier_classification.py +159 -0
- package/mcp_server/composer/framework/__init__.py +0 -0
- package/mcp_server/composer/framework/applier.py +179 -0
- package/mcp_server/composer/framework/artist_loader.py +63 -0
- package/mcp_server/composer/framework/atlas_resolver.py +554 -0
- package/mcp_server/composer/framework/brief.py +79 -0
- package/mcp_server/composer/framework/event_lexicon.py +71 -0
- package/mcp_server/composer/framework/genre_loader.py +77 -0
- package/mcp_server/composer/framework/intent_source.py +137 -0
- package/mcp_server/composer/framework/knowledge_pack.py +140 -0
- package/mcp_server/composer/framework/plan_compiler.py +10 -0
- package/mcp_server/composer/full/__init__.py +10 -0
- package/mcp_server/composer/full/apply.py +1139 -0
- package/mcp_server/composer/full/brief_builder.py +227 -0
- package/mcp_server/composer/full/engine.py +541 -0
- package/mcp_server/composer/full/layer_planner.py +491 -0
- package/mcp_server/composer/layer_planner.py +19 -465
- package/mcp_server/composer/sample_resolver.py +80 -7
- package/mcp_server/composer/tools.py +626 -28
- package/mcp_server/server.py +1 -0
- package/mcp_server/splice_client/client.py +7 -0
- package/mcp_server/tools/_analyzer_engine/sample.py +172 -7
- package/mcp_server/tools/_planner_engine.py +25 -63
- package/mcp_server/tools/analyzer.py +10 -4
- package/mcp_server/tools/browser.py +102 -19
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +3 -3
package/mcp_server/server.py
CHANGED
|
@@ -308,6 +308,7 @@ from .atlas import tools as atlas_tools # noqa: F401, E40
|
|
|
308
308
|
from .composer import tools as composer_tools # noqa: F401, E402
|
|
309
309
|
from .synthesis_brain import tools as synthesis_brain_tools # noqa: F401, E402
|
|
310
310
|
from .user_corpus import tools as user_corpus_tools # noqa: F401, E402
|
|
311
|
+
from .audit import tools as audit_tools # noqa: F401, E402
|
|
311
312
|
from .tools import diagnostics # noqa: F401, E402
|
|
312
313
|
from .tools import miditool # noqa: F401, E402
|
|
313
314
|
|
|
@@ -218,6 +218,7 @@ class SpliceGRPCClient:
|
|
|
218
218
|
tags: Optional[list[str]] = None,
|
|
219
219
|
genre: str = "",
|
|
220
220
|
sample_type: str = "",
|
|
221
|
+
instrument: str = "",
|
|
221
222
|
sort: str = "",
|
|
222
223
|
per_page: int = 20,
|
|
223
224
|
page: int = 1,
|
|
@@ -230,6 +231,11 @@ class SpliceGRPCClient:
|
|
|
230
231
|
`collection_uuid` scopes search to a single user collection
|
|
231
232
|
(e.g. "Likes", "bass") — pure taste signal when present.
|
|
232
233
|
`file_hash` is a direct lookup for a single sample.
|
|
234
|
+
`instrument` filters by Splice's instrument category — examples
|
|
235
|
+
the gRPC schema accepts include "bass", "drum", "synth",
|
|
236
|
+
"piano", "vocal", "fx", "guitar", "pad". Crucial for full-mode
|
|
237
|
+
composition where role-correctness matters more than text-match
|
|
238
|
+
on free-form query strings (BUG-FULL-MODE-9, 2026-05-01).
|
|
233
239
|
"""
|
|
234
240
|
if not self.connected:
|
|
235
241
|
return SpliceSearchResult()
|
|
@@ -248,6 +254,7 @@ class SpliceGRPCClient:
|
|
|
248
254
|
BPMMax=bpm_max,
|
|
249
255
|
Tags=tags or [],
|
|
250
256
|
Genre=genre,
|
|
257
|
+
Instrument=instrument,
|
|
251
258
|
SampleType=sample_type,
|
|
252
259
|
SortFn=sort,
|
|
253
260
|
PerPage=per_page,
|
|
@@ -66,10 +66,63 @@ _DRUM_ROOT_MAP = {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
_LOOP_PATH_HINTS = (
|
|
70
|
+
"/loops/",
|
|
71
|
+
"/drum_loops/",
|
|
72
|
+
"/melodic_loops/",
|
|
73
|
+
"/pad_loops/",
|
|
74
|
+
"/bass_loops/",
|
|
75
|
+
"/synth_loops/",
|
|
76
|
+
"/perc_loops/",
|
|
77
|
+
"/vocal_loops/",
|
|
78
|
+
"/fx_loops/",
|
|
79
|
+
)
|
|
80
|
+
_ONESHOT_HINTS = (
|
|
81
|
+
"oneshot",
|
|
82
|
+
"one_shot",
|
|
83
|
+
"one-shot",
|
|
84
|
+
"_os_",
|
|
85
|
+
"/oneshots/",
|
|
86
|
+
"/one_shots/",
|
|
87
|
+
"/one-shots/",
|
|
88
|
+
)
|
|
89
|
+
_LOOP_FILENAME_RE = re.compile(
|
|
90
|
+
r"(?:_|\b)\d{2,3}(?:_|bpm|\b)|(?:_|\b)loop(?:_|\b)",
|
|
91
|
+
re.IGNORECASE,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
69
95
|
def _is_warped_loop(file_path: str) -> bool:
|
|
70
|
-
"""Return True if the
|
|
96
|
+
"""Return True if the file is likely a tempo-locked loop sample.
|
|
97
|
+
|
|
98
|
+
2026-05-01 broadening (BUG-FULL-MODE-3): the original regex only matched
|
|
99
|
+
"125bpm" / "125 bpm" literal patterns, which fails for the most common
|
|
100
|
+
Splice naming where BPM is embedded as bare digits (e.g.
|
|
101
|
+
`lfh_drums_125_hubble_hatclp.wav`). The broadened detection also looks
|
|
102
|
+
at the path components (`/drum_loops/`, `/melodic_loops/`, etc.) and
|
|
103
|
+
excludes explicit one-shots.
|
|
104
|
+
|
|
105
|
+
Why it matters: the hygiene step that ran on a "false" verdict left
|
|
106
|
+
Simplers without `S Loop On=1`, so the loop never actually loops — it
|
|
107
|
+
plays once and stops. Combined with `Ve Mode=None` (also fixed below),
|
|
108
|
+
every Splice loop loaded into Simpler was silent.
|
|
109
|
+
"""
|
|
110
|
+
full_lower = file_path.lower()
|
|
111
|
+
# One-shots are explicitly NOT warped loops, even when path also has loop hints
|
|
112
|
+
if any(hint in full_lower for hint in _ONESHOT_HINTS):
|
|
113
|
+
return False
|
|
114
|
+
|
|
71
115
|
stem = os.path.splitext(os.path.basename(file_path))[0]
|
|
72
|
-
|
|
116
|
+
if _BPM_IN_FILENAME_RE.search(stem):
|
|
117
|
+
return True
|
|
118
|
+
if _LOOP_FILENAME_RE.search(stem):
|
|
119
|
+
return True
|
|
120
|
+
# Append trailing slash so `/loops/` and `/drum_loops/` match the
|
|
121
|
+
# last directory component cleanly (os.path.dirname strips trailing /).
|
|
122
|
+
parent = os.path.dirname(file_path).lower() + "/"
|
|
123
|
+
if any(seg in parent for seg in _LOOP_PATH_HINTS):
|
|
124
|
+
return True
|
|
125
|
+
return False
|
|
73
126
|
|
|
74
127
|
|
|
75
128
|
def _filename_stem(file_path: str) -> str:
|
|
@@ -104,9 +157,18 @@ async def _simpler_post_load_hygiene(
|
|
|
104
157
|
track_index: int,
|
|
105
158
|
device_index: int,
|
|
106
159
|
file_path: str,
|
|
160
|
+
warp_loops: bool = True,
|
|
107
161
|
) -> dict:
|
|
108
162
|
"""Apply post-load hygiene to a newly loaded Simpler and verify success.
|
|
109
163
|
|
|
164
|
+
`warp_loops` (BUG-FULL-MODE-12, 2026-05-01): when True (default),
|
|
165
|
+
tempo-locked loops get `simpler_set_warp(warping=1, mode=Beats|...)`
|
|
166
|
+
so they play in sync with project tempo. Set False for creative
|
|
167
|
+
chop mode where un-warped loops produce intentional rhythmic
|
|
168
|
+
mismatches (J Dilla territory). compose_full_apply translates its
|
|
169
|
+
`warp_strategy` parameter ("always" / "smart" / "chop") into the
|
|
170
|
+
right per-step boolean before calling this.
|
|
171
|
+
|
|
110
172
|
Steps:
|
|
111
173
|
1. Read track info to verify the device's actual name matches the
|
|
112
174
|
expected sample stem. If it doesn't, return an error.
|
|
@@ -150,13 +212,43 @@ async def _simpler_post_load_hygiene(
|
|
|
150
212
|
"expected_stem": expected_stem,
|
|
151
213
|
}
|
|
152
214
|
|
|
153
|
-
# Step 2:
|
|
215
|
+
# Step 2: post-load defaults
|
|
216
|
+
#
|
|
217
|
+
# Hygiene applied unconditionally (BUG-FULL-MODE-3, 2026-05-01):
|
|
218
|
+
#
|
|
219
|
+
# `Snap=0` — required so non-quantized sample playback works.
|
|
220
|
+
# `Volume=0` — load_browser_item / replace_sample come up at
|
|
221
|
+
# -12 dB (the documented Simpler default) which makes
|
|
222
|
+
# the sample audible-on-track-meter but inaudible on
|
|
223
|
+
# the master meter. 0 dB is the right gain-staged
|
|
224
|
+
# default for any newly loaded sample.
|
|
225
|
+
# Ref: feedback_simpler_default_volume.md.
|
|
226
|
+
#
|
|
227
|
+
# NOTE on Ve Mode (2026-05-01 reconsidered): an earlier draft of this
|
|
228
|
+
# hygiene set `Ve Mode = 4` ("Trigger" / AD-R envelope) so the sample
|
|
229
|
+
# would play "in full" regardless of note duration. That choice was
|
|
230
|
+
# wrong: AD-R retriggers the AD envelope continuously while held,
|
|
231
|
+
# producing audible tremolo on long sustained notes (every 600ms at
|
|
232
|
+
# default Ve Decay). Live's default `Ve Mode = 0` (None — standard
|
|
233
|
+
# ADSR with sustain held until note-off) is the correct idiomatic
|
|
234
|
+
# default, AS LONG AS the trigger note duration matches the clip
|
|
235
|
+
# length. The companion fix is in `engine.py` where the planner now
|
|
236
|
+
# emits `duration = SOURCE_BEATS` for sample-trigger notes.
|
|
237
|
+
#
|
|
238
|
+
# Empirical Ve Mode mapping (live-probed against Live 12.4):
|
|
239
|
+
# 0 = None (default; standard ADSR with sustain) ← keep this
|
|
240
|
+
# 1 = Loop (AD loops while held)
|
|
241
|
+
# 2 = Beat (envelope synced to beat divisions)
|
|
242
|
+
# 3 = Sync (envelope synced to host tempo)
|
|
243
|
+
# 4 = Trigger (AD-R; cycles AD until note-off — caused tremolo bug)
|
|
244
|
+
is_loop = _is_warped_loop(file_path)
|
|
154
245
|
hygiene_params: list[dict] = [
|
|
155
246
|
{"name_or_index": "Snap", "value": 0},
|
|
247
|
+
{"name_or_index": "Volume", "value": 0.0},
|
|
156
248
|
]
|
|
157
249
|
|
|
158
250
|
# Step 3: smart defaults for warped loops
|
|
159
|
-
if
|
|
251
|
+
if is_loop:
|
|
160
252
|
hygiene_params.extend([
|
|
161
253
|
{"name_or_index": "S Start", "value": 0.0},
|
|
162
254
|
{"name_or_index": "S Length", "value": 1.0},
|
|
@@ -166,12 +258,21 @@ async def _simpler_post_load_hygiene(
|
|
|
166
258
|
# Step 4: auto-detect drum root note from filename (BUG-2026-04-22#18).
|
|
167
259
|
# Only applied for one-shots — warped loops keep Live's default root
|
|
168
260
|
# because their root note is irrelevant at loop playback speeds.
|
|
261
|
+
#
|
|
262
|
+
# 2026-05-02 — fixed param name: was "Sample Pitch Coarse" (doesn't exist
|
|
263
|
+
# on OriginalSimpler — silently failed). Correct param is "Transpose"
|
|
264
|
+
# (semitone offset from C3=60). Convert detected drum root → Transpose:
|
|
265
|
+
# Transpose = 60 - drum_root. Example: drum_root=36 (C1) → Transpose=+24,
|
|
266
|
+
# so triggering MIDI 36 plays the sample at original recorded pitch.
|
|
169
267
|
drum_root = None
|
|
170
|
-
if not
|
|
268
|
+
if not is_loop:
|
|
171
269
|
drum_root = _detect_drum_root_note(file_path)
|
|
172
270
|
if drum_root is not None:
|
|
271
|
+
transpose_value = 60 - int(drum_root)
|
|
272
|
+
# Clamp to Simpler's Transpose range (-48..+48 semitones)
|
|
273
|
+
transpose_value = max(-48, min(48, transpose_value))
|
|
173
274
|
hygiene_params.append(
|
|
174
|
-
{"name_or_index": "
|
|
275
|
+
{"name_or_index": "Transpose", "value": transpose_value}
|
|
175
276
|
)
|
|
176
277
|
|
|
177
278
|
try:
|
|
@@ -185,11 +286,75 @@ async def _simpler_post_load_hygiene(
|
|
|
185
286
|
# non-fatal — verification already succeeded
|
|
186
287
|
pass
|
|
187
288
|
|
|
289
|
+
# Step 5: force Classic playback mode (BUG-FULL-MODE-3).
|
|
290
|
+
# Live auto-slices drum loops into Slice mode on load, which means a
|
|
291
|
+
# single C3 trigger note doesn't map to any slice → silence. Classic
|
|
292
|
+
# mode is the correct default for sample playback; user can switch
|
|
293
|
+
# to Slice/One-Shot explicitly if they want.
|
|
294
|
+
playback_mode_set = False
|
|
295
|
+
try:
|
|
296
|
+
ableton.send_command("set_simpler_playback_mode", {
|
|
297
|
+
"track_index": track_index,
|
|
298
|
+
"device_index": device_index,
|
|
299
|
+
"playback_mode": 0, # 0 = Classic, 1 = One-Shot, 2 = Slice
|
|
300
|
+
})
|
|
301
|
+
playback_mode_set = True
|
|
302
|
+
except Exception as exc:
|
|
303
|
+
logger.debug("_simpler_post_load_hygiene: set_simpler_playback_mode failed: %s", exc)
|
|
304
|
+
|
|
305
|
+
# Step 6: enable Simpler warp on tempo-locked loops (BUG-FULL-MODE-11,
|
|
306
|
+
# 2026-05-01). Splice loops embed the source BPM in the filename
|
|
307
|
+
# (e.g. `SO_SD_90_drum_loop_slippy.wav` = 90 BPM) but Simpler loads
|
|
308
|
+
# them at NATIVE rate by default — a 90-BPM drum loop in a 122-BPM
|
|
309
|
+
# project plays 35% slow.
|
|
310
|
+
#
|
|
311
|
+
# `simpler_set_warp` toggles `SimplerDevice.sample.warping` which
|
|
312
|
+
# lives on the sample child object — only reachable via the M4L
|
|
313
|
+
# bridge (Python LiveAPI can't step into the sample child). The
|
|
314
|
+
# bridge call is positional, NOT a dict.
|
|
315
|
+
#
|
|
316
|
+
# warp_mode mapping (from Live's docs):
|
|
317
|
+
# 0 = Beats — drums / percussive (transient-preserving)
|
|
318
|
+
# 1 = Tones — mono harmonic material
|
|
319
|
+
# 2 = Texture — poly / ambient / vocals (smoothest)
|
|
320
|
+
# 3 = Re-Pitch — classic pitch-shift (NOT what we want here)
|
|
321
|
+
# 4 = Complex — full musical material (mid CPU)
|
|
322
|
+
# 6 = Complex Pro — highest quality (highest CPU)
|
|
323
|
+
#
|
|
324
|
+
# Choosing by file path hint mirrors the `_LOOP_PATH_HINTS` partition.
|
|
325
|
+
# One-shots stay un-warped — warping a kick produces phasing.
|
|
326
|
+
warp_set = False
|
|
327
|
+
if is_loop and warp_loops:
|
|
328
|
+
path_lower = file_path.lower()
|
|
329
|
+
if any(seg in path_lower for seg in ("/drum_loops/", "drum_loop", "drumloop", "/breaks/", "/break_", "/perc_loops/")):
|
|
330
|
+
warp_mode = 0 # Beats
|
|
331
|
+
elif any(seg in path_lower for seg in ("/vocal_loops/", "vocal_loop", "/vox/", "vocal")):
|
|
332
|
+
warp_mode = 2 # Texture — preserves vocal transients
|
|
333
|
+
elif any(seg in path_lower for seg in ("/pad_loops/", "pad_loop", "/melodic_loops/", "melodic_loop", "/synth_loops/", "synth_loop", "/bass_loops/", "bass_loop")):
|
|
334
|
+
warp_mode = 4 # Complex — best for harmonic material
|
|
335
|
+
else:
|
|
336
|
+
warp_mode = 0 # default to Beats — safest for unknown loops
|
|
337
|
+
try:
|
|
338
|
+
await bridge.send_command(
|
|
339
|
+
"simpler_set_warp",
|
|
340
|
+
int(track_index),
|
|
341
|
+
int(device_index),
|
|
342
|
+
1, # warping ON
|
|
343
|
+
int(warp_mode),
|
|
344
|
+
timeout=10.0,
|
|
345
|
+
)
|
|
346
|
+
warp_set = True
|
|
347
|
+
except Exception as exc:
|
|
348
|
+
logger.debug("_simpler_post_load_hygiene: simpler_set_warp failed: %s", exc)
|
|
349
|
+
|
|
188
350
|
return {
|
|
189
351
|
"verified": True,
|
|
190
352
|
"device_name": actual_name,
|
|
191
353
|
"track_index": track_index,
|
|
192
354
|
"device_index": device_index,
|
|
193
|
-
"warped_loop_defaults_applied":
|
|
355
|
+
"warped_loop_defaults_applied": is_loop,
|
|
356
|
+
"volume_set": True,
|
|
357
|
+
"playback_mode_set": playback_mode_set,
|
|
358
|
+
"warp_set": warp_set,
|
|
194
359
|
"auto_root_note": drum_root,
|
|
195
360
|
}
|
|
@@ -26,64 +26,10 @@ from ._composition_engine import (
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
STYLE_TEMPLATES: dict[str, list[tuple[SectionType, float, float, int]]] = {
|
|
34
|
-
"electronic": [
|
|
35
|
-
(SectionType.INTRO, 0.2, 0.2, 16),
|
|
36
|
-
(SectionType.VERSE, 0.5, 0.5, 16),
|
|
37
|
-
(SectionType.BUILD, 0.6, 0.6, 8),
|
|
38
|
-
(SectionType.DROP, 0.9, 0.9, 16),
|
|
39
|
-
(SectionType.BREAKDOWN, 0.3, 0.3, 8),
|
|
40
|
-
(SectionType.BUILD, 0.7, 0.7, 8),
|
|
41
|
-
(SectionType.DROP, 1.0, 1.0, 16),
|
|
42
|
-
(SectionType.OUTRO, 0.2, 0.2, 16),
|
|
43
|
-
],
|
|
44
|
-
"hiphop": [
|
|
45
|
-
(SectionType.INTRO, 0.3, 0.3, 8),
|
|
46
|
-
(SectionType.VERSE, 0.6, 0.6, 16),
|
|
47
|
-
(SectionType.CHORUS, 0.8, 0.8, 8),
|
|
48
|
-
(SectionType.VERSE, 0.6, 0.6, 16),
|
|
49
|
-
(SectionType.CHORUS, 0.8, 0.8, 8),
|
|
50
|
-
(SectionType.BRIDGE, 0.5, 0.4, 8),
|
|
51
|
-
(SectionType.CHORUS, 0.9, 0.9, 8),
|
|
52
|
-
(SectionType.OUTRO, 0.3, 0.3, 8),
|
|
53
|
-
],
|
|
54
|
-
"pop": [
|
|
55
|
-
(SectionType.INTRO, 0.3, 0.3, 8),
|
|
56
|
-
(SectionType.VERSE, 0.5, 0.5, 16),
|
|
57
|
-
(SectionType.PRE_CHORUS, 0.6, 0.6, 8),
|
|
58
|
-
(SectionType.CHORUS, 0.8, 0.8, 8),
|
|
59
|
-
(SectionType.VERSE, 0.5, 0.5, 16),
|
|
60
|
-
(SectionType.PRE_CHORUS, 0.6, 0.6, 8),
|
|
61
|
-
(SectionType.CHORUS, 0.9, 0.9, 8),
|
|
62
|
-
(SectionType.BRIDGE, 0.4, 0.4, 8),
|
|
63
|
-
(SectionType.CHORUS, 1.0, 1.0, 8),
|
|
64
|
-
(SectionType.OUTRO, 0.3, 0.3, 8),
|
|
65
|
-
],
|
|
66
|
-
"ambient": [
|
|
67
|
-
(SectionType.INTRO, 0.1, 0.1, 32),
|
|
68
|
-
(SectionType.VERSE, 0.3, 0.3, 32),
|
|
69
|
-
(SectionType.VERSE, 0.5, 0.5, 32),
|
|
70
|
-
(SectionType.BREAKDOWN, 0.2, 0.2, 16),
|
|
71
|
-
(SectionType.VERSE, 0.4, 0.4, 32),
|
|
72
|
-
(SectionType.OUTRO, 0.1, 0.1, 32),
|
|
73
|
-
],
|
|
74
|
-
"techno": [
|
|
75
|
-
(SectionType.INTRO, 0.3, 0.3, 16),
|
|
76
|
-
(SectionType.VERSE, 0.6, 0.6, 32),
|
|
77
|
-
(SectionType.BUILD, 0.7, 0.7, 8),
|
|
78
|
-
(SectionType.DROP, 1.0, 1.0, 32),
|
|
79
|
-
(SectionType.BREAKDOWN, 0.3, 0.3, 16),
|
|
80
|
-
(SectionType.BUILD, 0.8, 0.8, 8),
|
|
81
|
-
(SectionType.DROP, 1.0, 1.0, 32),
|
|
82
|
-
(SectionType.OUTRO, 0.3, 0.3, 16),
|
|
83
|
-
],
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
VALID_STYLES = frozenset(STYLE_TEMPLATES.keys())
|
|
29
|
+
# v1.24: STYLE_TEMPLATES removed per vocabulary-not-form principle (Task 12).
|
|
30
|
+
# The framework provides VOCABULARY (descriptive). The LLM provides FORM
|
|
31
|
+
# (creative). Genre form templates (section sequences, bar counts, drop
|
|
32
|
+
# placements) belong in the LLM's training data + WebSearch fallback, NOT here.
|
|
87
33
|
|
|
88
34
|
|
|
89
35
|
# ── Loop Identity ────────────────────────────────────────────────────
|
|
@@ -226,18 +172,34 @@ def plan_arrangement_from_loop(
|
|
|
226
172
|
loop_identity: LoopIdentity,
|
|
227
173
|
target_duration_bars: int = 128,
|
|
228
174
|
style: str = "electronic",
|
|
175
|
+
section_template: Optional[list[tuple["SectionType", float, float, int]]] = None,
|
|
229
176
|
) -> ArrangementPlan:
|
|
230
177
|
"""Transform a loop identity into a full arrangement blueprint.
|
|
231
178
|
|
|
232
|
-
|
|
179
|
+
# DEPRECATED in v1.24 — full mode is now LLM-creative. This function may
|
|
180
|
+
# remain functional but should not be relied on for v1.24+ flows.
|
|
181
|
+
# v1.24: STYLE_TEMPLATES removed per vocabulary-not-form principle (Task 12).
|
|
182
|
+
# Callers must supply section_template explicitly; the built-in registry is gone.
|
|
183
|
+
|
|
184
|
+
1. Use caller-supplied section_template (required in v1.24+)
|
|
233
185
|
2. Scale section lengths to target duration
|
|
234
186
|
3. Plan element reveal order (what enters/exits when)
|
|
235
187
|
4. Suggest gesture automation for transitions
|
|
236
|
-
"""
|
|
237
|
-
if style not in STYLE_TEMPLATES:
|
|
238
|
-
raise ValueError(f"Unknown style '{style}'. Valid: {sorted(VALID_STYLES)}")
|
|
239
188
|
|
|
240
|
-
|
|
189
|
+
Args:
|
|
190
|
+
section_template: List of (SectionType, energy_target, density_target,
|
|
191
|
+
bars) tuples. The LLM or caller is responsible for providing this
|
|
192
|
+
based on the genre/mood context. MUST start with INTRO and end with
|
|
193
|
+
OUTRO by convention.
|
|
194
|
+
"""
|
|
195
|
+
if section_template is None:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
"section_template is required in v1.24+. "
|
|
198
|
+
"STYLE_TEMPLATES was removed — the LLM provides form, not the framework. "
|
|
199
|
+
"Pass an explicit section_template list."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
template = section_template
|
|
241
203
|
|
|
242
204
|
# 1. Scale sections to target duration
|
|
243
205
|
template_bars = sum(s[3] for s in template)
|
|
@@ -573,6 +573,7 @@ async def replace_simpler_sample(
|
|
|
573
573
|
file_path: str,
|
|
574
574
|
chain_index: Optional[int] = None,
|
|
575
575
|
nested_device_index: Optional[int] = None,
|
|
576
|
+
warp_loops: bool = True,
|
|
576
577
|
) -> dict:
|
|
577
578
|
"""Load an audio file into a Simpler device by absolute file path.
|
|
578
579
|
|
|
@@ -618,7 +619,8 @@ async def replace_simpler_sample(
|
|
|
618
619
|
)
|
|
619
620
|
if native is not None:
|
|
620
621
|
hygiene = await _simpler_post_load_hygiene(
|
|
621
|
-
bridge, ableton, track_index, device_index, file_path
|
|
622
|
+
bridge, ableton, track_index, device_index, file_path,
|
|
623
|
+
warp_loops=warp_loops,
|
|
622
624
|
)
|
|
623
625
|
if not hygiene.get("verified"):
|
|
624
626
|
return hygiene
|
|
@@ -642,7 +644,8 @@ async def replace_simpler_sample(
|
|
|
642
644
|
}
|
|
643
645
|
|
|
644
646
|
hygiene = await _simpler_post_load_hygiene(
|
|
645
|
-
bridge, ableton, track_index, device_index, file_path
|
|
647
|
+
bridge, ableton, track_index, device_index, file_path,
|
|
648
|
+
warp_loops=warp_loops,
|
|
646
649
|
)
|
|
647
650
|
if not hygiene.get("verified"):
|
|
648
651
|
return hygiene
|
|
@@ -660,6 +663,7 @@ async def load_sample_to_simpler(
|
|
|
660
663
|
track_index: int,
|
|
661
664
|
file_path: str,
|
|
662
665
|
device_index: int = 0,
|
|
666
|
+
warp_loops: bool = True,
|
|
663
667
|
) -> dict:
|
|
664
668
|
"""Load an audio file into a NEW Simpler device on a track.
|
|
665
669
|
|
|
@@ -703,7 +707,8 @@ async def load_sample_to_simpler(
|
|
|
703
707
|
)
|
|
704
708
|
if native is not None:
|
|
705
709
|
hygiene = await _simpler_post_load_hygiene(
|
|
706
|
-
bridge, ableton, track_index, actual_device_index, file_path
|
|
710
|
+
bridge, ableton, track_index, actual_device_index, file_path,
|
|
711
|
+
warp_loops=warp_loops,
|
|
707
712
|
)
|
|
708
713
|
if not hygiene.get("verified"):
|
|
709
714
|
return hygiene
|
|
@@ -759,7 +764,8 @@ async def load_sample_to_simpler(
|
|
|
759
764
|
|
|
760
765
|
# Step 4: Verify by reading back the device name (P0-1 guard)
|
|
761
766
|
hygiene = await _simpler_post_load_hygiene(
|
|
762
|
-
bridge, ableton, track_index, actual_device_index, file_path
|
|
767
|
+
bridge, ableton, track_index, actual_device_index, file_path,
|
|
768
|
+
warp_loops=warp_loops,
|
|
763
769
|
)
|
|
764
770
|
if not hygiene.get("verified"):
|
|
765
771
|
return hygiene
|
|
@@ -158,28 +158,57 @@ def search_browser(
|
|
|
158
158
|
return _get_ableton(ctx).send_command("search_browser", params)
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
#
|
|
161
|
+
# M4L instrument post-load hygiene — 2026-05-02.
|
|
162
|
+
# Some Max-for-Live instruments load with defaults that immediately produce loud
|
|
163
|
+
# unwanted output (Harmonic Drone Generator from Drone Lab is the canonical
|
|
164
|
+
# example: Latch on + Density 80% + Volume −6 dB + all 8 voices active = a wall
|
|
165
|
+
# of sustained drone the moment any MIDI note touches it). Apply tames here so
|
|
166
|
+
# the device is workable on first load. Each entry maps a device-name match
|
|
167
|
+
# (substring) to a list of (parameter_name, value) pairs.
|
|
168
|
+
#
|
|
169
|
+
# Detection runs UNCONDITIONALLY (not gated on `role` like _SIMPLER_ROLE_DEFAULTS)
|
|
170
|
+
# because these M4L instruments are typically loaded without a role parameter.
|
|
171
|
+
_M4L_INSTRUMENT_HYGIENE: dict[str, list[tuple[str, float]]] = {
|
|
172
|
+
"Harmonic Drone Generator": [
|
|
173
|
+
("Latch", 0), # Off — prevents indefinite note sustain after one trigger
|
|
174
|
+
("Volume", -40), # ≈ -20 dB display (default is -18 / -6 dB which is too loud)
|
|
175
|
+
("Density", 40), # 40% (default 80% is too dense for a background bed)
|
|
176
|
+
],
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Role-aware Simpler defaults — BUG-2026-04-22 #17 + #18, plus 2026-05-02 fix.
|
|
162
181
|
# Each role maps to a list of (parameter_name, value) pairs applied after
|
|
163
182
|
# load via set_device_parameter. Trigger Mode polarity per BUG #9:
|
|
164
|
-
# 0 = Trigger (one-shot), 1 = Gate (held). Volume in dB.
|
|
183
|
+
# 0 = Trigger (one-shot), 1 = Gate (held). Volume in dB. Transpose in semitones.
|
|
184
|
+
#
|
|
185
|
+
# 2026-05-02 — fixed pitch-shift bug:
|
|
186
|
+
# Earlier versions used "Sample Pitch Coarse" param name, which DOES NOT EXIST
|
|
187
|
+
# on OriginalSimpler — the call silently raised and was swallowed. Result: every
|
|
188
|
+
# drum-role Simpler played 24 semitones below original pitch ("super low" sound)
|
|
189
|
+
# because the Simpler's default sample root is C3 (60), but drum convention sends
|
|
190
|
+
# MIDI 36 (C1). The correct parameter is "Transpose" (range -48..+48 semitones);
|
|
191
|
+
# +24 compensates for the C3-vs-C1 mismatch so drum samples play at original
|
|
192
|
+
# recorded pitch when MIDI 36 is sent. Melodic/texture roles use Transpose=0
|
|
193
|
+
# because their default playback range centers on C3 (60) — no compensation needed.
|
|
165
194
|
_SIMPLER_ROLE_DEFAULTS = {
|
|
166
195
|
"drum": [
|
|
167
196
|
("Snap", 0),
|
|
168
197
|
("Volume", 0.0),
|
|
169
198
|
("Trigger Mode", 0), # Trigger / one-shot
|
|
170
|
-
("
|
|
199
|
+
("Transpose", 24), # Compensate C3-default → C1-drum-convention root
|
|
171
200
|
],
|
|
172
201
|
"melodic": [
|
|
173
202
|
("Snap", 1),
|
|
174
203
|
("Volume", 0.0),
|
|
175
204
|
("Trigger Mode", 1), # Gate / held
|
|
176
|
-
("
|
|
205
|
+
("Transpose", 0), # C3 default — melodic input range
|
|
177
206
|
],
|
|
178
207
|
"texture": [
|
|
179
208
|
("Snap", 0),
|
|
180
209
|
("Volume", -6.0),
|
|
181
210
|
("Trigger Mode", 1), # Gate
|
|
182
|
-
("
|
|
211
|
+
("Transpose", 0), # C3 default — sustained-input range
|
|
183
212
|
],
|
|
184
213
|
}
|
|
185
214
|
|
|
@@ -238,26 +267,80 @@ def load_browser_item(
|
|
|
238
267
|
"uri": uri,
|
|
239
268
|
})
|
|
240
269
|
|
|
241
|
-
# Post-load:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
# Post-load: probe the loaded device once, then apply two layers of hygiene.
|
|
271
|
+
#
|
|
272
|
+
# 2026-05-02 — fixed device-detection bug. The TCP load_browser_item command
|
|
273
|
+
# returns {loaded, name, device_count} with NO device_index and NO class_name,
|
|
274
|
+
# so the previous detection (`result.get("device_index")` / `result.get("class_name")`)
|
|
275
|
+
# always failed and the role-defaults branch was never entered. Resolution:
|
|
276
|
+
# treat newly-loaded sample-on-empty-track as device_index=0 (Live places the
|
|
277
|
+
# instrument at chain head) and verify class + name via get_device_info.
|
|
278
|
+
#
|
|
279
|
+
# Layer 1 (gated on `role`): Simpler role-aware defaults — Snap/Volume/
|
|
280
|
+
# Trigger Mode/Transpose for drum/melodic/texture roles.
|
|
281
|
+
# Layer 2 (unconditional): M4L instrument hygiene — name-matched tames for
|
|
282
|
+
# known problem devices (Harmonic Drone Generator's Latch + loud defaults).
|
|
283
|
+
device_index_resolved: Optional[int] = None
|
|
284
|
+
device_class = ""
|
|
285
|
+
device_name_loaded = ""
|
|
286
|
+
if isinstance(result, dict) and result.get("loaded") and not result.get("error"):
|
|
287
|
+
device_index_resolved = result.get("device_index")
|
|
288
|
+
try:
|
|
289
|
+
probe = ableton.send_command("get_device_info", {
|
|
290
|
+
"track_index": track_index,
|
|
291
|
+
"device_index": 0,
|
|
292
|
+
})
|
|
293
|
+
device_class = str(probe.get("class_name", "") or "")
|
|
294
|
+
device_name_loaded = str(probe.get("name", "") or result.get("name", "") or "")
|
|
295
|
+
if device_index_resolved is None:
|
|
296
|
+
device_index_resolved = 0
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
# Layer 1 — Simpler role-aware defaults
|
|
301
|
+
if role and device_index_resolved is not None and "Simpler" in device_class:
|
|
302
|
+
applied = []
|
|
303
|
+
for name, value in _SIMPLER_ROLE_DEFAULTS[role]:
|
|
304
|
+
try:
|
|
305
|
+
ableton.send_command("set_device_parameter", {
|
|
306
|
+
"track_index": track_index,
|
|
307
|
+
"device_index": int(device_index_resolved),
|
|
308
|
+
"parameter_name": name,
|
|
309
|
+
"value": value,
|
|
310
|
+
})
|
|
311
|
+
applied.append({"parameter": name, "value": value})
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
# Don't fail the whole load if one default doesn't apply
|
|
314
|
+
# (parameter name might not exist on every Simpler variant).
|
|
315
|
+
applied.append({"parameter": name, "skipped": str(exc)})
|
|
316
|
+
result["role"] = role
|
|
317
|
+
result["role_defaults_applied"] = applied
|
|
318
|
+
result["device_class"] = device_class
|
|
319
|
+
|
|
320
|
+
# Layer 2 — M4L instrument hygiene (unconditional, name-matched).
|
|
321
|
+
# Detects Harmonic Drone Generator and other known problem M4L instruments
|
|
322
|
+
# by name substring, applies tame defaults to prevent loud-on-load surprises.
|
|
323
|
+
if device_index_resolved is not None and device_name_loaded:
|
|
324
|
+
for hygiene_name, params in _M4L_INSTRUMENT_HYGIENE.items():
|
|
325
|
+
if hygiene_name not in device_name_loaded:
|
|
326
|
+
continue
|
|
327
|
+
applied_hygiene = []
|
|
328
|
+
for name, value in params:
|
|
248
329
|
try:
|
|
249
330
|
ableton.send_command("set_device_parameter", {
|
|
250
331
|
"track_index": track_index,
|
|
251
|
-
"device_index": int(
|
|
332
|
+
"device_index": int(device_index_resolved),
|
|
252
333
|
"parameter_name": name,
|
|
253
334
|
"value": value,
|
|
254
335
|
})
|
|
255
|
-
|
|
336
|
+
applied_hygiene.append({"parameter": name, "value": value})
|
|
256
337
|
except Exception as exc:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
338
|
+
applied_hygiene.append({"parameter": name, "skipped": str(exc)})
|
|
339
|
+
result["m4l_hygiene"] = {
|
|
340
|
+
"device_name": hygiene_name,
|
|
341
|
+
"applied": applied_hygiene,
|
|
342
|
+
}
|
|
343
|
+
result.setdefault("device_class", device_class)
|
|
344
|
+
break # one hygiene match per load
|
|
262
345
|
|
|
263
346
|
return result
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 \u2014
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 \u2014 462 tools, 55 domains, 44 semantic moves. Device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, 12 creative intelligence engines",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
|
|
|
5
5
|
when this script is selected in Preferences > Link, Tempo & MIDI.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.
|
|
8
|
+
__version__ = "1.25.0"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.dreamrec/livepilot",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "462-tool agentic MCP production system for Ableton Live 12 \u2014 55 domains, 44 semantic moves, device atlas (5264 devices), Splice intelligence (gRPC + GraphQL), 9-band spectral perception auto-loaded, Creative Director skill, technique memory, 12 creative engines",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.25.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.25.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|