livepilot 1.16.1 → 1.17.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 +311 -0
- package/README.md +16 -15
- package/installer/codex.js +14 -0
- 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/atlas/__init__.py +85 -0
- package/mcp_server/atlas/device_atlas.json +3183 -382
- package/mcp_server/atlas/device_techniques_index.json +1510 -0
- package/mcp_server/atlas/enrichments/__init__.py +1 -0
- package/mcp_server/atlas/enrichments/audio_effects/amp.yaml +112 -0
- package/mcp_server/atlas/enrichments/audio_effects/audio_effect_rack.yaml +77 -0
- package/mcp_server/atlas/enrichments/audio_effects/cabinet.yaml +81 -0
- package/mcp_server/atlas/enrichments/audio_effects/corpus.yaml +128 -0
- package/mcp_server/atlas/enrichments/audio_effects/envelope_follower.yaml +99 -0
- package/mcp_server/atlas/enrichments/audio_effects/external_audio_effect.yaml +64 -0
- package/mcp_server/atlas/enrichments/audio_effects/looper.yaml +85 -0
- package/mcp_server/atlas/enrichments/audio_effects/resonators.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/snipper.yaml +17 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectrum.yaml +61 -0
- package/mcp_server/atlas/enrichments/audio_effects/tuner.yaml +43 -0
- package/mcp_server/atlas/enrichments/audio_effects/utility.yaml +118 -0
- package/mcp_server/atlas/enrichments/audio_effects/vocoder.yaml +94 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/bell_tower.yaml +38 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/drum_rack.yaml +142 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +12 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +15 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +11 -0
- package/mcp_server/atlas/enrichments/midi_effects/filler.yaml +17 -0
- package/mcp_server/atlas/enrichments/utility/performer.yaml +15 -0
- package/mcp_server/atlas/enrichments/utility/vector_map.yaml +21 -0
- package/mcp_server/atlas/tools.py +291 -0
- package/mcp_server/m4l_bridge.py +19 -2
- package/mcp_server/sample_engine/tools.py +201 -128
- package/mcp_server/splice_client/http_bridge.py +319 -116
- package/mcp_server/tools/automation.py +168 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +216 -1
- package/server.json +3 -3
|
@@ -149,6 +149,297 @@ def atlas_compare(ctx: Context, device_a: str, device_b: str, role: str = "") ->
|
|
|
149
149
|
return atlas.compare(device_a, device_b, role=role)
|
|
150
150
|
|
|
151
151
|
|
|
152
|
+
@mcp.tool()
|
|
153
|
+
def atlas_describe_chain(
|
|
154
|
+
ctx: Context,
|
|
155
|
+
description: str,
|
|
156
|
+
genre: str = "",
|
|
157
|
+
limit_per_role: int = 3,
|
|
158
|
+
) -> dict:
|
|
159
|
+
"""Free-text describe-a-chain: "a granular pad that sounds like Tim Hecker"
|
|
160
|
+
→ device chain proposal.
|
|
161
|
+
|
|
162
|
+
The mirror of `splice_describe_sound` for the device library. Where
|
|
163
|
+
`atlas_chain_suggest(role, genre)` takes structured inputs, this takes
|
|
164
|
+
a free-form sentence and proposes a chain by:
|
|
165
|
+
|
|
166
|
+
1. Parsing role hints from the description ("bass", "pad", "lead",
|
|
167
|
+
"percussion", "drum", "texture", "vocal", "keys")
|
|
168
|
+
2. Parsing aesthetic hints (artist names → `artist-vocabularies.md`,
|
|
169
|
+
genre names → `genre-vocabularies.md`, character words → atlas tags)
|
|
170
|
+
3. Searching the atlas with those terms
|
|
171
|
+
4. Proposing the top devices per role with brief rationale
|
|
172
|
+
|
|
173
|
+
This does NOT autoload anything — it returns a proposal the caller can
|
|
174
|
+
review, adjust, then execute with `load_browser_item` + a chain of FX.
|
|
175
|
+
|
|
176
|
+
description: free text. Examples:
|
|
177
|
+
"a granular pad that sounds like Tim Hecker"
|
|
178
|
+
"warm analog bass for minimal techno, deep and dubby"
|
|
179
|
+
"chopped vocal melody, Akufen-style microhouse"
|
|
180
|
+
"brittle mallet percussion with long reverb, Stars of the Lid territory"
|
|
181
|
+
genre: optional genre bias if the description is genre-agnostic
|
|
182
|
+
limit_per_role: max devices to suggest per detected role (default 3)
|
|
183
|
+
|
|
184
|
+
Returns {description, detected_roles, detected_aesthetic,
|
|
185
|
+
per_role_suggestions: [...], chain_proposal: [...]}.
|
|
186
|
+
"""
|
|
187
|
+
atlas = _get_atlas()
|
|
188
|
+
if atlas is None:
|
|
189
|
+
return {"error": "Atlas not loaded. Run scan_full_library first."}
|
|
190
|
+
if not description or not description.strip():
|
|
191
|
+
return {"error": "description is required"}
|
|
192
|
+
|
|
193
|
+
desc_lower = description.lower().strip()
|
|
194
|
+
|
|
195
|
+
# ── Detect roles ──────────────────────────────────────────────
|
|
196
|
+
ROLE_KEYWORDS = {
|
|
197
|
+
"bass": ["bass", "sub", "808", "low end", "bottom"],
|
|
198
|
+
"lead": ["lead", "melody", "topline", "hook"],
|
|
199
|
+
"pad": ["pad", "texture", "atmosphere", "atmos", "drone", "ambient"],
|
|
200
|
+
"keys": ["keys", "piano", "rhodes", "wurli", "wurly", "chord"],
|
|
201
|
+
"percussion": ["percussion", "perc", "shaker", "conga", "claves", "tambourine"],
|
|
202
|
+
"drums": ["drums", "drum kit", "kick", "snare", "hat", "hi-hat", "hihat", "break"],
|
|
203
|
+
"vocal": ["vocal", "vox", "voice", "chop", "chant"],
|
|
204
|
+
"fx": ["fx", "riser", "downlifter", "sweep", "whoosh", "impact"],
|
|
205
|
+
}
|
|
206
|
+
detected_roles = []
|
|
207
|
+
for role, keywords in ROLE_KEYWORDS.items():
|
|
208
|
+
if any(k in desc_lower for k in keywords):
|
|
209
|
+
detected_roles.append(role)
|
|
210
|
+
if not detected_roles:
|
|
211
|
+
detected_roles = ["pad"] # sensible default
|
|
212
|
+
|
|
213
|
+
# ── Detect aesthetic / artist cues ────────────────────────────
|
|
214
|
+
ARTIST_TO_TAGS = {
|
|
215
|
+
"villalobos": ["minimal_techno", "deep_minimal"],
|
|
216
|
+
"hawtin": ["minimal_techno", "deep_minimal"],
|
|
217
|
+
"plastikman": ["minimal_techno"],
|
|
218
|
+
"basic channel": ["dub_techno", "dub"],
|
|
219
|
+
"rhythm and sound": ["dub_techno", "dub"],
|
|
220
|
+
"voigt": ["ambient", "dub_techno"],
|
|
221
|
+
"gas": ["ambient"],
|
|
222
|
+
"basinski": ["ambient", "drone"],
|
|
223
|
+
"stars of the lid": ["ambient", "drone", "modern_classical"],
|
|
224
|
+
"hecker": ["ambient", "drone", "experimental"],
|
|
225
|
+
"aphex": ["idm", "experimental"],
|
|
226
|
+
"autechre": ["idm", "experimental"],
|
|
227
|
+
"dilla": ["hip_hop", "lo_fi"],
|
|
228
|
+
"burial": ["dubstep", "uk_garage", "ambient"],
|
|
229
|
+
"akufen": ["microhouse"],
|
|
230
|
+
"isolee": ["microhouse", "deep_house"],
|
|
231
|
+
"henke": ["minimal_techno", "experimental"],
|
|
232
|
+
"monolake": ["minimal_techno", "experimental"],
|
|
233
|
+
"tycho": ["synthwave", "electronica"],
|
|
234
|
+
"boards of canada": ["downtempo", "lo_fi"],
|
|
235
|
+
}
|
|
236
|
+
CHARACTER_TAGS = [
|
|
237
|
+
"warm", "cold", "bright", "dark", "lush", "thin", "fat", "metallic",
|
|
238
|
+
"granular", "glitch", "gritty", "clean", "wet", "dry", "resonant",
|
|
239
|
+
"breathy", "analog", "digital", "vintage", "modern", "organic", "synthetic",
|
|
240
|
+
]
|
|
241
|
+
GENRE_KEYWORDS = [
|
|
242
|
+
"microhouse", "minimal", "techno", "house", "deep house", "ambient",
|
|
243
|
+
"drone", "idm", "experimental", "dubstep", "dnb", "drum and bass",
|
|
244
|
+
"hip hop", "hip-hop", "lo-fi", "lo fi", "lofi", "trap", "garage",
|
|
245
|
+
"dub techno", "dub", "jazz", "classical", "cinematic", "synthwave",
|
|
246
|
+
"vaporwave", "ambient techno", "deep minimal",
|
|
247
|
+
]
|
|
248
|
+
detected_aesthetic = []
|
|
249
|
+
for artist, tags in ARTIST_TO_TAGS.items():
|
|
250
|
+
if artist in desc_lower:
|
|
251
|
+
detected_aesthetic.extend(tags)
|
|
252
|
+
for tag in CHARACTER_TAGS:
|
|
253
|
+
if f" {tag}" in f" {desc_lower}":
|
|
254
|
+
detected_aesthetic.append(tag)
|
|
255
|
+
for g in GENRE_KEYWORDS:
|
|
256
|
+
if g in desc_lower:
|
|
257
|
+
detected_aesthetic.append(g.replace(" ", "_").replace("-", "_"))
|
|
258
|
+
if genre:
|
|
259
|
+
detected_aesthetic.append(genre.lower())
|
|
260
|
+
# Dedupe preserving order
|
|
261
|
+
seen = set()
|
|
262
|
+
detected_aesthetic = [
|
|
263
|
+
t for t in detected_aesthetic
|
|
264
|
+
if not (t in seen or seen.add(t))
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
# ── Build per-role suggestions via atlas.suggest ─────────────
|
|
268
|
+
per_role_suggestions = []
|
|
269
|
+
for role in detected_roles:
|
|
270
|
+
# Build an intent string that combines role + aesthetic cues
|
|
271
|
+
intent_parts = [role]
|
|
272
|
+
intent_parts.extend(detected_aesthetic[:3]) # top 3 aesthetic tags
|
|
273
|
+
intent = " ".join(intent_parts)
|
|
274
|
+
results = atlas.suggest(
|
|
275
|
+
intent=intent,
|
|
276
|
+
genre=(detected_aesthetic[0] if detected_aesthetic else genre),
|
|
277
|
+
energy="medium",
|
|
278
|
+
limit=int(limit_per_role),
|
|
279
|
+
)
|
|
280
|
+
per_role_suggestions.append({
|
|
281
|
+
"role": role,
|
|
282
|
+
"intent_used": intent,
|
|
283
|
+
"suggestions": [
|
|
284
|
+
{
|
|
285
|
+
"device_id": r["device"].get("id", ""),
|
|
286
|
+
"device_name": r["device"].get("name", ""),
|
|
287
|
+
"uri": r["device"].get("uri", ""),
|
|
288
|
+
"rationale": r.get("rationale", ""),
|
|
289
|
+
"recipe": r.get("recipe"),
|
|
290
|
+
}
|
|
291
|
+
for r in results
|
|
292
|
+
],
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
# ── Propose a simple chain from the highest-ranked suggestions ─
|
|
296
|
+
chain_proposal = []
|
|
297
|
+
position = 0
|
|
298
|
+
for role_block in per_role_suggestions:
|
|
299
|
+
if not role_block["suggestions"]:
|
|
300
|
+
continue
|
|
301
|
+
top = role_block["suggestions"][0]
|
|
302
|
+
chain_proposal.append({
|
|
303
|
+
"position": position,
|
|
304
|
+
"role": role_block["role"],
|
|
305
|
+
"device_name": top["device_name"],
|
|
306
|
+
"device_id": top["device_id"],
|
|
307
|
+
"uri": top["uri"],
|
|
308
|
+
"why": top["rationale"],
|
|
309
|
+
})
|
|
310
|
+
position += 1
|
|
311
|
+
|
|
312
|
+
# ── Cross-reference aesthetic to the vocabulary files ──────────
|
|
313
|
+
next_steps = []
|
|
314
|
+
if any("villalobos" in desc_lower or a in detected_aesthetic for a in
|
|
315
|
+
("microhouse", "deep_minimal", "minimal_techno", "dub_techno",
|
|
316
|
+
"ambient", "drone", "idm", "experimental")):
|
|
317
|
+
next_steps.append(
|
|
318
|
+
"Cross-reference "
|
|
319
|
+
"`livepilot/skills/livepilot-core/references/artist-vocabularies.md` "
|
|
320
|
+
"and `genre-vocabularies.md` for deeper aesthetic guidance."
|
|
321
|
+
)
|
|
322
|
+
if not detected_aesthetic:
|
|
323
|
+
next_steps.append(
|
|
324
|
+
"No aesthetic or genre cues detected. If the description "
|
|
325
|
+
"should have matched, add it to the ARTIST_TO_TAGS map or "
|
|
326
|
+
"provide genre= explicitly."
|
|
327
|
+
)
|
|
328
|
+
next_steps.append(
|
|
329
|
+
"Call `atlas_techniques_for_device(device_id)` on any proposal "
|
|
330
|
+
"to see what techniques reference it."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
"description": description,
|
|
335
|
+
"detected_roles": detected_roles,
|
|
336
|
+
"detected_aesthetic": detected_aesthetic,
|
|
337
|
+
"per_role_suggestions": per_role_suggestions,
|
|
338
|
+
"chain_proposal": chain_proposal,
|
|
339
|
+
"next_steps": next_steps,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@mcp.tool()
|
|
344
|
+
def atlas_techniques_for_device(ctx: Context, device_id: str) -> dict:
|
|
345
|
+
"""Reverse-lookup: what techniques / principles reference this device?
|
|
346
|
+
|
|
347
|
+
Answers questions like "what can I do with Granulator III?" by returning
|
|
348
|
+
every technique across the knowledge base that mentions this device —
|
|
349
|
+
the device's own `signature_techniques`, sample-manipulation principles
|
|
350
|
+
that use it, sound-design-deep.md references. Complements
|
|
351
|
+
`atlas_device_info` (which returns the device's own curated fields) by
|
|
352
|
+
showing the device's OUTWARD connections — how it fits into techniques
|
|
353
|
+
that weren't written from the device's perspective.
|
|
354
|
+
|
|
355
|
+
device_id: atlas ID (e.g. "granulator_iii", "simpler", "analog"). Use
|
|
356
|
+
`atlas_search` or `atlas_device_info` to discover IDs.
|
|
357
|
+
|
|
358
|
+
Returns {device_id, technique_count, techniques: [...]}, where each
|
|
359
|
+
technique entry has:
|
|
360
|
+
- technique: short name (e.g. "Vocal micro-chop (Akufen)")
|
|
361
|
+
- description: one-line
|
|
362
|
+
- aesthetic: list of aesthetic/genre tags
|
|
363
|
+
- source: where this technique lives (`atlas/<id>`,
|
|
364
|
+
`sample-techniques.md`, `sound-design-deep.md`)
|
|
365
|
+
- kind: signature_technique | sample_technique | sound_design_principle
|
|
366
|
+
|
|
367
|
+
Index is auto-generated from the knowledge base; regenerate via the
|
|
368
|
+
companion script when adding new techniques (rare — most additions
|
|
369
|
+
happen through enrichment YAMLs, which the index reads directly).
|
|
370
|
+
"""
|
|
371
|
+
import json, os
|
|
372
|
+
index_path = os.path.join(
|
|
373
|
+
os.path.dirname(os.path.abspath(__file__)),
|
|
374
|
+
"device_techniques_index.json",
|
|
375
|
+
)
|
|
376
|
+
if not os.path.isfile(index_path):
|
|
377
|
+
return {
|
|
378
|
+
"error": "device_techniques_index.json not found",
|
|
379
|
+
"hint": "regenerate via the post-v1.17 reverse-index builder script",
|
|
380
|
+
}
|
|
381
|
+
try:
|
|
382
|
+
with open(index_path, "r") as f:
|
|
383
|
+
data = json.load(f)
|
|
384
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
385
|
+
return {"error": f"Failed to load index: {exc}"}
|
|
386
|
+
|
|
387
|
+
if not device_id:
|
|
388
|
+
# Return a summary of indexed devices
|
|
389
|
+
devices = data.get("devices", {})
|
|
390
|
+
return {
|
|
391
|
+
"indexed_device_count": len(devices),
|
|
392
|
+
"total_cross_references": data.get("entry_count", 0),
|
|
393
|
+
"devices": sorted(devices.keys()),
|
|
394
|
+
"hint": "Pass a device_id for per-device techniques",
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
entries = data.get("devices", {}).get(device_id)
|
|
398
|
+
if entries is None:
|
|
399
|
+
return {
|
|
400
|
+
"device_id": device_id,
|
|
401
|
+
"technique_count": 0,
|
|
402
|
+
"techniques": [],
|
|
403
|
+
"hint": (
|
|
404
|
+
"No techniques indexed for this device. Try a different ID "
|
|
405
|
+
"or use `atlas_search` to find the correct one. Devices "
|
|
406
|
+
"with no cross-references either haven't been enriched yet "
|
|
407
|
+
"or aren't referenced in any technique doc."
|
|
408
|
+
),
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
"device_id": device_id,
|
|
413
|
+
"technique_count": len(entries),
|
|
414
|
+
"techniques": entries,
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@mcp.tool()
|
|
419
|
+
def atlas_pack_info(ctx: Context, pack_name: str = "") -> dict:
|
|
420
|
+
"""Inspect a single Ableton pack — device list + enrichment coverage.
|
|
421
|
+
|
|
422
|
+
pack_name: the pack name (e.g., "Drone Lab", "Core Library",
|
|
423
|
+
"Creative Extensions", "Inspired by Nature"). Case-insensitive.
|
|
424
|
+
Pass an empty string to get the full list of packs known to
|
|
425
|
+
the atlas with device counts.
|
|
426
|
+
|
|
427
|
+
Returns {pack, device_count, enriched_count, devices[...]} for a
|
|
428
|
+
specific pack, or {packs: [...]} when called with no name.
|
|
429
|
+
|
|
430
|
+
Use this to answer questions like "what's in Drone Lab?" or "how
|
|
431
|
+
much of Creative Extensions do we have aesthetic knowledge about?"
|
|
432
|
+
"""
|
|
433
|
+
atlas = _get_atlas()
|
|
434
|
+
if atlas is None:
|
|
435
|
+
return {"error": "Atlas not loaded. Run scan_full_library first."}
|
|
436
|
+
|
|
437
|
+
if not pack_name:
|
|
438
|
+
return {"packs": atlas.list_packs()}
|
|
439
|
+
|
|
440
|
+
return atlas.pack_info(pack_name)
|
|
441
|
+
|
|
442
|
+
|
|
152
443
|
@mcp.tool()
|
|
153
444
|
def scan_full_library(
|
|
154
445
|
ctx: Context,
|
package/mcp_server/m4l_bridge.py
CHANGED
|
@@ -479,7 +479,16 @@ class SpectralReceiver(asyncio.DatagramProtocol):
|
|
|
479
479
|
/response_chunk i i s — chunked response (index, total, data)
|
|
480
480
|
"""
|
|
481
481
|
|
|
482
|
-
|
|
482
|
+
# Band names keyed by how many bands the .amxd emits. 8 bands is the v1.x
|
|
483
|
+
# layout (sub starts at 20 Hz, ~octave per band). 9 bands is v1.16.x+
|
|
484
|
+
# with an explicit sub_low (20-60 Hz) split off so Villalobos-style kicks
|
|
485
|
+
# at 40-50 Hz are no longer hidden inside the sub band. The .amxd is the
|
|
486
|
+
# source of truth for band count — this server picks the right names
|
|
487
|
+
# based on how many floats actually arrive on /spectrum.
|
|
488
|
+
BAND_NAMES_8 = ["sub", "low", "low_mid", "mid", "high_mid", "high", "presence", "air"]
|
|
489
|
+
BAND_NAMES_9 = ["sub_low", "sub", "low", "low_mid", "mid", "high_mid", "high", "presence", "air"]
|
|
490
|
+
# Default alias kept for any external reader.
|
|
491
|
+
BAND_NAMES = BAND_NAMES_9
|
|
483
492
|
|
|
484
493
|
def __init__(self, cache: SpectralCache, miditool_cache: Optional["MidiToolCache"] = None):
|
|
485
494
|
self.cache = cache
|
|
@@ -571,8 +580,16 @@ class SpectralReceiver(asyncio.DatagramProtocol):
|
|
|
571
580
|
|
|
572
581
|
def _handle_message(self, address: str, args: list) -> None:
|
|
573
582
|
if address == "/spectrum" and len(args) >= 8:
|
|
583
|
+
# Pick the right name set based on how many bands the .amxd emits.
|
|
584
|
+
# 9-band payloads come from v1.16.x+ devices with the sub_low split.
|
|
585
|
+
# 8-band payloads come from older frozen .amxd builds — we keep
|
|
586
|
+
# working against them until every user has re-frozen.
|
|
587
|
+
if len(args) >= 9:
|
|
588
|
+
names = self.BAND_NAMES_9
|
|
589
|
+
else:
|
|
590
|
+
names = self.BAND_NAMES_8
|
|
574
591
|
bands = {}
|
|
575
|
-
for i, name in enumerate(
|
|
592
|
+
for i, name in enumerate(names):
|
|
576
593
|
if i < len(args):
|
|
577
594
|
bands[name] = round(float(args[i]), 4)
|
|
578
595
|
self.cache.update("spectrum", bands)
|