livepilot 1.9.19 → 1.9.21

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
3
  "name": "dreamrec-LivePilot",
4
- "description": "Agentic MCP production system for Ableton Live 12 — 236 tools, 32 domains",
4
+ "description": "Agentic MCP production system for Ableton Live 12 — 237 tools, 32 domains",
5
5
  "owner": {
6
6
  "name": "dreamrec",
7
7
  "email": "dreamrec@users.noreply.github.com"
@@ -9,8 +9,8 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "livepilot",
12
- "description": "Agentic production system for Ableton Live 12 — 236 tools, 32 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
13
- "version": "1.9.19",
12
+ "description": "Agentic production system for Ableton Live 12 — 237 tools, 32 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
13
+ "version": "1.9.21",
14
14
  "author": {
15
15
  "name": "Pilot Studio"
16
16
  },
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.21 — Verification Discipline Pass (April 2026)
4
+
5
+ ### Systemic Fixes
6
+ - **fix(devices):** `set_device_parameter` and `batch_set_parameters` now return `value_string`, `min`, `max` in response — the agent can immediately see "26.0 Hz" instead of just "75" and catch nonsensical values
7
+ - **fix(automation):** `apply_automation_recipe` now auto-scales 0-1 recipe curves to the target parameter's actual native range (e.g., Auto Filter 20-135, Bit Depth 1-16). Previously, a "0.3 center" vinyl_crackle on a 20-135 range wrote 0.3 literally, killing audio
8
+ - **fix(automation):** `auto_pan` recipe pan values now clamped to ±0.6 to prevent full L/R swing that makes tracks disappear from one channel
9
+ - **docs(skill):** Added Golden Rules 15-16 — mandatory post-write verification protocol: always read `value_string`, always check track meters after filter/effect changes, never apply automation recipes without understanding the target parameter's range
10
+
11
+ ## 1.9.20 — Deep Production Test Pass (April 2026)
12
+
13
+ ### New Tool
14
+ - **feat(analyzer):** `reconnect_bridge` — rebind UDP 9880 mid-session after port conflict clears, without restarting the MCP server
15
+
16
+ ### Bug Fixes
17
+ - **fix(arrangement):** `set_arrangement_automation` now returns `STATE_ERROR` (not `INVALID_PARAM`) with clear workaround suggestions for the known Live API limitation
18
+ - **fix(router):** added `RuntimeError` → `STATE_ERROR` mapping so state-related errors don't masquerade as parameter validation failures
19
+ - **fix(browser):** `load_browser_item` now accepts negative track_index (-1/-2 for returns, -1000 for master) — was incorrectly rejected by MCP-side validator
20
+ - **fix(tracks):** `set_track_name` on return tracks strips auto-prefix letter to prevent doubling (e.g. "C-Resonators" stays as-is, not "C-C-Resonators")
21
+ - **fix(theory):** `suggest_next_chord` now uses mode-aware progression maps — separate major/minor chord tables for common_practice, jazz, modal, and pop styles
22
+ - **fix(research):** `research_technique` now searches instruments, audio_effects, AND drums categories (was instruments-only); deep scope notes that web search is agent-layer responsibility
23
+ - **fix(server):** improved port competition error messages — directs users to `reconnect_bridge` tool instead of requiring server restart
24
+ - **fix(analyzer):** M4L Analyzer User Library copy synced to latest version (presentation mode enabled, bridge JS updated)
25
+
26
+ ### Documentation
27
+ - **docs(skill):** added "Volume reset on scene fire" and "M4L Analyzer auto-load" to error handling protocol in livepilot-core skill
28
+ - **docs(CLAUDE.md):** tool count updated from 236 to 237
29
+
3
30
  ## 1.9.19 — Theory Engine & Meters Fix Pass (April 2026)
4
31
 
5
32
  ### Bug Fixes
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.19",
4
- "description": "Agentic production system for Ableton Live 12 — 236 tools, 32 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
3
+ "version": "1.9.21",
4
+ "description": "Agentic production system for Ableton Live 12 — 237 tools, 32 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.19",
4
- "description": "Agentic production system for Ableton Live 12 — 236 tools, 32 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
3
+ "version": "1.9.21",
4
+ "description": "Agentic production system for Ableton Live 12 — 237 tools, 32 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
7
7
  }
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: livepilot-core
3
- description: Core discipline for LivePilot — agentic production system for Ableton Live 12. 236 tools across 32 domains. This skill should be used whenever working with Ableton Live through MCP tools. Provides golden rules, tool speed tiers, error handling protocol, and pointers to domain and engine skills.
3
+ description: Core discipline for LivePilot — agentic production system for Ableton Live 12. 237 tools across 32 domains. This skill should be used whenever working with Ableton Live through MCP tools. Provides golden rules, tool speed tiers, error handling protocol, and pointers to domain and engine skills.
4
4
  ---
5
5
 
6
6
  # LivePilot Core — Ableton Live 12
7
7
 
8
- Agentic production system for Ableton Live 12. 236 tools across 32 domains, three layers:
8
+ Agentic production system for Ableton Live 12. 237 tools across 32 domains, three layers:
9
9
 
10
10
  - **Device Atlas** — 280+ instruments, 139 drum kits, 350+ impulse responses. Consult `references/device-atlas/` before loading any device. Never guess a device name.
11
11
  - **M4L Analyzer** — Real-time audio analysis on the master bus (8-band spectrum, RMS/peak, key detection). Optional — all core tools work without it.
@@ -27,6 +27,13 @@ Agentic production system for Ableton Live 12. 236 tools across 32 domains, thre
27
27
  12. **ALWAYS report tool errors** — never silently swallow errors. Include: tool name, error message, fallback plan
28
28
  13. **Verify plugin health after loading** — check `health_flags`, `mcp_sound_design_ready`, `plugin_host_status`. If `parameter_count` <= 1 on AU/VST → dead plugin, delete and replace
29
29
  14. **Use `C hijaz` for Hijaz/Phrygian Dominant keys** — avoids false out-of-key warnings
30
+ 15. **VERIFY AFTER EVERY WRITE** — mandatory, non-negotiable:
31
+ - After `set_device_parameter` or `batch_set_parameters`: read `value_string` in the response to confirm the actual Hz/dB/% value makes sense
32
+ - After any filter, EQ, or effect parameter change: call `get_track_meters(include_stereo=true)` and verify the target track has non-zero left AND right levels
33
+ - After `apply_automation_recipe`: check that the recipe didn't push the parameter to an extreme that kills audio
34
+ - If a track's stereo output drops to 0: the effect is killing the signal — check `get_device_parameters` for `value_string`, fix, re-verify
35
+ - **Parameter ranges are NOT always 0-1.** Auto Filter Frequency is 20-135. Bit Depth is 1-16. Always read `value_string` to see actual units.
36
+ 16. **NEVER apply automation recipes without understanding the target parameter's range** — recipes generate 0-1 curves that get auto-scaled for device parameters, but always verify the result
30
37
 
31
38
  ## Tool Speed Tiers
32
39
 
@@ -54,6 +61,8 @@ Report ALL errors to the user immediately. Common failure modes:
54
61
  - **Empty Drum Rack** — bare rack = silence → always load a kit preset
55
62
  - **M4L bridge timeout** — device may be busy or removed → retry or skip analyzer features
56
63
  - **Connection timeout** — Ableton unresponsive → check if session is heavy
64
+ - **Volume reset on scene fire** — Ableton restores mixer state when firing scenes. Always re-apply `set_track_volume`/`set_track_pan` after `fire_scene` if your mix settings differ from what was stored in the clips
65
+ - **M4L Analyzer not connected** — if `get_master_spectrum` errors with "Analyzer not detected", auto-load it: `find_and_load_device(track_index=-1000, device_name="LivePilot_Analyzer")`. If it errors with "UDP bridge not connected", try `reconnect_bridge` first
57
66
 
58
67
  ## Technique Memory
59
68
 
@@ -91,7 +100,7 @@ Deep production knowledge in `references/`:
91
100
 
92
101
  | File | Content |
93
102
  |------|---------|
94
- | `references/overview.md` | All 236 tools with params and ranges |
103
+ | `references/overview.md` | All 237 tools with params and ranges |
95
104
  | `references/device-atlas/` | 280+ device corpus with URIs and presets |
96
105
  | `references/midi-recipes.md` | Drum patterns, chord voicings, humanization |
97
106
  | `references/sound-design.md` | Synth recipes, device chain patterns |
@@ -1,6 +1,6 @@
1
- # LivePilot v1.9.19 — Architecture & Tool Reference
1
+ # LivePilot v1.9.21 — Architecture & Tool Reference
2
2
 
3
- Agentic production system for Ableton Live 12. 236 tools across 32 domains. Device atlas (280+ devices), spectral perception (M4L analyzer), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O.
3
+ Agentic production system for Ableton Live 12. 237 tools across 32 domains. Device atlas (280+ devices), spectral perception (M4L analyzer), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O.
4
4
 
5
5
  ## Architecture
6
6
 
@@ -84,7 +84,7 @@ function anything() {
84
84
  function dispatch(cmd, args) {
85
85
  switch(cmd) {
86
86
  case "ping":
87
- send_response({"ok": true, "version": "1.9.19"});
87
+ send_response({"ok": true, "version": "1.9.21"});
88
88
  break;
89
89
  case "get_params":
90
90
  cmd_get_params(args);
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.9.19"
2
+ __version__ = "1.9.21"
@@ -58,27 +58,37 @@ async def lifespan(server):
58
58
  )
59
59
  except OSError:
60
60
  # Port 9880 already bound — another LivePilot instance is running.
61
- # Do NOT kill the holder; degrade gracefully instead.
61
+ # Degrade gracefully. The reconnect_bridge tool can retry later
62
+ # if the other instance is stopped.
62
63
  import sys
63
64
  holder_info = _identify_port_holder(9880)
64
65
  print(
65
66
  "LivePilot: UDP port 9880 already in use%s — "
66
- "analyzer/bridge tools will be unavailable. "
67
- "Stop the other LivePilot instance to enable them."
67
+ "analyzer/bridge tools unavailable at startup. "
68
+ "Use the reconnect_bridge tool after stopping the other instance, "
69
+ "or restart this server."
68
70
  % (f" (PID {holder_info})" if holder_info else ""),
69
71
  file=sys.stderr,
70
72
  )
71
73
  transport = None
72
74
 
75
+ # Store transport + loop so tools can attempt reconnection mid-session
76
+ bridge_state = {
77
+ "transport": transport,
78
+ "loop": loop,
79
+ "receiver": receiver,
80
+ }
81
+
73
82
  try:
74
83
  yield {
75
84
  "ableton": ableton,
76
85
  "spectral": spectral,
77
86
  "m4l": m4l,
87
+ "_bridge_state": bridge_state,
78
88
  }
79
89
  finally:
80
- if transport:
81
- transport.close()
90
+ if bridge_state["transport"]:
91
+ bridge_state["transport"].close()
82
92
  m4l.close()
83
93
  ableton.disconnect()
84
94
 
@@ -77,6 +77,42 @@ def _require_analyzer(cache) -> None:
77
77
  )
78
78
 
79
79
 
80
+ @mcp.tool()
81
+ async def reconnect_bridge(ctx: Context) -> dict:
82
+ """Attempt to reconnect the M4L UDP bridge (port 9880).
83
+
84
+ Use this when the bridge was unavailable at server startup (port
85
+ conflict) but is now free. Binds the UDP listener so spectral
86
+ analysis and bridge commands become available without restarting
87
+ the MCP server.
88
+ """
89
+ import asyncio
90
+
91
+ bridge_state = ctx.lifespan_context.get("_bridge_state")
92
+ if not bridge_state:
93
+ return {"error": "Bridge state not available — restart the MCP server"}
94
+
95
+ if bridge_state["transport"] is not None:
96
+ return {"ok": True, "message": "Bridge already connected on UDP 9880"}
97
+
98
+ loop = bridge_state["loop"]
99
+ receiver = bridge_state["receiver"]
100
+ try:
101
+ transport, _ = await loop.create_datagram_endpoint(
102
+ lambda: receiver,
103
+ local_addr=('127.0.0.1', 9880),
104
+ )
105
+ bridge_state["transport"] = transport
106
+ return {"ok": True, "message": "Bridge reconnected on UDP 9880"}
107
+ except OSError:
108
+ holder = _identify_port_holder(9880)
109
+ return {
110
+ "ok": False,
111
+ "error": f"UDP port 9880 still in use{f' (PID {holder})' if holder else ''}. "
112
+ "Close the other LivePilot instance first.",
113
+ }
114
+
115
+
80
116
  @mcp.tool()
81
117
  def get_master_spectrum(ctx: Context) -> dict:
82
118
  """Get 8-band frequency analysis of the master bus.
@@ -341,6 +341,32 @@ def apply_automation_recipe(
341
341
 
342
342
  points = generate_from_recipe(recipe, duration=duration, density=density)
343
343
 
344
+ # Scale recipe's 0.0-1.0 curves to the parameter's actual native range.
345
+ # Without this, a "0.3" center on a 20-135 range parameter writes 0.3
346
+ # literally instead of scaling to the 20-135 range — killing the signal.
347
+ if parameter_type == "device" and device_index is not None and parameter_index is not None:
348
+ try:
349
+ dev_info = _get_ableton(ctx).send_command("get_device_parameters", {
350
+ "track_index": track_index,
351
+ "device_index": device_index,
352
+ })
353
+ params_list = dev_info.get("parameters", [])
354
+ if parameter_index < len(params_list):
355
+ p_info = params_list[parameter_index]
356
+ p_min = float(p_info.get("min", 0))
357
+ p_max = float(p_info.get("max", 1))
358
+ # Only scale if the range is NOT already 0-1
359
+ if abs(p_max - p_min) > 1.5 or p_min < -0.5:
360
+ for pt in points:
361
+ pt["value"] = p_min + pt["value"] * (p_max - p_min)
362
+ except Exception:
363
+ pass # Fail open — write values as-is if we can't read the range
364
+
365
+ # Safety clamp: auto_pan amplitude should be limited to avoid full L/R swing
366
+ if recipe == "auto_pan" and parameter_type == "panning":
367
+ for pt in points:
368
+ pt["value"] = max(-0.6, min(0.6, pt["value"]))
369
+
344
370
  if time_offset > 0:
345
371
  for p in points:
346
372
  p["time"] += time_offset
@@ -18,9 +18,16 @@ def _get_ableton(ctx: Context):
18
18
 
19
19
 
20
20
  def _validate_track_index(track_index: int):
21
- """Validate track index. Must be >= 0 for regular tracks."""
22
- if track_index < 0:
23
- raise ValueError("track_index must be >= 0")
21
+ """Validate track index.
22
+
23
+ 0+ for regular tracks, -1/-2/... for return tracks (A/B/...),
24
+ -1000 for master track.
25
+ """
26
+ if track_index < 0 and track_index != -1000 and track_index < -20:
27
+ raise ValueError(
28
+ "track_index must be >= 0 for regular tracks, "
29
+ "-1/-2/... for return tracks, or -1000 for master"
30
+ )
24
31
 
25
32
 
26
33
  @mcp.tool()
@@ -53,18 +53,21 @@ def research_technique(
53
53
  # 1. Analyze query to predict relevant devices
54
54
  query_info = research_engine.analyze_query(query)
55
55
 
56
- # 2. Search device atlas for relevant devices (Fix 2: correct params)
56
+ # 2. Search device atlas for relevant devices across all categories
57
57
  device_atlas_results = []
58
58
  for device_name in query_info.get("likely_devices", [])[:5]:
59
- try:
60
- ref = ableton.send_command("search_browser", {
61
- "path": "instruments",
62
- "name_filter": device_name,
63
- })
64
- if ref and not ref.get("error"):
65
- device_atlas_results.append(ref)
66
- except Exception:
67
- pass
59
+ for search_path in ("instruments", "audio_effects", "drums"):
60
+ try:
61
+ ref = ableton.send_command("search_browser", {
62
+ "path": search_path,
63
+ "name_filter": device_name,
64
+ "max_results": 5,
65
+ })
66
+ if ref and not ref.get("error") and ref.get("count", 0) > 0:
67
+ device_atlas_results.append(ref)
68
+ break # Found in this category, skip others
69
+ except Exception:
70
+ pass
68
71
 
69
72
  # 3. Search memory for related techniques (direct TechniqueStore)
70
73
  memory_results = []
@@ -88,14 +91,21 @@ def research_technique(
88
91
  query, device_atlas_results, memory_results,
89
92
  )
90
93
  else:
91
- # Deep research — try to get web results
92
- # Note: web search requires external integration (graceful degradation)
94
+ # Deep research — web search is delegated to the agent (LLM) layer.
95
+ # The MCP server cannot perform web searches directly. When scope
96
+ # is "deep", we still return device atlas + memory results and flag
97
+ # that the agent should supplement with its own web search.
93
98
  result = research_engine.deep_research(
94
99
  query,
95
- web_results=[], # Web search injected by agent if available
100
+ web_results=[], # Agent supplements with WebSearch tool
96
101
  device_atlas_results=device_atlas_results,
97
102
  memory_results=memory_results,
98
103
  )
104
+ # Flag to the caller that web results should be sourced externally
105
+ result.web_search_note = (
106
+ "Deep scope requested but web search is handled by the agent layer. "
107
+ "Use WebSearch or web browsing tools to supplement these device atlas findings."
108
+ )
99
109
 
100
110
  return result.to_dict()
101
111
 
@@ -161,8 +161,9 @@ def suggest_next_chord(
161
161
  last_rn = engine.roman_numeral(last_group["pitch_classes"], tonic, mode)
162
162
  last_figure = last_rn["figure"]
163
163
 
164
- # Progression maps by style
165
- _progressions = {
164
+ # Progression maps by style — separate major/minor variants where needed.
165
+ # Minor key maps use lowercase i for tonic and scale-derived numerals.
166
+ _progressions_major = {
166
167
  "common_practice": {
167
168
  "I": ["IV", "V", "vi", "ii"],
168
169
  "ii": ["V", "vii\u00b0", "IV"],
@@ -173,10 +174,10 @@ def suggest_next_chord(
173
174
  "vii\u00b0": ["I", "iii"],
174
175
  },
175
176
  "jazz": {
176
- "I": ["IV7", "ii7", "vi7", "bVII7"],
177
- "ii7": ["V7", "bII7"],
178
- "IV7": ["V7", "#ivo7", "bVII7"],
179
- "V7": ["I", "vi", "bVI"],
177
+ "I": ["IV7", "ii7", "vi7", "V7"],
178
+ "ii7": ["V7", "IV7"],
179
+ "IV7": ["V7", "I", "ii7"],
180
+ "V7": ["I", "vi", "IV"],
180
181
  "vi7": ["ii7", "IV7"],
181
182
  },
182
183
  "modal": {
@@ -194,8 +195,47 @@ def suggest_next_chord(
194
195
  "vi": ["IV", "V", "I"],
195
196
  },
196
197
  }
198
+ _progressions_minor = {
199
+ "common_practice": {
200
+ "i": ["iv", "v", "VI", "III"],
201
+ "ii\u00b0": ["v", "VII", "iv"],
202
+ "III": ["VI", "iv", "VII"],
203
+ "iv": ["v", "i", "VII"],
204
+ "v": ["i", "VI", "iv"],
205
+ "VI": ["iv", "ii\u00b0", "v", "VII"],
206
+ "VII": ["III", "i"],
207
+ },
208
+ "jazz": {
209
+ "i": ["iv7", "v", "VI7", "VII7"],
210
+ "i7": ["iv7", "v", "VI7", "VII7"],
211
+ "ii\u00b07": ["v", "VII7"],
212
+ "iv7": ["VII7", "v", "i"],
213
+ "v": ["i", "VI", "iv"],
214
+ "VI7": ["ii\u00b07", "iv7", "VII7"],
215
+ "VImaj7": ["ii\u00b07", "iv7", "VII7"],
216
+ "VII7": ["III", "i", "VI"],
217
+ },
218
+ "modal": {
219
+ "i": ["VII", "iv", "v", "III"],
220
+ "iv": ["i", "VII", "v"],
221
+ "v": ["VII", "iv", "i"],
222
+ "VII": ["i", "iv", "v"],
223
+ "III": ["iv", "VII"],
224
+ },
225
+ "pop": {
226
+ "i": ["VII", "VI", "iv"],
227
+ "iv": ["i", "VII", "VI"],
228
+ "v": ["i", "VI", "iv"],
229
+ "VI": ["iv", "VII", "i"],
230
+ "VII": ["i", "VI", "III"],
231
+ },
232
+ }
197
233
 
198
- style_map = _progressions.get(style, _progressions["common_practice"])
234
+ # Select the right map based on mode
235
+ is_minor = mode in ("minor", "dorian", "phrygian", "aeolian",
236
+ "phrygian_dominant")
237
+ prog_set = _progressions_minor if is_minor else _progressions_major
238
+ style_map = prog_set.get(style, prog_set.get("common_practice", {}))
199
239
 
200
240
  # Match the last chord to the closest key in the map
201
241
  candidates = style_map.get(last_figure)
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.19",
3
+ "version": "1.9.21",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 236 tools, 32 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
+ "description": "Agentic production system for Ableton Live 12 — 237 tools, 32 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
6
6
  "author": "Pilot Studio",
7
7
  "license": "MIT",
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.9.19"
8
+ __version__ = "1.9.21"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from .server import LivePilotServer
@@ -543,13 +543,15 @@ def set_arrangement_automation(song, params):
543
543
  # arming the track and playing back with parameter changes,
544
544
  # or use session-clip automation (set_clip_automation) and then
545
545
  # record the session performance to arrangement.
546
- raise ValueError(
546
+ raise RuntimeError(
547
547
  "Cannot create automation envelope for parameter '%s' on this "
548
- "arrangement clip. This is a known Live API limitation for "
549
- "programmatically-created arrangement clips. Workarounds: "
550
- "(1) use set_clip_automation on a session clip instead, "
551
- "(2) record automation by arming the track during playback, "
552
- "or (3) use the M4L bridge for deeper LOM envelope access."
548
+ "arrangement clip. Direct envelope access is not supported for "
549
+ "programmatically-created arrangement clips (known Live API limitation). "
550
+ "Workarounds: "
551
+ "(1) use set_clip_automation on a session clip instead, then fire "
552
+ "the scene and record to arrangement; "
553
+ "(2) use arrangement-level volume/pan fades by creating separate "
554
+ "clips at different volumes for each section."
553
555
  % parameter.name
554
556
  )
555
557
 
@@ -104,7 +104,13 @@ def set_device_parameter(song, params):
104
104
  raise ValueError("Must provide parameter_name or parameter_index")
105
105
 
106
106
  param.value = value
107
- return {"name": param.name, "value": param.value}
107
+ return {
108
+ "name": param.name,
109
+ "value": param.value,
110
+ "value_string": param.str_for_value(param.value),
111
+ "min": param.min,
112
+ "max": param.max,
113
+ }
108
114
 
109
115
 
110
116
  @register("batch_set_parameters")
@@ -157,7 +163,11 @@ def batch_set_parameters(song, params):
157
163
  )
158
164
 
159
165
  param.value = value
160
- results.append({"name": param.name, "value": param.value})
166
+ results.append({
167
+ "name": param.name,
168
+ "value": param.value,
169
+ "value_string": param.str_for_value(param.value),
170
+ })
161
171
 
162
172
  return {"parameters": results}
163
173
 
@@ -11,6 +11,7 @@ from .utils import (
11
11
  INDEX_ERROR,
12
12
  INVALID_PARAM,
13
13
  NOT_FOUND,
14
+ STATE_ERROR,
14
15
  INTERNAL,
15
16
  )
16
17
 
@@ -92,5 +93,7 @@ def dispatch(song, command):
92
93
  return error_response(request_id, str(exc), INDEX_ERROR)
93
94
  except ValueError as exc:
94
95
  return error_response(request_id, str(exc), INVALID_PARAM)
96
+ except RuntimeError as exc:
97
+ return error_response(request_id, str(exc), STATE_ERROR)
95
98
  except Exception as exc:
96
99
  return error_response(request_id, str(exc), INTERNAL)
@@ -193,10 +193,29 @@ def duplicate_track(song, params):
193
193
 
194
194
  @register("set_track_name")
195
195
  def set_track_name(song, params):
196
- """Rename a track."""
196
+ """Rename a track.
197
+
198
+ For return tracks, Ableton auto-prefixes with a letter (A-, B-, C-).
199
+ If the requested name already starts with that prefix, strip it to
200
+ avoid doubling (e.g. "C-Resonators" stays as "C-Resonators" not
201
+ "C-C-Resonators").
202
+ """
197
203
  track_index = int(params["track_index"])
198
204
  track = get_track(song, track_index)
199
- track.name = str(params["name"])
205
+ new_name = str(params["name"])
206
+
207
+ # For return tracks, strip auto-prefix from user's name if it matches
208
+ if track_index < 0 and track_index != -1000:
209
+ return_tracks = list(song.return_tracks)
210
+ ri = abs(track_index) - 1
211
+ if ri < len(return_tracks):
212
+ # Return tracks have letter prefixes: "A-", "B-", "C-", etc.
213
+ letter = chr(ord('A') + ri)
214
+ prefix = letter + "-"
215
+ if new_name.startswith(prefix):
216
+ new_name = new_name[len(prefix):]
217
+
218
+ track.name = new_name
200
219
  return {"index": track_index, "name": track.name}
201
220
 
202
221