livepilot 1.6.5 → 1.7.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.
- package/CHANGELOG.md +29 -0
- package/README.md +20 -5
- package/bin/livepilot.js +2 -2
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/server.py +3 -0
- package/mcp_server/tools/_generative_engine.py +271 -0
- package/mcp_server/tools/_harmony_engine.py +207 -0
- package/mcp_server/tools/generative.py +273 -0
- package/mcp_server/tools/harmony.py +253 -0
- package/mcp_server/tools/midi_io.py +305 -0
- package/package.json +2 -2
- package/plugin/plugin.json +2 -2
- package/plugin/skills/livepilot-core/SKILL.md +44 -6
- package/plugin/skills/livepilot-core/references/overview.md +3 -3
- package/remote_script/LivePilot/__init__.py +2 -2
- package/requirements.txt +3 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""MIDI file I/O tools — export, import, analyze, piano roll.
|
|
2
|
+
|
|
3
|
+
4 tools bridging LivePilot's session clips with .mid files.
|
|
4
|
+
Tools 1-2 require Ableton connection. Tools 3-4 are offline-capable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import statistics
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from fastmcp import Context
|
|
16
|
+
|
|
17
|
+
from ..server import mcp
|
|
18
|
+
from . import _theory_engine as theory
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_ableton(ctx: Context):
|
|
22
|
+
return ctx.lifespan_context["ableton"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _require_midiutil():
|
|
26
|
+
try:
|
|
27
|
+
from midiutil import MIDIFile
|
|
28
|
+
return MIDIFile
|
|
29
|
+
except ImportError:
|
|
30
|
+
raise ImportError(
|
|
31
|
+
"midiutil is required for MIDI export. "
|
|
32
|
+
"Install with: pip install midiutil"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _require_pretty_midi():
|
|
37
|
+
try:
|
|
38
|
+
import pretty_midi
|
|
39
|
+
return pretty_midi
|
|
40
|
+
except ImportError:
|
|
41
|
+
raise ImportError(
|
|
42
|
+
"pretty-midi is required for this tool. "
|
|
43
|
+
"Install with: pip install pretty-midi"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _output_dir() -> Path:
|
|
48
|
+
d = Path.home() / "Documents" / "LivePilot" / "outputs" / "midi"
|
|
49
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
return d
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _validate_midi_path(file_path: str) -> Path:
|
|
54
|
+
p = Path(file_path)
|
|
55
|
+
if not p.exists():
|
|
56
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
57
|
+
if p.suffix.lower() not in (".mid", ".midi"):
|
|
58
|
+
raise ValueError(f"Not a MIDI file: {file_path}")
|
|
59
|
+
return p
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# -- Tool 1: export_clip_midi ------------------------------------------------
|
|
63
|
+
|
|
64
|
+
@mcp.tool()
|
|
65
|
+
def export_clip_midi(
|
|
66
|
+
ctx: Context,
|
|
67
|
+
track_index: int,
|
|
68
|
+
clip_index: int,
|
|
69
|
+
filename: Optional[str] = None,
|
|
70
|
+
) -> dict:
|
|
71
|
+
"""Export a session clip's notes to a .mid file.
|
|
72
|
+
|
|
73
|
+
Fetches notes from the clip and writes them to a standard MIDI file.
|
|
74
|
+
Auto-generates filename from track/clip if not provided.
|
|
75
|
+
"""
|
|
76
|
+
MIDIFile = _require_midiutil()
|
|
77
|
+
ableton = _get_ableton(ctx)
|
|
78
|
+
|
|
79
|
+
notes_result = ableton.send_command("get_notes", {
|
|
80
|
+
"track_index": track_index,
|
|
81
|
+
"clip_index": clip_index,
|
|
82
|
+
})
|
|
83
|
+
notes = notes_result.get("notes", [])
|
|
84
|
+
|
|
85
|
+
session = ableton.send_command("get_session_info", {})
|
|
86
|
+
tempo = float(session.get("tempo", 120.0))
|
|
87
|
+
|
|
88
|
+
if not filename:
|
|
89
|
+
filename = f"track{track_index}_clip{clip_index}.mid"
|
|
90
|
+
if not filename.endswith((".mid", ".midi")):
|
|
91
|
+
filename += ".mid"
|
|
92
|
+
|
|
93
|
+
out_path = _output_dir() / filename
|
|
94
|
+
|
|
95
|
+
midi = MIDIFile(1)
|
|
96
|
+
midi.addTempo(0, 0, tempo)
|
|
97
|
+
|
|
98
|
+
duration_beats = 0.0
|
|
99
|
+
for n in notes:
|
|
100
|
+
start = float(n["start_time"])
|
|
101
|
+
dur = float(n["duration"])
|
|
102
|
+
pitch = int(n["pitch"])
|
|
103
|
+
vel = int(n.get("velocity", 100))
|
|
104
|
+
midi.addNote(0, 0, pitch, start, dur, vel)
|
|
105
|
+
duration_beats = max(duration_beats, start + dur)
|
|
106
|
+
|
|
107
|
+
with open(out_path, "wb") as f:
|
|
108
|
+
midi.writeFile(f)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"file_path": str(out_path),
|
|
112
|
+
"note_count": len(notes),
|
|
113
|
+
"duration_beats": round(duration_beats, 4),
|
|
114
|
+
"tempo": tempo,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# -- Tool 2: import_midi_to_clip ---------------------------------------------
|
|
119
|
+
|
|
120
|
+
@mcp.tool()
|
|
121
|
+
def import_midi_to_clip(
|
|
122
|
+
ctx: Context,
|
|
123
|
+
file_path: str,
|
|
124
|
+
track_index: int,
|
|
125
|
+
clip_index: int,
|
|
126
|
+
create_clip: bool = True,
|
|
127
|
+
) -> dict:
|
|
128
|
+
"""Load a .mid file into a session clip.
|
|
129
|
+
|
|
130
|
+
Reads MIDI, converts timing to beats using session tempo, and writes
|
|
131
|
+
notes into the target clip slot. Creates the clip if needed.
|
|
132
|
+
"""
|
|
133
|
+
pretty_midi = _require_pretty_midi()
|
|
134
|
+
ableton = _get_ableton(ctx)
|
|
135
|
+
|
|
136
|
+
path = _validate_midi_path(file_path)
|
|
137
|
+
pm = pretty_midi.PrettyMIDI(str(path))
|
|
138
|
+
|
|
139
|
+
session = ableton.send_command("get_session_info", {})
|
|
140
|
+
tempo = float(session.get("tempo", 120.0))
|
|
141
|
+
|
|
142
|
+
notes_raw = []
|
|
143
|
+
for inst in pm.instruments:
|
|
144
|
+
for n in inst.notes:
|
|
145
|
+
start_beat = round(n.start * (tempo / 60.0), 3)
|
|
146
|
+
dur_beat = round((n.end - n.start) * (tempo / 60.0), 3)
|
|
147
|
+
notes_raw.append({
|
|
148
|
+
"pitch": n.pitch,
|
|
149
|
+
"start_time": start_beat,
|
|
150
|
+
"duration": max(dur_beat, 0.001),
|
|
151
|
+
"velocity": n.velocity,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
seen = set()
|
|
155
|
+
notes = []
|
|
156
|
+
for n in notes_raw:
|
|
157
|
+
key = (n["pitch"], round(n["start_time"], 3), round(n["duration"], 3))
|
|
158
|
+
if key not in seen:
|
|
159
|
+
seen.add(key)
|
|
160
|
+
notes.append(n)
|
|
161
|
+
|
|
162
|
+
notes = notes[:2000]
|
|
163
|
+
|
|
164
|
+
duration_beats = max((n["start_time"] + n["duration"] for n in notes),
|
|
165
|
+
default=4.0)
|
|
166
|
+
|
|
167
|
+
if create_clip:
|
|
168
|
+
ableton.send_command("create_clip", {
|
|
169
|
+
"track_index": track_index,
|
|
170
|
+
"clip_index": clip_index,
|
|
171
|
+
"length": round(duration_beats, 2),
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
if notes:
|
|
175
|
+
ableton.send_command("add_notes", {
|
|
176
|
+
"track_index": track_index,
|
|
177
|
+
"clip_index": clip_index,
|
|
178
|
+
"notes": notes,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"note_count": len(notes),
|
|
183
|
+
"duration_beats": round(duration_beats, 4),
|
|
184
|
+
"tempo_source": tempo,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# -- Tool 3: analyze_midi_file -----------------------------------------------
|
|
189
|
+
|
|
190
|
+
@mcp.tool()
|
|
191
|
+
def analyze_midi_file(
|
|
192
|
+
ctx: Context,
|
|
193
|
+
file_path: str,
|
|
194
|
+
) -> dict:
|
|
195
|
+
"""Analyze a .mid file — works offline, no Ableton needed.
|
|
196
|
+
|
|
197
|
+
Returns note count, duration, tempo, pitch range, instruments,
|
|
198
|
+
velocity stats, density curve, and estimated key.
|
|
199
|
+
"""
|
|
200
|
+
pretty_midi = _require_pretty_midi()
|
|
201
|
+
path = _validate_midi_path(file_path)
|
|
202
|
+
pm = pretty_midi.PrettyMIDI(str(path))
|
|
203
|
+
|
|
204
|
+
all_notes = []
|
|
205
|
+
for inst in pm.instruments:
|
|
206
|
+
for n in inst.notes:
|
|
207
|
+
all_notes.append(n)
|
|
208
|
+
|
|
209
|
+
if not all_notes:
|
|
210
|
+
return {
|
|
211
|
+
"note_count": 0,
|
|
212
|
+
"duration_seconds": round(pm.get_end_time(), 2),
|
|
213
|
+
"tempo_estimates": list(pm.get_tempo_changes()[1]),
|
|
214
|
+
"pitch_range": [0, 0],
|
|
215
|
+
"instruments": [i.name for i in pm.instruments],
|
|
216
|
+
"velocity_stats": {},
|
|
217
|
+
"density_curve": [],
|
|
218
|
+
"key_estimate": "unknown",
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
pitches = [n.pitch for n in all_notes]
|
|
222
|
+
velocities = [n.velocity for n in all_notes]
|
|
223
|
+
duration = pm.get_end_time()
|
|
224
|
+
|
|
225
|
+
density_curve = []
|
|
226
|
+
window = 1.0
|
|
227
|
+
t = 0.0
|
|
228
|
+
while t < duration:
|
|
229
|
+
count = sum(1 for n in all_notes if t <= n.start < t + window)
|
|
230
|
+
density_curve.append({
|
|
231
|
+
"time": round(t, 1),
|
|
232
|
+
"density": count / window,
|
|
233
|
+
})
|
|
234
|
+
t += window
|
|
235
|
+
|
|
236
|
+
notes_for_key = [
|
|
237
|
+
{"pitch": n.pitch, "duration": n.end - n.start}
|
|
238
|
+
for n in all_notes
|
|
239
|
+
]
|
|
240
|
+
key_result = theory.detect_key(notes_for_key)
|
|
241
|
+
key_str = f"{key_result['tonic_name']} {key_result['mode']}"
|
|
242
|
+
|
|
243
|
+
vel_stats = {
|
|
244
|
+
"mean": round(statistics.mean(velocities), 1),
|
|
245
|
+
"min": min(velocities),
|
|
246
|
+
"max": max(velocities),
|
|
247
|
+
"std": round(statistics.stdev(velocities), 1) if len(velocities) > 1 else 0.0,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"note_count": len(all_notes),
|
|
252
|
+
"duration_seconds": round(duration, 2),
|
|
253
|
+
"tempo_estimates": [round(t, 1) for t in pm.get_tempo_changes()[1]],
|
|
254
|
+
"pitch_range": [min(pitches), max(pitches)],
|
|
255
|
+
"instruments": [i.name for i in pm.instruments],
|
|
256
|
+
"velocity_stats": vel_stats,
|
|
257
|
+
"density_curve": density_curve,
|
|
258
|
+
"key_estimate": key_str,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# -- Tool 4: extract_piano_roll ----------------------------------------------
|
|
263
|
+
|
|
264
|
+
@mcp.tool()
|
|
265
|
+
def extract_piano_roll(
|
|
266
|
+
ctx: Context,
|
|
267
|
+
file_path: str,
|
|
268
|
+
resolution: float = 0.125,
|
|
269
|
+
) -> dict:
|
|
270
|
+
"""Extract a 2D piano roll matrix from a .mid file. Offline-capable.
|
|
271
|
+
|
|
272
|
+
Returns a velocity matrix [pitch_index][time_step] trimmed to
|
|
273
|
+
the actual pitch range. Resolution is in beats (0.125 = 32nd note).
|
|
274
|
+
"""
|
|
275
|
+
pretty_midi = _require_pretty_midi()
|
|
276
|
+
path = _validate_midi_path(file_path)
|
|
277
|
+
pm = pretty_midi.PrettyMIDI(str(path))
|
|
278
|
+
|
|
279
|
+
tempo_changes = pm.get_tempo_changes()
|
|
280
|
+
tempo = float(tempo_changes[1][0]) if len(tempo_changes[1]) > 0 else 120.0
|
|
281
|
+
fs = (tempo / 60.0) / resolution
|
|
282
|
+
|
|
283
|
+
roll = pm.get_piano_roll(fs=fs) # shape (128, T)
|
|
284
|
+
|
|
285
|
+
active_pitches = roll.sum(axis=1).nonzero()[0]
|
|
286
|
+
if len(active_pitches) == 0:
|
|
287
|
+
return {
|
|
288
|
+
"piano_roll": [],
|
|
289
|
+
"pitch_min": 0,
|
|
290
|
+
"pitch_max": 0,
|
|
291
|
+
"time_steps": 0,
|
|
292
|
+
"resolution": resolution,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
pitch_min = int(active_pitches[0])
|
|
296
|
+
pitch_max = int(active_pitches[-1])
|
|
297
|
+
trimmed = roll[pitch_min:pitch_max + 1, :]
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
"piano_roll": trimmed.astype(int).tolist(),
|
|
301
|
+
"pitch_min": pitch_min,
|
|
302
|
+
"pitch_max": pitch_max,
|
|
303
|
+
"time_steps": int(trimmed.shape[1]),
|
|
304
|
+
"resolution": resolution,
|
|
305
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "AI copilot for Ableton Live 12 —
|
|
5
|
+
"description": "AI copilot for Ableton Live 12 — 155 tools, device atlas (280+ devices), real-time audio analysis, generative algorithms, neo-Riemannian harmony, MIDI file I/O, and technique memory",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"type": "commonjs",
|
package/plugin/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AI copilot for Ableton Live 12 —
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "AI copilot for Ableton Live 12 — 155 tools, device atlas (280+ devices), real-time audio analysis, generative algorithms, neo-Riemannian harmony, MIDI file I/O, and technique memory",
|
|
5
5
|
"author": "Pilot Studio",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills/livepilot-core",
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: livepilot-core
|
|
3
|
-
description: Core discipline for controlling Ableton Live 12 through LivePilot's
|
|
3
|
+
description: Core discipline for controlling Ableton Live 12 through LivePilot's 155 MCP tools, device atlas (280+ devices), M4L analyzer (spectrum/RMS/key detection), automation intelligence (16 curve types, 15 recipes), music theory analysis, generative algorithms, neo-Riemannian harmony, MIDI file I/O, and technique memory. Use whenever working with Ableton Live through MCP tools.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# LivePilot Core — Ableton Live 12 AI Copilot
|
|
7
7
|
|
|
8
|
-
LivePilot is an agentic production system for Ableton Live 12. It combines
|
|
8
|
+
LivePilot is an agentic production system for Ableton Live 12. It combines 155 MCP tools with three layers of intelligence:
|
|
9
9
|
|
|
10
10
|
- **Device Atlas** — A structured knowledge corpus of 280+ instruments, 139 drum kits, and 350+ impulse responses. Consult the atlas before loading any device. It contains real browser URIs, preset names, and sonic descriptions. Never guess a device name — look it up.
|
|
11
11
|
- **M4L Analyzer** — Real-time audio analysis on the master bus (8-band spectrum, RMS/peak, key detection). Use it to verify mixing decisions, detect frequency problems, and find the key before writing harmonic content.
|
|
12
12
|
- **Technique Memory** — Persistent storage for production decisions. Consult `memory_recall` before creative tasks to understand the user's taste. Save techniques when the user likes something. The memory shapes future decisions without constraining them.
|
|
13
13
|
|
|
14
|
-
These layers sit on top of
|
|
14
|
+
These layers sit on top of 155 deterministic tools across 16 domains: transport, tracks, clips, MIDI notes, devices, scenes, mixing, browser, arrangement, technique memory, real-time DSP analysis, automation, music theory, generative algorithms, neo-Riemannian harmony, and MIDI file I/O.
|
|
15
15
|
|
|
16
16
|
## Golden Rules
|
|
17
17
|
|
|
@@ -32,7 +32,7 @@ These layers sit on top of 142 deterministic tools across 13 domains: transport,
|
|
|
32
32
|
Not all tools respond instantly. Know the tiers and act accordingly.
|
|
33
33
|
|
|
34
34
|
### Instant (<1s) — Use freely, no warning needed
|
|
35
|
-
All
|
|
35
|
+
All 155 core tools (transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, automation, theory, generative, harmony, midi_io) plus Layer A perception tools (spectral shape, timbral profile, mel spectrum, chroma, onsets, harmonic/percussive, novelty, momentary loudness). These are the reflex tools — call them anytime without hesitation.
|
|
36
36
|
|
|
37
37
|
### Fast (1-5s) — Use freely, barely noticeable
|
|
38
38
|
`analyze_loudness` · `analyze_dynamic_range` · `compare_loudness`
|
|
@@ -117,7 +117,7 @@ Never skip levels. The user's question determines the entry point, but always st
|
|
|
117
117
|
- MIDI track with no instrument loaded
|
|
118
118
|
- Notes programmed but clip not fired
|
|
119
119
|
|
|
120
|
-
## Tool Domains (
|
|
120
|
+
## Tool Domains (155 total)
|
|
121
121
|
|
|
122
122
|
### Transport (12)
|
|
123
123
|
`get_session_info` · `set_tempo` · `set_time_signature` · `start_playback` · `stop_playback` · `continue_playback` · `toggle_metronome` · `set_session_loop` · `undo` · `redo` · `get_recent_actions` · `get_session_diagnostics`
|
|
@@ -187,6 +187,44 @@ Music theory analysis — built-in pure Python engine, zero external dependencie
|
|
|
187
187
|
- Use your own musical knowledge alongside these tools — the engine provides data, you provide interpretation
|
|
188
188
|
- Processing time: 2-5s for generative tools (harmonize, countermelody)
|
|
189
189
|
|
|
190
|
+
### Generative (5)
|
|
191
|
+
Algorithmic composition tools — Euclidean rhythms, minimalist techniques.
|
|
192
|
+
|
|
193
|
+
**Tools:** `generate_euclidean_rhythm` · `layer_euclidean_rhythms` · `generate_tintinnabuli` · `generate_phase_shift` · `generate_additive_process`
|
|
194
|
+
|
|
195
|
+
**Key discipline:**
|
|
196
|
+
- All generative tools return note arrays — use `add_notes` to place them in clips
|
|
197
|
+
- `generate_euclidean_rhythm` uses the Bjorklund algorithm and identifies named rhythms (e.g., "tresillo", "cinquillo")
|
|
198
|
+
- `layer_euclidean_rhythms` stacks multiple patterns for polyrhythmic textures across tracks
|
|
199
|
+
- `generate_tintinnabuli` implements Arvo Pärt's technique: a T-voice (triad arpeggio) against a M-voice (melody)
|
|
200
|
+
- `generate_phase_shift` implements Steve Reich's phasing: two identical patterns drifting apart over time
|
|
201
|
+
- `generate_additive_process` implements Philip Glass's technique: melody expanded by adding one note per iteration
|
|
202
|
+
|
|
203
|
+
### Harmony (4)
|
|
204
|
+
Neo-Riemannian harmony tools — Tonnetz navigation, voice leading, chromatic mediants.
|
|
205
|
+
|
|
206
|
+
**Tools:** `navigate_tonnetz` · `find_voice_leading_path` · `classify_progression` · `suggest_chromatic_mediants`
|
|
207
|
+
|
|
208
|
+
**Key discipline:**
|
|
209
|
+
- These tools work with chord names and return harmonic relationships — no clip MIDI required
|
|
210
|
+
- `navigate_tonnetz` returns PRL (Parallel, Relative, Leading-tone) neighbors for any chord
|
|
211
|
+
- `find_voice_leading_path` finds the shortest harmonic path between two chords through Tonnetz space
|
|
212
|
+
- `classify_progression` identifies the neo-Riemannian transform pattern in a chord sequence
|
|
213
|
+
- `suggest_chromatic_mediants` returns all chromatic mediant relations with film score usage notes
|
|
214
|
+
- Opycleid library provides full Tonnetz; falls back to pure Python PRL if not installed
|
|
215
|
+
|
|
216
|
+
### MIDI I/O (4)
|
|
217
|
+
MIDI file import/export — works with standard .mid files on disk.
|
|
218
|
+
|
|
219
|
+
**Tools:** `export_clip_midi` · `import_midi_to_clip` · `analyze_midi_file` · `extract_piano_roll`
|
|
220
|
+
|
|
221
|
+
**Key discipline:**
|
|
222
|
+
- `export_clip_midi` exports a session clip's notes to a .mid file at the specified path
|
|
223
|
+
- `import_midi_to_clip` loads a .mid file into a clip, replacing existing notes
|
|
224
|
+
- `analyze_midi_file` performs offline analysis of any .mid file (tempo, notes, structure) — does not require Ableton connection
|
|
225
|
+
- `extract_piano_roll` returns a 2D velocity matrix (pitch × time) from a .mid file for visualization or processing
|
|
226
|
+
- Dependencies: midiutil (export), pretty-midi (import/analysis) — lazy-loaded, ~5 MB total
|
|
227
|
+
|
|
190
228
|
## Workflow: Building a Beat
|
|
191
229
|
|
|
192
230
|
1. `get_session_info` — check current state
|
|
@@ -328,7 +366,7 @@ Deep production knowledge lives in `references/`. Consult these when making crea
|
|
|
328
366
|
|
|
329
367
|
| File | What's inside | When to consult |
|
|
330
368
|
|------|--------------|-----------------|
|
|
331
|
-
| `references/overview.md` | All
|
|
369
|
+
| `references/overview.md` | All 155 tools mapped with params, units, ranges | Quick lookup for any tool |
|
|
332
370
|
| `references/midi-recipes.md` | Drum patterns by genre, chord voicings, scales, hi-hat techniques, humanization, polymetrics | Programming MIDI notes, building beats |
|
|
333
371
|
| `references/sound-design.md` | Stock instruments/effects, parameter recipes for bass/pad/lead/pluck, device chain patterns | Loading and configuring devices |
|
|
334
372
|
| `references/mixing-patterns.md` | Gain staging, parallel compression, sidechain, EQ by instrument, bus processing, stereo width | Setting volumes, panning, adding effects |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# LivePilot v1.
|
|
1
|
+
# LivePilot v1.7.0 — Architecture & Tool Reference
|
|
2
2
|
|
|
3
|
-
LivePilot is an agentic production system for Ableton Live 12. It combines
|
|
3
|
+
LivePilot is an agentic production system for Ableton Live 12. It combines 155 MCP tools with a device knowledge corpus, real-time audio analysis, automation intelligence, generative algorithms, neo-Riemannian harmony, MIDI file I/O, and persistent technique memory.
|
|
4
4
|
|
|
5
5
|
## Architecture
|
|
6
6
|
|
|
@@ -32,7 +32,7 @@ A flat tool list lets the AI press buttons. LivePilot's three layers give it con
|
|
|
32
32
|
|
|
33
33
|
This turns "set EQ band 3 to -4 dB" into "cut 400 Hz by 4 dB, then read the spectrum to confirm the mud is actually reduced."
|
|
34
34
|
|
|
35
|
-
## The
|
|
35
|
+
## The 155 Tools — What Each One Does
|
|
36
36
|
|
|
37
37
|
### Transport (12) — Playback, tempo, global state, diagnostics
|
|
38
38
|
|
|
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
|
|
|
5
5
|
when this script is selected in Preferences > Link, Tempo & MIDI.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.
|
|
8
|
+
__version__ = "1.7.0"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from .server import LivePilotServer
|
|
@@ -34,7 +34,7 @@ class LivePilot(ControlSurface):
|
|
|
34
34
|
ControlSurface.__init__(self, c_instance)
|
|
35
35
|
self._server = LivePilotServer(self)
|
|
36
36
|
self._server.start()
|
|
37
|
-
self.log_message("LivePilot v1.
|
|
37
|
+
self.log_message("LivePilot v1.7.0 initialized")
|
|
38
38
|
self.show_message("LivePilot: Listening on port 9878")
|
|
39
39
|
|
|
40
40
|
def disconnect(self):
|