livepilot 1.16.0 → 1.16.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 +75 -5
- package/README.md +11 -11
- 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/enrichments/audio_effects/pitch_hack.yaml +61 -0
- package/mcp_server/atlas/enrichments/audio_effects/pitchloop89.yaml +111 -0
- package/mcp_server/atlas/enrichments/audio_effects/re_enveloper.yaml +51 -0
- package/mcp_server/atlas/enrichments/audio_effects/snipper.yaml +36 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_blur.yaml +64 -0
- package/mcp_server/atlas/enrichments/instruments/bell_tower.yaml +37 -0
- package/mcp_server/atlas/enrichments/instruments/granulator_iii.yaml +124 -0
- package/mcp_server/atlas/enrichments/instruments/harmonic_drone_generator.yaml +83 -0
- package/mcp_server/atlas/enrichments/instruments/impulse.yaml +47 -0
- package/mcp_server/atlas/enrichments/instruments/sting_iftah.yaml +44 -0
- package/mcp_server/atlas/enrichments/midi_effects/expressive_chords.yaml +38 -0
- package/mcp_server/atlas/enrichments/midi_effects/filler.yaml +32 -0
- package/mcp_server/atlas/enrichments/midi_effects/microtuner.yaml +83 -0
- package/mcp_server/atlas/enrichments/midi_effects/patterns_iftah.yaml +38 -0
- package/mcp_server/atlas/enrichments/midi_effects/phase_pattern.yaml +51 -0
- package/mcp_server/atlas/enrichments/midi_effects/polyrhythm.yaml +46 -0
- package/mcp_server/atlas/enrichments/midi_effects/retrigger.yaml +40 -0
- package/mcp_server/atlas/enrichments/midi_effects/slice_shuffler.yaml +39 -0
- package/mcp_server/atlas/enrichments/midi_effects/sq_sequencer.yaml +39 -0
- package/mcp_server/atlas/enrichments/midi_effects/stages.yaml +38 -0
- package/mcp_server/atlas/enrichments/utility/arrangement_looper.yaml +31 -0
- package/mcp_server/atlas/enrichments/utility/cv_clock_in.yaml +25 -0
- package/mcp_server/atlas/enrichments/utility/cv_clock_out.yaml +25 -0
- package/mcp_server/atlas/enrichments/utility/cv_envelope_follower.yaml +38 -0
- package/mcp_server/atlas/enrichments/utility/cv_in.yaml +26 -0
- package/mcp_server/atlas/enrichments/utility/cv_instrument.yaml +34 -0
- package/mcp_server/atlas/enrichments/utility/cv_lfo.yaml +38 -0
- package/mcp_server/atlas/enrichments/utility/cv_shaper.yaml +35 -0
- package/mcp_server/atlas/enrichments/utility/cv_triggers.yaml +26 -0
- package/mcp_server/atlas/enrichments/utility/cv_utility.yaml +37 -0
- package/mcp_server/atlas/enrichments/utility/performer.yaml +36 -0
- package/mcp_server/atlas/enrichments/utility/prearranger.yaml +36 -0
- package/mcp_server/atlas/enrichments/utility/rotating_rhythm_generator.yaml +35 -0
- package/mcp_server/atlas/enrichments/utility/surround_panner.yaml +40 -0
- package/mcp_server/atlas/enrichments/utility/variations.yaml +40 -0
- package/mcp_server/atlas/enrichments/utility/vector_map.yaml +36 -0
- package/mcp_server/sample_engine/tools.py +50 -4
- package/mcp_server/server.py +18 -6
- package/mcp_server/splice_client/client.py +90 -18
- package/mcp_server/splice_client/http_bridge.py +101 -28
- package/mcp_server/splice_client/models.py +12 -0
- package/mcp_server/tools/analyzer.py +150 -1
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +3 -3
|
@@ -212,6 +212,7 @@ async def get_master_spectrum(
|
|
|
212
212
|
ctx: Context,
|
|
213
213
|
window_ms: int = 0,
|
|
214
214
|
samples: int = 0,
|
|
215
|
+
sub_detail: bool = False,
|
|
215
216
|
) -> dict:
|
|
216
217
|
"""Get 8-band frequency analysis of the master bus.
|
|
217
218
|
|
|
@@ -236,6 +237,14 @@ async def get_master_spectrum(
|
|
|
236
237
|
The sampled bands are also returned as `bands_min`, `bands_max` and
|
|
237
238
|
`bands_std` so callers can see variance within the window — useful
|
|
238
239
|
for detecting transient-heavy content vs. sustained material.
|
|
240
|
+
|
|
241
|
+
BUG-2026-04-22#15 fix — sub-band resolution:
|
|
242
|
+
Pass `sub_detail=True` to attach a `sub_detail` dict with three
|
|
243
|
+
finer buckets: `sub_deep` (20-45 Hz), `sub_mid` (45-60 Hz),
|
|
244
|
+
`sub_high` (60-80 Hz). Derived from the FluCoMa mel spectrum
|
|
245
|
+
(40 bands) rather than the 8-band cache, so it requires FluCoMa
|
|
246
|
+
to be active. When FluCoMa is unavailable, sub_detail is omitted
|
|
247
|
+
with a `sub_detail_warning` field explaining why.
|
|
239
248
|
"""
|
|
240
249
|
cache = _get_spectral(ctx)
|
|
241
250
|
_require_analyzer(cache)
|
|
@@ -291,6 +300,8 @@ async def get_master_spectrum(
|
|
|
291
300
|
key_data = cache.get("key")
|
|
292
301
|
if key_data:
|
|
293
302
|
result["detected_key"] = key_data["value"]
|
|
303
|
+
if sub_detail:
|
|
304
|
+
_attach_sub_detail(cache, result)
|
|
294
305
|
return result
|
|
295
306
|
|
|
296
307
|
# Legacy instantaneous path
|
|
@@ -303,10 +314,55 @@ async def get_master_spectrum(
|
|
|
303
314
|
key_data = cache.get("key")
|
|
304
315
|
if key_data:
|
|
305
316
|
result["detected_key"] = key_data["value"]
|
|
317
|
+
if sub_detail:
|
|
318
|
+
_attach_sub_detail(cache, result)
|
|
306
319
|
|
|
307
320
|
return result
|
|
308
321
|
|
|
309
322
|
|
|
323
|
+
def _attach_sub_detail(cache, result: dict) -> None:
|
|
324
|
+
"""Compute finer sub-band breakdown (20-45, 45-60, 60-80 Hz) from
|
|
325
|
+
FluCoMa's 40-band mel spectrum and attach to the result dict.
|
|
326
|
+
|
|
327
|
+
Mel band edges are perceptual, not linear Hz — we map approximately:
|
|
328
|
+
with a standard 40-band mel filterbank from 0-20kHz, the first
|
|
329
|
+
~4 bands cover 0-80 Hz and are distributed roughly:
|
|
330
|
+
band 0: ~0-25 Hz
|
|
331
|
+
band 1: ~25-45 Hz
|
|
332
|
+
band 2: ~45-65 Hz
|
|
333
|
+
band 3: ~65-90 Hz
|
|
334
|
+
We use these mappings as approximations; exact cutoffs depend on
|
|
335
|
+
FluCoMa's filterbank config but this is tight enough for mixing
|
|
336
|
+
decisions (the question is "is energy in the 30 Hz or 60 Hz range?").
|
|
337
|
+
"""
|
|
338
|
+
mel_snap = cache.get("mel_bands")
|
|
339
|
+
if not mel_snap or not mel_snap.get("value"):
|
|
340
|
+
result["sub_detail_warning"] = (
|
|
341
|
+
"FluCoMa mel spectrum not available — sub_detail requires "
|
|
342
|
+
"FluCoMa active on the M4L analyzer. Use check_flucoma to diagnose."
|
|
343
|
+
)
|
|
344
|
+
return
|
|
345
|
+
mel_bands = mel_snap["value"]
|
|
346
|
+
if not isinstance(mel_bands, list) or len(mel_bands) < 4:
|
|
347
|
+
result["sub_detail_warning"] = (
|
|
348
|
+
f"Mel spectrum has {len(mel_bands) if isinstance(mel_bands, list) else 0} "
|
|
349
|
+
"bands — need at least 4 for sub_detail decomposition."
|
|
350
|
+
)
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
def _mean(indices):
|
|
354
|
+
vals = [float(mel_bands[i]) for i in indices if i < len(mel_bands)]
|
|
355
|
+
return round(sum(vals) / len(vals), 4) if vals else 0.0
|
|
356
|
+
|
|
357
|
+
result["sub_detail"] = {
|
|
358
|
+
"sub_deep": _mean([0, 1]), # ~0-45 Hz (kick fundamental)
|
|
359
|
+
"sub_mid": _mean([2]), # ~45-60 Hz (808 body / kick upper)
|
|
360
|
+
"sub_high": _mean([3]), # ~60-80 Hz (bass guitar low, sub-bass crossover)
|
|
361
|
+
"age_ms": mel_snap.get("age_ms"),
|
|
362
|
+
"source": "flucoma_mel_40",
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
310
366
|
def _sanitize_pitch(pitch: Optional[dict]) -> Optional[dict]:
|
|
311
367
|
"""Validate a pitch reading from the M4L analyzer (BUG-F1).
|
|
312
368
|
|
|
@@ -751,7 +807,10 @@ async def add_drum_rack_pad(
|
|
|
751
807
|
"method": "native_12_4",
|
|
752
808
|
}
|
|
753
809
|
"""
|
|
754
|
-
|
|
810
|
+
# _simpler_post_load_hygiene is already imported at module scope
|
|
811
|
+
# (line 29). Do not re-import inline — the earlier inline form used
|
|
812
|
+
# the wrong relative path (..; should've been .) and crashed at
|
|
813
|
+
# runtime with "No module named 'mcp_server._analyzer_engine'".
|
|
755
814
|
ableton = ctx.lifespan_context["ableton"]
|
|
756
815
|
caps = _live_caps(ctx)
|
|
757
816
|
if not caps.has_replace_sample_native:
|
|
@@ -1534,6 +1593,96 @@ async def verify_device_health(
|
|
|
1534
1593
|
}
|
|
1535
1594
|
|
|
1536
1595
|
|
|
1596
|
+
@mcp.tool()
|
|
1597
|
+
async def verify_all_devices_health(
|
|
1598
|
+
ctx: Context,
|
|
1599
|
+
test_midi_note: int = 60,
|
|
1600
|
+
test_velocity: int = 100,
|
|
1601
|
+
test_duration_ms: int = 250,
|
|
1602
|
+
threshold: float = 0.005,
|
|
1603
|
+
skip_audio_tracks: bool = True,
|
|
1604
|
+
skip_empty_tracks: bool = True,
|
|
1605
|
+
) -> dict:
|
|
1606
|
+
"""Run verify_device_health across every eligible track in one call.
|
|
1607
|
+
|
|
1608
|
+
Session-wide silent-track detector. Useful right after opening a
|
|
1609
|
+
project to surface dead plugins before mixing. Serial execution —
|
|
1610
|
+
firing notes in parallel would make the meter readings ambiguous.
|
|
1611
|
+
|
|
1612
|
+
skip_audio_tracks: audio tracks have no MIDI input, skip them (default True)
|
|
1613
|
+
skip_empty_tracks: tracks without any instrument also skip (default True)
|
|
1614
|
+
|
|
1615
|
+
Returns: {
|
|
1616
|
+
"ok": bool,
|
|
1617
|
+
"tracks_tested": int,
|
|
1618
|
+
"alive": [track_index...],
|
|
1619
|
+
"dead": [{track_index, track_name, peak_level}...],
|
|
1620
|
+
"skipped": [{track_index, reason}...],
|
|
1621
|
+
}
|
|
1622
|
+
"""
|
|
1623
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
1624
|
+
try:
|
|
1625
|
+
session = ableton.send_command("get_session_info", {})
|
|
1626
|
+
except Exception as exc:
|
|
1627
|
+
return {"ok": False, "error": f"get_session_info failed: {exc}"}
|
|
1628
|
+
if not isinstance(session, dict):
|
|
1629
|
+
return {"ok": False, "error": "Unexpected get_session_info response"}
|
|
1630
|
+
|
|
1631
|
+
tracks = session.get("tracks", []) or []
|
|
1632
|
+
alive: list = []
|
|
1633
|
+
dead: list = []
|
|
1634
|
+
skipped: list = []
|
|
1635
|
+
|
|
1636
|
+
for t in tracks:
|
|
1637
|
+
tid = t.get("index")
|
|
1638
|
+
tname = t.get("name", f"Track {tid}")
|
|
1639
|
+
if tid is None:
|
|
1640
|
+
continue
|
|
1641
|
+
is_audio = bool(t.get("is_audio_track") or t.get("type") == "audio")
|
|
1642
|
+
if skip_audio_tracks and is_audio:
|
|
1643
|
+
skipped.append({"track_index": tid, "track_name": tname,
|
|
1644
|
+
"reason": "audio_track_no_midi_input"})
|
|
1645
|
+
continue
|
|
1646
|
+
devices = t.get("devices") or []
|
|
1647
|
+
if skip_empty_tracks and not devices:
|
|
1648
|
+
skipped.append({"track_index": tid, "track_name": tname,
|
|
1649
|
+
"reason": "no_devices_on_track"})
|
|
1650
|
+
continue
|
|
1651
|
+
|
|
1652
|
+
# Run the per-track health check.
|
|
1653
|
+
result = await verify_device_health(
|
|
1654
|
+
ctx, track_index=tid,
|
|
1655
|
+
test_midi_note=test_midi_note,
|
|
1656
|
+
test_velocity=test_velocity,
|
|
1657
|
+
test_duration_ms=test_duration_ms,
|
|
1658
|
+
threshold=threshold,
|
|
1659
|
+
)
|
|
1660
|
+
if not result.get("ok"):
|
|
1661
|
+
skipped.append({"track_index": tid, "track_name": tname,
|
|
1662
|
+
"reason": result.get("error", "health_check_failed")})
|
|
1663
|
+
continue
|
|
1664
|
+
if result.get("alive"):
|
|
1665
|
+
alive.append(tid)
|
|
1666
|
+
else:
|
|
1667
|
+
dead.append({
|
|
1668
|
+
"track_index": tid,
|
|
1669
|
+
"track_name": tname,
|
|
1670
|
+
"peak_level": result.get("peak_level", 0),
|
|
1671
|
+
})
|
|
1672
|
+
|
|
1673
|
+
return {
|
|
1674
|
+
"ok": True,
|
|
1675
|
+
"tracks_tested": len(alive) + len(dead),
|
|
1676
|
+
"alive": alive,
|
|
1677
|
+
"dead": dead,
|
|
1678
|
+
"skipped": skipped,
|
|
1679
|
+
"summary": (
|
|
1680
|
+
f"{len(alive)} alive, {len(dead)} dead, "
|
|
1681
|
+
f"{len(skipped)} skipped out of {len(tracks)} total tracks"
|
|
1682
|
+
),
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
|
|
1537
1686
|
@mcp.tool()
|
|
1538
1687
|
def get_momentary_loudness(ctx: Context) -> dict:
|
|
1539
1688
|
"""Get EBU R128 momentary LUFS + true peak from FluCoMa.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.1",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 — 422 tools, 52 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
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.16.
|
|
8
|
+
__version__ = "1.16.1"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.dreamrec/livepilot",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "422-tool agentic MCP production system for Ableton Live 12 — device atlas, sample engine, composer",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.16.
|
|
9
|
+
"version": "1.16.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.16.
|
|
14
|
+
"version": "1.16.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|