livepilot 1.9.5 → 1.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +6 -0
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/references/overview.md +1 -1
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +50 -26
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"name": "livepilot",
|
|
12
12
|
"description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
13
|
-
"version": "1.9.
|
|
13
|
+
"version": "1.9.6",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Pilot Studio"
|
|
16
16
|
},
|
package/AGENTS.md
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.6 — Arrangement clip identification + expression data (March 2026)
|
|
4
|
+
|
|
5
|
+
- Fix(P1): create_arrangement_clip now identifies new clips by object identity, not position match — prevents mutating pre-existing overlapping clips
|
|
6
|
+
- Fix(P2): set_arrangement_automation fallback preserves probability, velocity_deviation, release_velocity when rebuilding notes
|
|
7
|
+
- Fix(P2): get_arrangement_clips effective length uses loop_end - loop_start (not just loop_end)
|
|
8
|
+
|
|
3
9
|
## 1.9.5 — TCP Retry Fix + Arrangement Automation Fix (March 2026)
|
|
4
10
|
|
|
5
11
|
- Fix(P1): disconnect() now clears _recv_buf — prevents partial JSON corruption on TCP retry
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.6",
|
|
4
4
|
"description": "Agentic production system for Ableton Live 12 — 178 tools, 17 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"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# LivePilot v1.9.
|
|
1
|
+
# LivePilot v1.9.6 — Architecture & Tool Reference
|
|
2
2
|
|
|
3
3
|
Agentic production system for Ableton Live 12. 178 tools across 17 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
|
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.9.
|
|
2
|
+
__version__ = "1.9.6"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.6",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
5
|
"description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
6
6
|
"author": "Pilot Studio",
|
|
@@ -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.
|
|
8
|
+
__version__ = "1.9.6"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from .server import LivePilotServer
|
|
@@ -18,8 +18,10 @@ def get_arrangement_clips(song, params):
|
|
|
18
18
|
# (arrangement clips may have been trimmed by create_arrangement_clip)
|
|
19
19
|
effective_length = timeline_length
|
|
20
20
|
try:
|
|
21
|
-
if clip.looping
|
|
22
|
-
|
|
21
|
+
if clip.looping:
|
|
22
|
+
loop_len = clip.loop_end - clip.loop_start
|
|
23
|
+
if 0 < loop_len < timeline_length:
|
|
24
|
+
effective_length = loop_len
|
|
23
25
|
except (AttributeError, RuntimeError):
|
|
24
26
|
pass
|
|
25
27
|
clips.append({
|
|
@@ -86,35 +88,49 @@ def create_arrangement_clip(song, params):
|
|
|
86
88
|
first_clip_index = None
|
|
87
89
|
|
|
88
90
|
while pos < end_pos:
|
|
91
|
+
# Snapshot clip IDs before duplication to identify the new one
|
|
92
|
+
old_ids = set(id(c) for c in track.arrangement_clips)
|
|
93
|
+
|
|
89
94
|
track.duplicate_clip_to_arrangement(source_clip, pos)
|
|
90
95
|
|
|
91
|
-
# Find
|
|
96
|
+
# Find the NEW clip (not in old_ids) at the target position
|
|
92
97
|
arr_clips = list(track.arrangement_clips)
|
|
98
|
+
new_clip = None
|
|
99
|
+
new_clip_idx = None
|
|
93
100
|
for i, c in enumerate(arr_clips):
|
|
94
|
-
if abs(c.start_time - pos) < 0.01:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if name:
|
|
98
|
-
c.name = name
|
|
99
|
-
if color_index is not None:
|
|
100
|
-
c.color_index = int(color_index)
|
|
101
|
-
|
|
102
|
-
# When loop_length < source_length, set the internal
|
|
103
|
-
# loop region so only loop_length beats of content play.
|
|
104
|
-
# Arrangement clip timeline length is read-only in the
|
|
105
|
-
# LOM, but overlapping clips are handled by Ableton
|
|
106
|
-
# (later clips take priority), so playback is correct.
|
|
107
|
-
remaining = end_pos - pos
|
|
108
|
-
target_len = min(loop_length, remaining)
|
|
109
|
-
if target_len < source_length:
|
|
110
|
-
try:
|
|
111
|
-
c.looping = True
|
|
112
|
-
c.loop_start = 0.0
|
|
113
|
-
c.loop_end = target_len
|
|
114
|
-
except (AttributeError, RuntimeError):
|
|
115
|
-
pass
|
|
101
|
+
if id(c) not in old_ids and abs(c.start_time - pos) < 0.01:
|
|
102
|
+
new_clip = c
|
|
103
|
+
new_clip_idx = i
|
|
116
104
|
break
|
|
117
105
|
|
|
106
|
+
# Fallback: if id-based detection fails, match by position
|
|
107
|
+
if new_clip is None:
|
|
108
|
+
for i, c in enumerate(arr_clips):
|
|
109
|
+
if abs(c.start_time - pos) < 0.01:
|
|
110
|
+
new_clip = c
|
|
111
|
+
new_clip_idx = i
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
if new_clip is not None:
|
|
115
|
+
if first_clip_index is None:
|
|
116
|
+
first_clip_index = new_clip_idx
|
|
117
|
+
if name:
|
|
118
|
+
new_clip.name = name
|
|
119
|
+
if color_index is not None:
|
|
120
|
+
new_clip.color_index = int(color_index)
|
|
121
|
+
|
|
122
|
+
# When loop_length < source_length, set the internal
|
|
123
|
+
# loop region so only loop_length beats of content play.
|
|
124
|
+
remaining = end_pos - pos
|
|
125
|
+
target_len = min(loop_length, remaining)
|
|
126
|
+
if target_len < source_length:
|
|
127
|
+
try:
|
|
128
|
+
new_clip.looping = True
|
|
129
|
+
new_clip.loop_start = 0.0
|
|
130
|
+
new_clip.loop_end = target_len
|
|
131
|
+
except (AttributeError, RuntimeError):
|
|
132
|
+
pass
|
|
133
|
+
|
|
118
134
|
clip_count += 1
|
|
119
135
|
pos += loop_length
|
|
120
136
|
|
|
@@ -574,13 +590,21 @@ def set_arrangement_automation(song, params):
|
|
|
574
590
|
import Live
|
|
575
591
|
note_specs = []
|
|
576
592
|
for note in orig_notes:
|
|
577
|
-
|
|
593
|
+
kwargs = dict(
|
|
578
594
|
pitch=note.pitch,
|
|
579
595
|
start_time=note.start_time,
|
|
580
596
|
duration=note.duration,
|
|
581
597
|
velocity=note.velocity,
|
|
582
598
|
mute=note.mute,
|
|
583
599
|
)
|
|
600
|
+
# Preserve per-note expression data
|
|
601
|
+
if hasattr(note, 'probability') and note.probability is not None:
|
|
602
|
+
kwargs['probability'] = note.probability
|
|
603
|
+
if hasattr(note, 'velocity_deviation') and note.velocity_deviation is not None:
|
|
604
|
+
kwargs['velocity_deviation'] = note.velocity_deviation
|
|
605
|
+
if hasattr(note, 'release_velocity') and note.release_velocity is not None:
|
|
606
|
+
kwargs['release_velocity'] = note.release_velocity
|
|
607
|
+
spec = Live.Clip.MidiNoteSpecification(**kwargs)
|
|
584
608
|
note_specs.append(spec)
|
|
585
609
|
if note_specs:
|
|
586
610
|
temp_clip.add_new_notes(tuple(note_specs))
|