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.
Files changed (48) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +60 -14
  3. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  4. package/m4l_device/livepilot_bridge.js +1 -1
  5. package/mcp_server/__init__.py +1 -1
  6. package/mcp_server/atlas/__init__.py +17 -3
  7. package/mcp_server/atlas/explore_tools.py +332 -0
  8. package/mcp_server/atlas/tools.py +161 -0
  9. package/mcp_server/audit/__init__.py +6 -0
  10. package/mcp_server/audit/checks.py +618 -0
  11. package/mcp_server/audit/tools.py +232 -0
  12. package/mcp_server/composer/branch_producer.py +5 -2
  13. package/mcp_server/composer/develop/__init__.py +19 -0
  14. package/mcp_server/composer/develop/apply.py +217 -0
  15. package/mcp_server/composer/develop/brief_builder.py +269 -0
  16. package/mcp_server/composer/develop/seed_introspector.py +195 -0
  17. package/mcp_server/composer/engine.py +15 -521
  18. package/mcp_server/composer/fast/__init__.py +62 -0
  19. package/mcp_server/composer/fast/apply.py +533 -0
  20. package/mcp_server/composer/fast/brief_builder.py +1479 -0
  21. package/mcp_server/composer/fast/tier_classification.py +159 -0
  22. package/mcp_server/composer/framework/__init__.py +0 -0
  23. package/mcp_server/composer/framework/applier.py +179 -0
  24. package/mcp_server/composer/framework/artist_loader.py +63 -0
  25. package/mcp_server/composer/framework/atlas_resolver.py +554 -0
  26. package/mcp_server/composer/framework/brief.py +79 -0
  27. package/mcp_server/composer/framework/event_lexicon.py +71 -0
  28. package/mcp_server/composer/framework/genre_loader.py +77 -0
  29. package/mcp_server/composer/framework/intent_source.py +137 -0
  30. package/mcp_server/composer/framework/knowledge_pack.py +140 -0
  31. package/mcp_server/composer/framework/plan_compiler.py +10 -0
  32. package/mcp_server/composer/full/__init__.py +10 -0
  33. package/mcp_server/composer/full/apply.py +1139 -0
  34. package/mcp_server/composer/full/brief_builder.py +227 -0
  35. package/mcp_server/composer/full/engine.py +541 -0
  36. package/mcp_server/composer/full/layer_planner.py +491 -0
  37. package/mcp_server/composer/layer_planner.py +19 -465
  38. package/mcp_server/composer/sample_resolver.py +80 -7
  39. package/mcp_server/composer/tools.py +626 -28
  40. package/mcp_server/server.py +1 -0
  41. package/mcp_server/splice_client/client.py +7 -0
  42. package/mcp_server/tools/_analyzer_engine/sample.py +172 -7
  43. package/mcp_server/tools/_planner_engine.py +25 -63
  44. package/mcp_server/tools/analyzer.py +10 -4
  45. package/mcp_server/tools/browser.py +102 -19
  46. package/package.json +2 -2
  47. package/remote_script/LivePilot/__init__.py +1 -1
  48. package/server.json +3 -3
@@ -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 filename contains a BPM marker (likely a tempo-locked loop)."""
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
- return bool(_BPM_IN_FILENAME_RE.search(stem))
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: turn Snap OFF — required for reliable playback after replace
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 _is_warped_loop(file_path):
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 _is_warped_loop(file_path):
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": "Sample Pitch Coarse", "value": drum_root}
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": _is_warped_loop(file_path),
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
- # ── Section Templates ────────────────────────────────────────────────
30
-
31
- # Prototypical section sequences by style. Each entry:
32
- # (section_type, energy_target, density_target, typical_bars)
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
- 1. Select section template based on style
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
- template = STYLE_TEMPLATES[style]
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
- # Role-aware Simpler defaults BUG-2026-04-22 #17 + #18.
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. Root in MIDI pitch.
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
- ("Sample Pitch Coarse", 36), # C1, matches drum-pad convention
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
- ("Sample Pitch Coarse", 60), # C3
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
- ("Sample Pitch Coarse", 60), # C3
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: apply role-aware defaults if the loaded device is a Simpler.
242
- if role and isinstance(result, dict) and not result.get("error"):
243
- device_index = result.get("device_index")
244
- device_class = str(result.get("class_name") or result.get("device_name") or "")
245
- if device_index is not None and "Simpler" in device_class:
246
- applied = []
247
- for name, value in _SIMPLER_ROLE_DEFAULTS[role]:
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(device_index),
332
+ "device_index": int(device_index_resolved),
252
333
  "parameter_name": name,
253
334
  "value": value,
254
335
  })
255
- applied.append({"parameter": name, "value": value})
336
+ applied_hygiene.append({"parameter": name, "value": value})
256
337
  except Exception as exc:
257
- # Don't fail the whole load if one default doesn't apply
258
- # (parameter name might not exist on every Simpler variant).
259
- applied.append({"parameter": name, "skipped": str(exc)})
260
- result["role"] = role
261
- result["role_defaults_applied"] = applied
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.23.6",
3
+ "version": "1.25.0",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 \u2014 453 tools, 54 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",
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.23.6"
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": "453-tool agentic MCP production system for Ableton Live 12 \u2014 53 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",
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.23.6",
9
+ "version": "1.25.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "livepilot",
14
- "version": "1.23.1",
14
+ "version": "1.25.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }