livepilot 1.16.0 → 1.17.0

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +344 -5
  2. package/README.md +16 -15
  3. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  4. package/m4l_device/livepilot_bridge.js +1 -1
  5. package/mcp_server/__init__.py +1 -1
  6. package/mcp_server/atlas/__init__.py +85 -0
  7. package/mcp_server/atlas/device_atlas.json +3183 -382
  8. package/mcp_server/atlas/device_techniques_index.json +1510 -0
  9. package/mcp_server/atlas/enrichments/__init__.py +1 -0
  10. package/mcp_server/atlas/enrichments/audio_effects/amp.yaml +112 -0
  11. package/mcp_server/atlas/enrichments/audio_effects/audio_effect_rack.yaml +77 -0
  12. package/mcp_server/atlas/enrichments/audio_effects/cabinet.yaml +81 -0
  13. package/mcp_server/atlas/enrichments/audio_effects/corpus.yaml +128 -0
  14. package/mcp_server/atlas/enrichments/audio_effects/envelope_follower.yaml +99 -0
  15. package/mcp_server/atlas/enrichments/audio_effects/external_audio_effect.yaml +64 -0
  16. package/mcp_server/atlas/enrichments/audio_effects/looper.yaml +85 -0
  17. package/mcp_server/atlas/enrichments/audio_effects/pitch_hack.yaml +61 -0
  18. package/mcp_server/atlas/enrichments/audio_effects/pitchloop89.yaml +111 -0
  19. package/mcp_server/atlas/enrichments/audio_effects/re_enveloper.yaml +51 -0
  20. package/mcp_server/atlas/enrichments/audio_effects/resonators.yaml +121 -0
  21. package/mcp_server/atlas/enrichments/audio_effects/snipper.yaml +53 -0
  22. package/mcp_server/atlas/enrichments/audio_effects/spectral_blur.yaml +64 -0
  23. package/mcp_server/atlas/enrichments/audio_effects/spectrum.yaml +61 -0
  24. package/mcp_server/atlas/enrichments/audio_effects/tuner.yaml +43 -0
  25. package/mcp_server/atlas/enrichments/audio_effects/utility.yaml +118 -0
  26. package/mcp_server/atlas/enrichments/audio_effects/vocoder.yaml +94 -0
  27. package/mcp_server/atlas/enrichments/instruments/analog.yaml +11 -0
  28. package/mcp_server/atlas/enrichments/instruments/bass.yaml +11 -0
  29. package/mcp_server/atlas/enrichments/instruments/bell_tower.yaml +75 -0
  30. package/mcp_server/atlas/enrichments/instruments/collision.yaml +11 -0
  31. package/mcp_server/atlas/enrichments/instruments/drift.yaml +11 -0
  32. package/mcp_server/atlas/enrichments/instruments/drum_rack.yaml +142 -0
  33. package/mcp_server/atlas/enrichments/instruments/electric.yaml +11 -0
  34. package/mcp_server/atlas/enrichments/instruments/emit.yaml +11 -0
  35. package/mcp_server/atlas/enrichments/instruments/granulator_iii.yaml +124 -0
  36. package/mcp_server/atlas/enrichments/instruments/harmonic_drone_generator.yaml +83 -0
  37. package/mcp_server/atlas/enrichments/instruments/impulse.yaml +47 -0
  38. package/mcp_server/atlas/enrichments/instruments/meld.yaml +11 -0
  39. package/mcp_server/atlas/enrichments/instruments/operator.yaml +11 -0
  40. package/mcp_server/atlas/enrichments/instruments/poli.yaml +11 -0
  41. package/mcp_server/atlas/enrichments/instruments/sampler.yaml +12 -0
  42. package/mcp_server/atlas/enrichments/instruments/simpler.yaml +15 -0
  43. package/mcp_server/atlas/enrichments/instruments/sting_iftah.yaml +44 -0
  44. package/mcp_server/atlas/enrichments/instruments/tension.yaml +11 -0
  45. package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +11 -0
  46. package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +11 -0
  47. package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +11 -0
  48. package/mcp_server/atlas/enrichments/midi_effects/expressive_chords.yaml +38 -0
  49. package/mcp_server/atlas/enrichments/midi_effects/filler.yaml +49 -0
  50. package/mcp_server/atlas/enrichments/midi_effects/microtuner.yaml +83 -0
  51. package/mcp_server/atlas/enrichments/midi_effects/patterns_iftah.yaml +38 -0
  52. package/mcp_server/atlas/enrichments/midi_effects/phase_pattern.yaml +51 -0
  53. package/mcp_server/atlas/enrichments/midi_effects/polyrhythm.yaml +46 -0
  54. package/mcp_server/atlas/enrichments/midi_effects/retrigger.yaml +40 -0
  55. package/mcp_server/atlas/enrichments/midi_effects/slice_shuffler.yaml +39 -0
  56. package/mcp_server/atlas/enrichments/midi_effects/sq_sequencer.yaml +39 -0
  57. package/mcp_server/atlas/enrichments/midi_effects/stages.yaml +38 -0
  58. package/mcp_server/atlas/enrichments/utility/arrangement_looper.yaml +31 -0
  59. package/mcp_server/atlas/enrichments/utility/cv_clock_in.yaml +25 -0
  60. package/mcp_server/atlas/enrichments/utility/cv_clock_out.yaml +25 -0
  61. package/mcp_server/atlas/enrichments/utility/cv_envelope_follower.yaml +38 -0
  62. package/mcp_server/atlas/enrichments/utility/cv_in.yaml +26 -0
  63. package/mcp_server/atlas/enrichments/utility/cv_instrument.yaml +34 -0
  64. package/mcp_server/atlas/enrichments/utility/cv_lfo.yaml +38 -0
  65. package/mcp_server/atlas/enrichments/utility/cv_shaper.yaml +35 -0
  66. package/mcp_server/atlas/enrichments/utility/cv_triggers.yaml +26 -0
  67. package/mcp_server/atlas/enrichments/utility/cv_utility.yaml +37 -0
  68. package/mcp_server/atlas/enrichments/utility/performer.yaml +51 -0
  69. package/mcp_server/atlas/enrichments/utility/prearranger.yaml +36 -0
  70. package/mcp_server/atlas/enrichments/utility/rotating_rhythm_generator.yaml +35 -0
  71. package/mcp_server/atlas/enrichments/utility/surround_panner.yaml +40 -0
  72. package/mcp_server/atlas/enrichments/utility/variations.yaml +40 -0
  73. package/mcp_server/atlas/enrichments/utility/vector_map.yaml +57 -0
  74. package/mcp_server/atlas/tools.py +291 -0
  75. package/mcp_server/m4l_bridge.py +19 -2
  76. package/mcp_server/sample_engine/tools.py +190 -72
  77. package/mcp_server/server.py +18 -6
  78. package/mcp_server/splice_client/client.py +90 -18
  79. package/mcp_server/splice_client/http_bridge.py +414 -138
  80. package/mcp_server/splice_client/models.py +12 -0
  81. package/mcp_server/tools/analyzer.py +150 -1
  82. package/mcp_server/tools/automation.py +168 -0
  83. package/package.json +2 -2
  84. package/remote_script/LivePilot/__init__.py +1 -1
  85. package/remote_script/LivePilot/arrangement.py +216 -1
  86. package/server.json +3 -3
@@ -0,0 +1,40 @@
1
+ id: surround_panner
2
+ name: Surround Panner
3
+ sonic_description: >
4
+ Multichannel spatial panner. 4/6/8-channel speaker setups, 8 presets.
5
+ Not Ambisonic — discrete-channel panning. Only relevant for multi-channel
6
+ output setups (installation, quad+ club, cinema). In stereo studio work,
7
+ offers minimal benefit.
8
+ category: spatial_fx
9
+ character_tags: [multichannel, spatial, quad, surround]
10
+ use_cases: [installation, quad_club, cinema, multichannel_studio]
11
+ genre_affinity:
12
+ primary: [ambient_installation, cinema]
13
+ secondary: [experimental]
14
+ complexity: intermediate
15
+ introduced_in: "11.0"
16
+ pack: Surround Panner
17
+ creator: Ableton
18
+ class_name: PluginDevice
19
+ requires_special_output: true # multichannel interface
20
+
21
+ key_parameters:
22
+ - name: "Channel Layout"
23
+ description: "4.0, 5.1, 7.1, 8.0 — select target format."
24
+ - name: "XY Pan"
25
+ description: "2D pan position."
26
+ - name: "Rotate"
27
+ description: "Auto-rotate the signal around the listening position."
28
+ - name: "Focus / Spread"
29
+ description: "Width of the source in the field."
30
+
31
+ signature_techniques:
32
+ - name: "Quad-club rotating hi-hat"
33
+ description: "Hi-hat bus → Surround Panner in quad → auto-Rotate at slow rate → hats circle the dance floor."
34
+ aesthetic: [live_techno]
35
+
36
+ gotchas:
37
+ - "Stereo monitoring shows only a downmix — impossible to audition properly without a multichannel setup"
38
+
39
+ learn_more:
40
+ official: "https://www.ableton.com/en/packs/surround-panner/"
@@ -0,0 +1,40 @@
1
+ id: variations
2
+ name: Variations
3
+ sonic_description: >
4
+ Snapshot management from Performance Pack (Iftah). Captures any device
5
+ chain state or clip state and morphs between snapshots, quantized to a
6
+ musical boundary. The studio weapon: save 5 variations of a chord stab's
7
+ effect chain, clip-trigger morphing between them = gradual evolution over
8
+ 8-16 bars without 50 automation lanes. This is how you get the
9
+ 'chord-is-different-every-time' Villalobos behavior programmatically.
10
+ category: state_management
11
+ character_tags: [snapshot, morph, quantized, state_recall]
12
+ use_cases: [chain_morph, live_recall, section_transition, effect_evolution]
13
+ genre_affinity:
14
+ primary: [deep_minimal, microhouse, electronic_live]
15
+ secondary: [all]
16
+ complexity: intermediate
17
+ introduced_in: "11.0"
18
+ pack: Performance Pack
19
+ creator: Iftah
20
+ class_name: PluginDevice
21
+
22
+ key_parameters:
23
+ - name: "Snapshot Slots"
24
+ description: "Store up to N states. Each slot captures every parameter's value."
25
+ - name: "Morph Time"
26
+ description: "How long the interpolation between snapshots takes."
27
+ - name: "Quantization"
28
+ description: "Snap morph to bar / beat / scene boundary."
29
+ - name: "Selective Inclusion"
30
+ description: "Mark which parameters participate in each snapshot — exclude the ones you don't want to morph."
31
+ signature_techniques:
32
+ - name: "Chord stab evolution"
33
+ description: "Save 5 effect-chain states on a chord stab (different reverbs, pitches, filters). Clip-trigger morph between them every 2 bars. Result: chord stab is subtly different every time it plays."
34
+ aesthetic: [deep_minimal, microhouse]
35
+ - name: "Live scene state recall"
36
+ description: "Bind scenes to Variation snapshots — firing a scene also morphs the whole device chain."
37
+ aesthetic: [electronic_live]
38
+
39
+ learn_more:
40
+ official: "https://www.ableton.com/en/packs/performance-pack/"
@@ -0,0 +1,57 @@
1
+ id: vector_map
2
+ name: Vector Map
3
+ sonic_description: >
4
+ Particle-physics modulation router (Inspired by Nature, Dillon Bastan).
5
+ One particle drives multiple parameter destinations simultaneously. Unlike
6
+ an LFO (one source → one destination), Vector Map lets a single particle's
7
+ position control filter cutoff AND send level AND reverb decay at once —
8
+ coupled modulation that LFOs cannot produce. Essential for organic,
9
+ physically-coherent sound design.
10
+ category: modulation_source
11
+ character_tags: [particle_physics, coupled_modulation, multi_destination, generative]
12
+ use_cases: [coupled_modulation, non_trivial_modulation, physical_motion]
13
+ genre_affinity:
14
+ primary: [experimental, ambient, deep_minimal]
15
+ secondary: [idm, sound_design]
16
+ complexity: advanced
17
+ introduced_in: "11.0"
18
+ pack: Inspired by Nature
19
+ creator: Dillon Bastan
20
+ class_name: PluginDevice
21
+
22
+ key_parameters:
23
+ - name: "Particle Behavior"
24
+ description: "Gravity, friction, forces — defines how the particle moves."
25
+ - name: "Destination Mappings"
26
+ description: "Route the particle's X/Y/velocity to multiple Live parameters."
27
+ - name: "Depth per destination"
28
+ description: "How much the particle influences each destination."
29
+
30
+ signature_techniques:
31
+ - name: "Coupled filter + reverb motion"
32
+ description: "Particle X → filter cutoff, particle Y → reverb wet, particle velocity → dry/wet balance. Single physical motion drives a 3-parameter sonic change."
33
+ aesthetic: [ambient, experimental]
34
+ - name: "Gravity-well attractor"
35
+ description: "Set strong gravity toward center + low friction → particle orbits return to origin with decay. Map to pad tone controls → pad breathes in-and-out toward a rest state."
36
+ aesthetic: [ambient, generative]
37
+ - name: "Chaotic lock-in"
38
+ description: "High initial velocity + medium friction → particle wanders, eventually settles. Map to filter + detune → synth starts mobile, finds a rest-state timbre over 16 bars."
39
+ aesthetic: [deep_minimal, experimental]
40
+
41
+ pairs_well_with:
42
+ - device: "Tree Tone"
43
+ reason: "Both from Inspired by Nature — Vector Map drives Tree Tone's structural parameters for coupled organic motion"
44
+ - device: "Bouncy Notes"
45
+ reason: "Vector Map modulates Bouncy Notes' ballistics → asymmetric, evolving note cascades"
46
+ - device: "Any parameter-rich synth"
47
+ reason: "Vector Map really shines when addressing 4+ coupled parameters on the same instrument — Wavetable, Tension, Collision"
48
+
49
+ gotchas:
50
+ - "Vector Map is a MODULATOR. Without destination mappings, the particle motion is invisible"
51
+ - "Heavy-parameter mappings can create runaway feedback loops (mod destination is itself a modulator source) — sanity-check your mod routing for cycles"
52
+ - "The particle is a single point — NOT multi-particle. For swarm behavior, stack multiple Vector Maps"
53
+ - "Very low friction values make the particle orbit forever — pair with gravity to stabilize"
54
+ - "Freezing the session (freeze_track) captures the particle's current state, not its dynamic behavior — flatten with the particle paused at a known position"
55
+
56
+ learn_more:
57
+ pack: "Inspired by Nature"
@@ -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,
@@ -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
- BAND_NAMES = ["sub", "low", "low_mid", "mid", "high_mid", "high", "presence", "air"]
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(self.BAND_NAMES):
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)