livepilot 1.9.24 → 1.10.1

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 (185) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +223 -0
  4. package/CONTRIBUTING.md +2 -2
  5. package/LICENSE +62 -21
  6. package/README.md +291 -276
  7. package/bin/livepilot.js +87 -0
  8. package/installer/codex.js +147 -0
  9. package/livepilot/.Codex-plugin/plugin.json +2 -2
  10. package/livepilot/.claude-plugin/plugin.json +2 -2
  11. package/livepilot/skills/livepilot-arrangement/SKILL.md +18 -1
  12. package/livepilot/skills/livepilot-core/SKILL.md +22 -5
  13. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
  14. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
  15. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
  16. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
  17. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
  18. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
  19. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
  20. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
  21. package/livepilot/skills/livepilot-core/references/overview.md +13 -9
  22. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
  23. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
  24. package/livepilot/skills/livepilot-devices/SKILL.md +39 -4
  25. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  26. package/livepilot/skills/livepilot-release/SKILL.md +23 -19
  27. package/livepilot/skills/livepilot-sample-engine/SKILL.md +105 -0
  28. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
  29. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
  30. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
  31. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
  32. package/livepilot/skills/livepilot-wonder/SKILL.md +17 -0
  33. package/livepilot.mcpb +0 -0
  34. package/m4l_device/livepilot_bridge.js +1 -1
  35. package/manifest.json +4 -4
  36. package/mcp_server/__init__.py +1 -1
  37. package/mcp_server/atlas/__init__.py +357 -0
  38. package/mcp_server/atlas/device_atlas.json +44067 -0
  39. package/mcp_server/atlas/enrichments/__init__.py +111 -0
  40. package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
  41. package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
  42. package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
  43. package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
  44. package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
  45. package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
  46. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
  47. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
  48. package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
  49. package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
  50. package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
  51. package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
  52. package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
  53. package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
  54. package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
  55. package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
  56. package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
  57. package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
  58. package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
  59. package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
  60. package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
  61. package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
  62. package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
  63. package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
  64. package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
  65. package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
  66. package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
  67. package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
  68. package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
  69. package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
  70. package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
  71. package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
  72. package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
  73. package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
  74. package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
  75. package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
  76. package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
  77. package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
  78. package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
  79. package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
  80. package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
  81. package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
  82. package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
  83. package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
  84. package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
  85. package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
  86. package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
  87. package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
  88. package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
  89. package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
  90. package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
  91. package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
  92. package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
  93. package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
  94. package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
  95. package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
  96. package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
  97. package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
  98. package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
  99. package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
  100. package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
  101. package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
  102. package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
  103. package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
  104. package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
  105. package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
  106. package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
  107. package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
  108. package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
  109. package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
  110. package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
  111. package/mcp_server/atlas/scanner.py +236 -0
  112. package/mcp_server/atlas/tools.py +224 -0
  113. package/mcp_server/composer/__init__.py +1 -0
  114. package/mcp_server/composer/engine.py +532 -0
  115. package/mcp_server/composer/layer_planner.py +427 -0
  116. package/mcp_server/composer/prompt_parser.py +329 -0
  117. package/mcp_server/composer/sample_resolver.py +153 -0
  118. package/mcp_server/composer/tools.py +211 -0
  119. package/mcp_server/connection.py +53 -8
  120. package/mcp_server/corpus/__init__.py +377 -0
  121. package/mcp_server/device_forge/__init__.py +1 -0
  122. package/mcp_server/device_forge/builder.py +377 -0
  123. package/mcp_server/device_forge/models.py +142 -0
  124. package/mcp_server/device_forge/templates.py +483 -0
  125. package/mcp_server/device_forge/tools.py +162 -0
  126. package/mcp_server/m4l_bridge.py +1 -0
  127. package/mcp_server/memory/taste_accessors.py +47 -0
  128. package/mcp_server/preview_studio/engine.py +9 -2
  129. package/mcp_server/preview_studio/tools.py +78 -35
  130. package/mcp_server/project_brain/tools.py +34 -0
  131. package/mcp_server/runtime/capability_probe.py +21 -2
  132. package/mcp_server/runtime/execution_router.py +184 -38
  133. package/mcp_server/runtime/live_version.py +102 -0
  134. package/mcp_server/runtime/mcp_dispatch.py +46 -0
  135. package/mcp_server/runtime/remote_commands.py +13 -5
  136. package/mcp_server/runtime/tools.py +66 -29
  137. package/mcp_server/sample_engine/__init__.py +1 -0
  138. package/mcp_server/sample_engine/analyzer.py +216 -0
  139. package/mcp_server/sample_engine/critics.py +390 -0
  140. package/mcp_server/sample_engine/models.py +193 -0
  141. package/mcp_server/sample_engine/moves.py +127 -0
  142. package/mcp_server/sample_engine/planner.py +186 -0
  143. package/mcp_server/sample_engine/slice_workflow.py +190 -0
  144. package/mcp_server/sample_engine/sources.py +540 -0
  145. package/mcp_server/sample_engine/techniques.py +908 -0
  146. package/mcp_server/sample_engine/tools.py +545 -0
  147. package/mcp_server/semantic_moves/__init__.py +3 -0
  148. package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
  149. package/mcp_server/semantic_moves/mix_moves.py +8 -8
  150. package/mcp_server/semantic_moves/models.py +7 -7
  151. package/mcp_server/semantic_moves/performance_moves.py +4 -4
  152. package/mcp_server/semantic_moves/sample_compilers.py +377 -0
  153. package/mcp_server/semantic_moves/sound_design_moves.py +4 -4
  154. package/mcp_server/semantic_moves/tools.py +63 -10
  155. package/mcp_server/semantic_moves/transition_moves.py +4 -4
  156. package/mcp_server/server.py +71 -1
  157. package/mcp_server/session_continuity/tracker.py +4 -1
  158. package/mcp_server/sound_design/critics.py +89 -1
  159. package/mcp_server/splice_client/__init__.py +1 -0
  160. package/mcp_server/splice_client/client.py +347 -0
  161. package/mcp_server/splice_client/models.py +96 -0
  162. package/mcp_server/splice_client/protos/__init__.py +1 -0
  163. package/mcp_server/splice_client/protos/app_pb2.py +319 -0
  164. package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
  165. package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
  166. package/mcp_server/tools/_conductor.py +16 -0
  167. package/mcp_server/tools/_planner_engine.py +24 -0
  168. package/mcp_server/tools/analyzer.py +2 -0
  169. package/mcp_server/tools/arrangement.py +69 -0
  170. package/mcp_server/tools/automation.py +15 -2
  171. package/mcp_server/tools/devices.py +117 -6
  172. package/mcp_server/tools/notes.py +37 -4
  173. package/mcp_server/tools/planner.py +3 -0
  174. package/mcp_server/wonder_mode/diagnosis.py +5 -0
  175. package/mcp_server/wonder_mode/engine.py +144 -14
  176. package/mcp_server/wonder_mode/tools.py +33 -1
  177. package/package.json +14 -4
  178. package/remote_script/LivePilot/__init__.py +8 -1
  179. package/remote_script/LivePilot/arrangement.py +114 -0
  180. package/remote_script/LivePilot/browser.py +56 -1
  181. package/remote_script/LivePilot/devices.py +246 -6
  182. package/remote_script/LivePilot/mixing.py +8 -3
  183. package/remote_script/LivePilot/server.py +5 -1
  184. package/remote_script/LivePilot/transport.py +3 -0
  185. package/remote_script/LivePilot/version_detect.py +78 -0
@@ -110,6 +110,12 @@ _ROUTING_PATTERNS: list[tuple[str, str, str, str, list[str]]] = [
110
110
  # Research requests
111
111
  (r"research|how.?to|technique|tutorial|learn", "research", "research", "research_technique", []),
112
112
  (r"style.?tactic|production.?style|genre.?approach", "research", "research", "get_style_tactics", []),
113
+
114
+ # Sample requests
115
+ (r"sample|splice|loop|chop|flip|break(?:beat)?|one.?shot", "sample_engine", "sample", "search_samples", ["analyze_sample", "plan_sample_workflow"]),
116
+ (r"slice|transient.?hit|slice.?mode", "sample_engine", "sample", "plan_slice_workflow", ["search_samples"]),
117
+ (r"vocal.?sample|foley|field.?record|found.?sound", "sample_engine", "sample", "search_samples", ["analyze_sample"]),
118
+ (r"texture.?sample|ambient.?sample|atmo.?sample", "sample_engine", "sample", "search_samples", ["suggest_sample_technique"]),
113
119
  ]
114
120
 
115
121
 
@@ -164,6 +170,16 @@ def _infer_workflow_mode(request_lower: str) -> str:
164
170
  if re.search(r"fix|quick|just|only|undo|revert|simple", request_lower):
165
171
  return "quick_fix"
166
172
 
173
+ # Slice workflow
174
+ if re.search(r"slice|chop|transient.?hit", request_lower):
175
+ return "slice_workflow"
176
+
177
+ # Sample workflows
178
+ if re.search(r"sample|splice|foley|found.?sound|one.?shot|break(?:beat)?|flip|loop", request_lower):
179
+ if re.search(r"arrange|section|verse|chorus|drop|bridge|hook", request_lower):
180
+ return "sample_plus_arrangement"
181
+ return "sample_discovery"
182
+
167
183
  # Agentic loop keywords (full autonomous)
168
184
  if re.search(r"autonomous|auto|full|everything|deep|polish|finish", request_lower):
169
185
  return "agentic_loop"
@@ -164,6 +164,8 @@ class SectionPlan:
164
164
  tracks_entering: list[int] # new elements introduced in this section
165
165
  tracks_exiting: list[int] # elements removed in this section
166
166
 
167
+ sample_hints: list[str] = field(default_factory=list)
168
+
167
169
  def length_bars(self) -> int:
168
170
  return self.end_bar - self.start_bar
169
171
 
@@ -196,6 +198,28 @@ class ArrangementPlan:
196
198
  }
197
199
 
198
200
 
201
+ # ── Section Sample Hints ─────────────────────────────────────────────
202
+
203
+ _SECTION_SAMPLE_DEFAULTS: dict[str, list[str]] = {
204
+ "intro": ["texture_bed", "fill_one_shot"],
205
+ "verse": ["texture_bed", "fill_one_shot"],
206
+ "pre_chorus": ["transition_fx", "texture_bed"],
207
+ "chorus": ["hook_sample", "break_layer", "fill_one_shot"],
208
+ "drop": ["hook_sample", "break_layer", "fill_one_shot"],
209
+ "build": ["transition_fx", "texture_bed"],
210
+ "bridge": ["texture_bed", "transition_fx"],
211
+ "breakdown": ["texture_bed"],
212
+ "outro": ["texture_bed", "fill_one_shot"],
213
+ }
214
+
215
+
216
+ def add_sample_hints(plan: "ArrangementPlan") -> None:
217
+ """Populate sample_hints on each section based on section type."""
218
+ for section in plan.sections:
219
+ section_key = section.section_type.value.lower()
220
+ section.sample_hints = _SECTION_SAMPLE_DEFAULTS.get(section_key, ["texture_bed"])
221
+
222
+
199
223
  # ── Core Planner ─────────────────────────────────────────────────────
200
224
 
201
225
  def plan_arrangement_from_loop(
@@ -381,6 +381,8 @@ async def load_sample_to_simpler(
381
381
  return {"error": "Sample replacement failed after bootstrap"}
382
382
 
383
383
  result["method"] = "bootstrap_and_replace"
384
+ result["device_index"] = actual_device_index # additive — for step-result binding
385
+ result["track_index"] = track_index
384
386
  return result
385
387
 
386
388
 
@@ -143,6 +143,48 @@ def create_arrangement_clip(
143
143
  return _get_ableton(ctx).send_command("create_arrangement_clip", params)
144
144
 
145
145
 
146
+ @mcp.tool()
147
+ def create_native_arrangement_clip(
148
+ ctx: Context,
149
+ track_index: int,
150
+ start_time: float,
151
+ length: float,
152
+ name: str = "",
153
+ color_index: Optional[int] = None,
154
+ ) -> dict:
155
+ """Create an empty MIDI clip directly in Arrangement View (Live 12.1.10+).
156
+
157
+ Unlike create_arrangement_clip (which duplicates a session clip), this creates
158
+ a native arrangement clip with full automation envelope support — volume rides,
159
+ filter sweeps, send automation all work natively.
160
+
161
+ Requires Live 12.1.10+. Falls back with a clear error on older versions.
162
+
163
+ track_index: 0+ for regular MIDI tracks
164
+ start_time: beat position (0.0 = song start, 4.0 = bar 2 in 4/4)
165
+ length: clip length in beats
166
+ name: optional clip display name
167
+ color_index: optional 0-69 Ableton color
168
+ """
169
+ _validate_track_index(track_index)
170
+ if start_time < 0:
171
+ raise ValueError("start_time must be >= 0")
172
+ if length <= 0:
173
+ raise ValueError("length must be > 0")
174
+
175
+ params = {
176
+ "track_index": track_index,
177
+ "start_time": start_time,
178
+ "length": length,
179
+ }
180
+ if name:
181
+ params["name"] = name
182
+ if color_index is not None:
183
+ params["color_index"] = color_index
184
+
185
+ return _get_ableton(ctx).send_command("create_native_arrangement_clip", params)
186
+
187
+
146
188
  @mcp.tool()
147
189
  def add_arrangement_notes(
148
190
  ctx: Context,
@@ -288,6 +330,33 @@ def back_to_arranger(ctx: Context) -> dict:
288
330
  return _get_ableton(ctx).send_command("back_to_arranger")
289
331
 
290
332
 
333
+ @mcp.tool()
334
+ def force_arrangement(
335
+ ctx: Context,
336
+ beat_time: float = 0,
337
+ loop_start: float = 0,
338
+ loop_length: float = 0,
339
+ play: bool = True,
340
+ ) -> dict:
341
+ """Force ALL tracks to follow the arrangement and start playback.
342
+
343
+ Atomically: stops all session clips, releases every track from
344
+ session override, sets back-to-arranger, jumps to position, and
345
+ starts playing. This is the "play my arrangement from the top"
346
+ command.
347
+
348
+ beat_time: position to start from (default 0 = beginning)
349
+ loop_start: loop region start in beats (default 0)
350
+ loop_length: loop region length in beats (0 = no loop change)
351
+ play: whether to start playback (default True)
352
+ """
353
+ params: dict = {"beat_time": beat_time, "play": play}
354
+ if loop_length > 0:
355
+ params["loop_start"] = loop_start
356
+ params["loop_length"] = loop_length
357
+ return _get_ableton(ctx).send_command("force_arrangement", params)
358
+
359
+
291
360
  @mcp.tool()
292
361
  def get_arrangement_notes(
293
362
  ctx: Context,
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  from typing import Any, Optional
11
11
 
12
12
  from fastmcp import Context
13
+ from pydantic import BaseModel, Field
13
14
 
14
15
  from ..curves import generate_curve, generate_from_recipe, list_recipes
15
16
  from ..server import mcp
@@ -27,10 +28,22 @@ def _ensure_list(v: Any) -> list:
27
28
  except json.JSONDecodeError as exc:
28
29
  raise ValueError(f"Invalid JSON in parameter: {exc}") from exc
29
30
  if isinstance(v, list):
30
- return v
31
+ normalized = []
32
+ for item in v:
33
+ if isinstance(item, BaseModel):
34
+ normalized.append(item.model_dump(exclude_none=True))
35
+ else:
36
+ normalized.append(item)
37
+ return normalized
31
38
  return [v]
32
39
 
33
40
 
41
+ class AutomationPoint(BaseModel):
42
+ time: float
43
+ value: float
44
+ duration: Optional[float] = Field(default=None, ge=0.0)
45
+
46
+
34
47
  @mcp.tool()
35
48
  def get_clip_automation(
36
49
  ctx: Context,
@@ -62,7 +75,7 @@ def set_clip_automation(
62
75
  track_index: int,
63
76
  clip_index: int,
64
77
  parameter_type: str,
65
- points: Any,
78
+ points: list[AutomationPoint] | str,
66
79
  device_index: Optional[int] = None,
67
80
  parameter_index: Optional[int] = None,
68
81
  send_index: Optional[int] = None,
@@ -1,6 +1,6 @@
1
1
  """Device MCP tools — parameters, racks, browser loading, plugin deep control.
2
2
 
3
- 15 tools matching the Remote Script devices domain + M4L bridge.
3
+ 16 tools matching the Remote Script devices domain + M4L bridge.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
@@ -359,14 +359,15 @@ def find_and_load_device(ctx: Context, track_index: int, device_name: str) -> di
359
359
  if not device_name.strip():
360
360
  raise ValueError("device_name cannot be empty")
361
361
 
362
- # Guardrail: bare Drum Rack produces silence (no samples loaded)
362
+ # Guardrail: bare Drum Rack produces silence unless building programmatically (12.3+)
363
363
  if device_name.strip().lower() == "drum rack":
364
364
  raise ValueError(
365
365
  "Loading a bare 'Drum Rack' creates an empty rack that produces silence. "
366
- "Instead, use search_browser(path='drums') to find a kit preset "
367
- "(e.g., '808 Core Kit'), then load it with load_browser_item(). "
368
- "Or use DS drum synths (DS Kick, DS Snare, DS HH, DS Tom, DS Clap, "
369
- "DS Cymbal) which are self-contained."
366
+ "Options: (1) search_browser(path='drums') to find a kit preset "
367
+ "(e.g., '808 Core Kit'), then load with load_browser_item(). "
368
+ "(2) On Live 12.3+: use insert_device('Drum Rack') + insert_rack_chain "
369
+ "+ set_drum_chain_note to build a kit programmatically. "
370
+ "(3) DS drum synths (DS Kick, DS Snare, DS HH) are self-contained."
370
371
  )
371
372
 
372
373
  result = _get_ableton(ctx).send_command("find_and_load_device", {
@@ -376,6 +377,116 @@ def find_and_load_device(ctx: Context, track_index: int, device_name: str) -> di
376
377
  return _postflight_loaded_device(ctx, result)
377
378
 
378
379
 
380
+ @mcp.tool()
381
+ def insert_device(
382
+ ctx: Context,
383
+ track_index: int,
384
+ device_name: str,
385
+ position: int = -1,
386
+ device_index: Optional[int] = None,
387
+ chain_index: Optional[int] = None,
388
+ ) -> dict:
389
+ """Insert a native Live device by name — 10x faster than browser search (Live 12.3+).
390
+
391
+ Only works for native devices (Reverb, Compressor, EQ Eight, Drift, etc.).
392
+ For plugins, M4L devices, or presets, use find_and_load_device or load_browser_item.
393
+
394
+ track_index: 0+ for regular tracks, -1/-2 for returns, -1000 for master
395
+ device_name: exact device name (e.g. 'Reverb', 'Auto Filter', 'Wavetable')
396
+ position: device chain position (0 = first, -1 = end of chain)
397
+ device_index: required when inserting into a rack chain (identifies the rack)
398
+ chain_index: insert into this chain of a rack device (for building drum kits)
399
+
400
+ Drum Rack construction workflow (12.3+):
401
+ 1. insert_device(track_index, 'Drum Rack') — create empty rack
402
+ 2. insert_rack_chain(track_index, device_index) — add chains
403
+ 3. set_drum_chain_note(chain_index, note=36) — assign C1 (kick)
404
+ 4. insert_device(track_index, 'Simpler', — add instrument
405
+ device_index=rack_idx, chain_index=0) into chain
406
+
407
+ On Live < 12.3: returns an error suggesting find_and_load_device instead.
408
+ """
409
+ _validate_track_index(track_index)
410
+ if not device_name.strip():
411
+ raise ValueError("device_name cannot be empty")
412
+
413
+ params = {
414
+ "track_index": track_index,
415
+ "device_name": device_name,
416
+ "position": position,
417
+ }
418
+ if device_index is not None:
419
+ params["device_index"] = device_index
420
+ if chain_index is not None:
421
+ if device_index is None:
422
+ raise ValueError("device_index is required when chain_index is provided")
423
+ _validate_device_index(device_index)
424
+ _validate_chain_index(chain_index)
425
+ params["chain_index"] = chain_index
426
+
427
+ result = _get_ableton(ctx).send_command("insert_device", params)
428
+ return _postflight_loaded_device(ctx, result)
429
+
430
+
431
+ @mcp.tool()
432
+ def insert_rack_chain(
433
+ ctx: Context,
434
+ track_index: int,
435
+ device_index: int,
436
+ position: int = -1,
437
+ ) -> dict:
438
+ """Insert a new chain into a Rack device — Instrument Rack, Audio Effect Rack, or Drum Rack (Live 12.3+).
439
+
440
+ Use with insert_device + set_drum_chain_note to build Drum Racks from scratch:
441
+ 1. insert_device(track, 'Drum Rack') to create the rack
442
+ 2. insert_rack_chain(track, rack_device_index) to add chains
443
+ 3. set_drum_chain_note(chain_index=0, note=36) to assign C1 (kick)
444
+ 4. insert_device(track, 'Simpler', device_index=rack, chain_index=0) into chain
445
+
446
+ track_index: track containing the rack
447
+ device_index: rack device index on the track
448
+ position: chain position (-1 = append to end)
449
+ """
450
+ _validate_track_index(track_index)
451
+ _validate_device_index(device_index)
452
+
453
+ return _get_ableton(ctx).send_command("insert_rack_chain", {
454
+ "track_index": track_index,
455
+ "device_index": device_index,
456
+ "position": position,
457
+ })
458
+
459
+
460
+ @mcp.tool()
461
+ def set_drum_chain_note(
462
+ ctx: Context,
463
+ track_index: int,
464
+ device_index: int,
465
+ chain_index: int,
466
+ note: int,
467
+ ) -> dict:
468
+ """Set which MIDI note triggers a Drum Rack chain (Live 12.3+).
469
+
470
+ Standard drum mapping:
471
+ C1 (36) = Kick, D1 (38) = Snare, F#1 (42) = Closed HH,
472
+ A#1 (46) = Open HH, C#2 (49) = Crash, D#2 (51) = Ride
473
+
474
+ note: MIDI note 0-127, or -1 for 'All Notes'
475
+ """
476
+ _validate_track_index(track_index)
477
+ _validate_device_index(device_index)
478
+ _validate_chain_index(chain_index)
479
+ if note < -1 or note > 127:
480
+ raise ValueError("note must be -1 (All Notes) or 0-127")
481
+
482
+ return _get_ableton(ctx).send_command("set_drum_chain_note", {
483
+ "track_index": track_index,
484
+ "device_index": device_index,
485
+ "chain_index": chain_index,
486
+ "note": note,
487
+ })
488
+
489
+
379
490
  @mcp.tool()
380
491
  def set_simpler_playback_mode(
381
492
  ctx: Context,
@@ -9,6 +9,7 @@ import json
9
9
  from typing import Any, Optional
10
10
 
11
11
  from fastmcp import Context
12
+ from pydantic import BaseModel, Field
12
13
 
13
14
  from ..server import mcp
14
15
 
@@ -25,9 +26,36 @@ def _ensure_list(value: Any) -> list:
25
26
  return json.loads(value)
26
27
  except json.JSONDecodeError as exc:
27
28
  raise ValueError(f"Invalid JSON in parameter: {exc}") from exc
29
+ if isinstance(value, list):
30
+ normalized = []
31
+ for item in value:
32
+ if isinstance(item, BaseModel):
33
+ normalized.append(item.model_dump(exclude_none=True))
34
+ else:
35
+ normalized.append(item)
36
+ return normalized
28
37
  return value
29
38
 
30
39
 
40
+ class NoteSpec(BaseModel):
41
+ pitch: int = Field(ge=0, le=127)
42
+ start_time: float
43
+ duration: float = Field(gt=0)
44
+ velocity: Optional[float] = Field(default=None, ge=0.0, le=127.0)
45
+ probability: Optional[float] = Field(default=None, ge=0.0, le=1.0)
46
+ velocity_deviation: Optional[float] = Field(default=None, ge=-127.0, le=127.0)
47
+ release_velocity: Optional[float] = Field(default=None, ge=0.0, le=127.0)
48
+
49
+
50
+ class NoteModification(BaseModel):
51
+ note_id: int
52
+ pitch: Optional[int] = Field(default=None, ge=0, le=127)
53
+ start_time: Optional[float] = None
54
+ duration: Optional[float] = Field(default=None, gt=0)
55
+ velocity: Optional[float] = Field(default=None, ge=0.0, le=127.0)
56
+ probability: Optional[float] = Field(default=None, ge=0.0, le=1.0)
57
+
58
+
31
59
  def _validate_track_index(track_index: int):
32
60
  """Validate track index. Must be >= 0 for regular tracks."""
33
61
  if track_index < 0:
@@ -78,7 +106,7 @@ def _validate_note(note: dict):
78
106
 
79
107
 
80
108
  @mcp.tool()
81
- def add_notes(ctx: Context, track_index: int, clip_index: int, notes: Any) -> dict:
109
+ def add_notes(ctx: Context, track_index: int, clip_index: int, notes: list[NoteSpec] | str) -> dict:
82
110
  """Add MIDI notes to a clip. notes is a JSON array: [{pitch, start_time, duration, velocity?, probability?, velocity_deviation?, release_velocity?}]."""
83
111
  _validate_track_index(track_index)
84
112
  _validate_clip_index(clip_index)
@@ -157,7 +185,7 @@ def remove_notes(
157
185
 
158
186
 
159
187
  @mcp.tool()
160
- def remove_notes_by_id(ctx: Context, track_index: int, clip_index: int, note_ids: Any) -> dict:
188
+ def remove_notes_by_id(ctx: Context, track_index: int, clip_index: int, note_ids: list[int] | str) -> dict:
161
189
  """Remove specific MIDI notes by their IDs (JSON array of ints). Use undo to revert."""
162
190
  _validate_track_index(track_index)
163
191
  _validate_clip_index(clip_index)
@@ -172,7 +200,12 @@ def remove_notes_by_id(ctx: Context, track_index: int, clip_index: int, note_ids
172
200
 
173
201
 
174
202
  @mcp.tool()
175
- def modify_notes(ctx: Context, track_index: int, clip_index: int, modifications: Any) -> dict:
203
+ def modify_notes(
204
+ ctx: Context,
205
+ track_index: int,
206
+ clip_index: int,
207
+ modifications: list[NoteModification] | str,
208
+ ) -> dict:
176
209
  """Modify existing MIDI notes by ID. modifications is a JSON array: [{note_id, pitch?, start_time?, duration?, velocity?, probability?}]."""
177
210
  _validate_track_index(track_index)
178
211
  _validate_clip_index(clip_index)
@@ -202,7 +235,7 @@ def duplicate_notes(
202
235
  ctx: Context,
203
236
  track_index: int,
204
237
  clip_index: int,
205
- note_ids: Any,
238
+ note_ids: list[int] | str,
206
239
  time_offset: float = 0.0,
207
240
  ) -> dict:
208
241
  """Duplicate specific notes by ID (JSON array of ints), with optional time offset (in beats)."""
@@ -88,6 +88,9 @@ def plan_arrangement(
88
88
  style=style,
89
89
  )
90
90
 
91
+ # Add section-level sample role hints
92
+ planner_engine.add_sample_hints(plan)
93
+
91
94
  result = plan.to_dict()
92
95
  result["loop_identity"] = loop_identity.to_dict()
93
96
  result["available_styles"] = sorted(planner_engine.VALID_STYLES)
@@ -23,6 +23,11 @@ _DOMAIN_MAP: dict[str, list[str]] = {
23
23
  "too_safe_to_progress": ["sound_design", "transition"],
24
24
  "section_missing": ["arrangement", "transition"],
25
25
  "transition_not_earned": ["transition", "arrangement"],
26
+ # Sample-domain patterns (session-state analysis, not action-history)
27
+ "no_organic_texture": ["sample", "sound_design"],
28
+ "stale_drums": ["sample", "arrangement"],
29
+ "vocal_processing_monotony": ["sample", "sound_design"],
30
+ "dense_but_static": ["sample", "mix"],
26
31
  }
27
32
 
28
33
  _STUCKNESS_THRESHOLD = 0.2 # Below this, treat as user_request