livepilot 1.27.2 → 1.27.3
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 +66 -0
- package/README.md +10 -7
- package/bin/livepilot.js +97 -39
- package/livepilot/.Codex-plugin/plugin.json +1 -1
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/SKILL.md +33 -1
- package/livepilot/skills/livepilot-core/references/atlas-tool-notes.md +69 -0
- package/livepilot/skills/livepilot-core/references/overview.md +18 -11
- package/livepilot/skills/livepilot-core/references/perception.md +125 -0
- package/livepilot/skills/livepilot-devices/references/device-parameter-units.md +66 -0
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +8 -0
- package/livepilot/skills/livepilot-release/SKILL.md +1 -1
- package/livepilot/skills/livepilot-sample-engine/references/splice-tools-notes.md +56 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +6 -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 +181 -40
- package/mcp_server/atlas/overlays.py +46 -3
- package/mcp_server/atlas/tools.py +226 -86
- package/mcp_server/audit/checks.py +50 -22
- package/mcp_server/audit/state.py +10 -2
- package/mcp_server/audit/tools.py +27 -6
- package/mcp_server/composer/develop/apply.py +7 -7
- package/mcp_server/composer/fast/apply.py +29 -23
- package/mcp_server/composer/framework/atlas_resolver.py +16 -1
- package/mcp_server/composer/full/apply.py +40 -34
- package/mcp_server/composer/full/engine.py +66 -32
- package/mcp_server/composer/tools.py +4 -3
- package/mcp_server/connection.py +67 -9
- package/mcp_server/creative_constraints/engine.py +10 -2
- package/mcp_server/creative_constraints/tools.py +24 -7
- package/mcp_server/curves.py +30 -7
- package/mcp_server/device_forge/builder.py +40 -11
- package/mcp_server/device_forge/models.py +9 -0
- package/mcp_server/device_forge/tools.py +30 -11
- package/mcp_server/experiment/engine.py +29 -22
- package/mcp_server/experiment/tools.py +15 -5
- package/mcp_server/m4l_bridge.py +69 -7
- package/mcp_server/memory/taste_graph.py +43 -0
- package/mcp_server/memory/technique_store.py +26 -3
- package/mcp_server/memory/tools.py +62 -4
- package/mcp_server/mix_engine/critics.py +187 -30
- package/mcp_server/mix_engine/models.py +21 -1
- package/mcp_server/mix_engine/state_builder.py +87 -8
- package/mcp_server/mix_engine/tools.py +16 -4
- package/mcp_server/performance_engine/tools.py +56 -8
- package/mcp_server/persistence/base_store.py +11 -0
- package/mcp_server/persistence/project_store.py +132 -8
- package/mcp_server/persistence/taste_store.py +90 -7
- package/mcp_server/preview_studio/engine.py +40 -10
- package/mcp_server/preview_studio/models.py +40 -0
- package/mcp_server/preview_studio/tools.py +56 -12
- package/mcp_server/project_brain/arrangement_graph.py +4 -0
- package/mcp_server/project_brain/models.py +2 -0
- package/mcp_server/project_brain/role_graph.py +13 -7
- package/mcp_server/reference_engine/gap_analyzer.py +58 -3
- package/mcp_server/reference_engine/profile_builder.py +47 -4
- package/mcp_server/reference_engine/tools.py +6 -0
- package/mcp_server/runtime/execution_router.py +51 -1
- package/mcp_server/runtime/tools.py +0 -1
- package/mcp_server/sample_engine/sources.py +0 -2
- package/mcp_server/sample_engine/tools.py +19 -38
- package/mcp_server/semantic_moves/mix_compilers.py +276 -51
- package/mcp_server/semantic_moves/performance_compilers.py +51 -17
- package/mcp_server/semantic_moves/resolvers.py +45 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +14 -6
- package/mcp_server/semantic_moves/tools.py +80 -2
- package/mcp_server/semantic_moves/transition_compilers.py +26 -9
- package/mcp_server/server.py +26 -24
- package/mcp_server/session_continuity/tracker.py +51 -3
- package/mcp_server/song_brain/builder.py +47 -5
- package/mcp_server/song_brain/tools.py +21 -7
- package/mcp_server/sound_design/critics.py +1 -0
- package/mcp_server/splice_client/client.py +117 -33
- package/mcp_server/splice_client/http_bridge.py +15 -3
- package/mcp_server/splice_client/quota.py +28 -0
- package/mcp_server/stuckness_detector/detector.py +8 -5
- package/mcp_server/synthesis_brain/adapters/drift.py +13 -0
- package/mcp_server/synthesis_brain/adapters/meld.py +13 -0
- package/mcp_server/tools/_analyzer_engine/sample.py +36 -10
- package/mcp_server/tools/_perception_engine.py +6 -0
- package/mcp_server/tools/agent_os.py +4 -1
- package/mcp_server/tools/analyzer.py +198 -209
- package/mcp_server/tools/arrangement.py +7 -4
- package/mcp_server/tools/automation.py +24 -4
- package/mcp_server/tools/browser.py +25 -11
- package/mcp_server/tools/clips.py +6 -0
- package/mcp_server/tools/composition.py +33 -2
- package/mcp_server/tools/devices.py +53 -53
- package/mcp_server/tools/generative.py +14 -14
- package/mcp_server/tools/harmony.py +7 -7
- package/mcp_server/tools/mixing.py +4 -4
- package/mcp_server/tools/planner.py +68 -6
- package/mcp_server/tools/research.py +20 -2
- package/mcp_server/tools/theory.py +10 -10
- package/mcp_server/tools/transport.py +7 -2
- package/mcp_server/transition_engine/critics.py +13 -1
- package/mcp_server/user_corpus/tools.py +30 -1
- package/mcp_server/wonder_mode/engine.py +82 -9
- package/mcp_server/wonder_mode/session.py +32 -10
- package/mcp_server/wonder_mode/tools.py +14 -1
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +93 -33
- package/remote_script/LivePilot/browser.py +60 -4
- package/remote_script/LivePilot/devices.py +132 -62
- package/remote_script/LivePilot/mixing.py +31 -5
- package/remote_script/LivePilot/server.py +94 -22
- package/remote_script/LivePilot/transport.py +11 -0
- package/requirements.txt +5 -5
- package/server.json +2 -2
|
@@ -14,6 +14,53 @@ from .models import SemanticMove
|
|
|
14
14
|
from . import resolvers
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _kernel_track(kernel: dict, track_index: int) -> dict:
|
|
18
|
+
"""Return the raw kernel track dict (with its `devices`) for an index.
|
|
19
|
+
|
|
20
|
+
resolvers.find_tracks_by_role returns a slimmed dict without `devices`, so
|
|
21
|
+
device resolution must reach back into the raw session_info track to see the
|
|
22
|
+
chain. Returns {} when not found.
|
|
23
|
+
"""
|
|
24
|
+
for t in kernel.get("session_info", {}).get("tracks", []) or []:
|
|
25
|
+
if isinstance(t, dict) and t.get("index") == track_index:
|
|
26
|
+
return t
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _find_eq_device_index(track: dict) -> int | None:
|
|
31
|
+
"""Return the chain index of an EQ device on this track, or None.
|
|
32
|
+
|
|
33
|
+
Only inspects device data the kernel already carries — never assumes a
|
|
34
|
+
device that hasn't been confirmed present. Used so frequency-carve writes
|
|
35
|
+
target a resolved device rather than a blind device_index=0 (the
|
|
36
|
+
wrong-device hazard guarded by tests/test_compiler_safety_contract.py).
|
|
37
|
+
"""
|
|
38
|
+
for i, dev in enumerate(track.get("devices", []) or []):
|
|
39
|
+
if not isinstance(dev, dict):
|
|
40
|
+
continue
|
|
41
|
+
name = str(dev.get("name", "")).lower()
|
|
42
|
+
class_name = str(dev.get("class_name", "")).lower()
|
|
43
|
+
if "eq" in name or "eq8" in class_name or "eqeight" in class_name:
|
|
44
|
+
return dev.get("index", i)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _find_compressor_device_index(track: dict) -> int | None:
|
|
49
|
+
"""Return the chain index of a Compressor on this track, or None.
|
|
50
|
+
|
|
51
|
+
Same kernel-snapshot-only discipline as _find_eq_device_index — used so the
|
|
52
|
+
sidechain routing targets a resolved compressor rather than a blind index.
|
|
53
|
+
"""
|
|
54
|
+
for i, dev in enumerate(track.get("devices", []) or []):
|
|
55
|
+
if not isinstance(dev, dict):
|
|
56
|
+
continue
|
|
57
|
+
name = str(dev.get("name", "")).lower()
|
|
58
|
+
class_name = str(dev.get("class_name", "")).lower()
|
|
59
|
+
if "compressor" in name or "compressor" in class_name:
|
|
60
|
+
return dev.get("index", i)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
17
64
|
def _compile_make_punchier(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
18
65
|
"""Compile 'make_punchier': push drums, pull pads, tighten master bus."""
|
|
19
66
|
steps = []
|
|
@@ -39,25 +86,33 @@ def _compile_make_punchier(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
|
39
86
|
verify_after=False,
|
|
40
87
|
))
|
|
41
88
|
|
|
42
|
-
# Step 2: Push drum volume
|
|
89
|
+
# Step 2: Push drum volume — RELATIVE nudge (P2-21), not an absolute
|
|
90
|
+
# overwrite. A hot drum bus must get louder, not slammed down to 0.75.
|
|
43
91
|
for dt in drum_tracks[:1]: # Only first drum track
|
|
44
92
|
idx = dt["index"]
|
|
93
|
+
target = resolvers.compile_relative_volume(
|
|
94
|
+
dt.get("volume"), 8, cap=0.85, fallback=0.75
|
|
95
|
+
)
|
|
45
96
|
steps.append(CompiledStep(
|
|
46
97
|
tool="set_track_volume",
|
|
47
|
-
params={"track_index": idx, "volume":
|
|
48
|
-
description=f"Push {dt['name']} (track {idx}) to
|
|
98
|
+
params={"track_index": idx, "volume": target},
|
|
99
|
+
description=f"Push {dt['name']} (track {idx}) to {target:.2f} for transient punch",
|
|
49
100
|
))
|
|
50
|
-
descriptions.append(f"Push {dt['name']} volume to
|
|
101
|
+
descriptions.append(f"Push {dt['name']} volume to {target:.2f}")
|
|
51
102
|
|
|
52
|
-
# Step 3: Pull back pads
|
|
103
|
+
# Step 3: Pull back pads — RELATIVE nudge, floored so a quiet pad
|
|
104
|
+
# doesn't get muted outright.
|
|
53
105
|
for pt in pad_tracks:
|
|
54
106
|
idx = pt["index"]
|
|
107
|
+
target = resolvers.compile_relative_volume(
|
|
108
|
+
pt.get("volume"), -10, floor=0.15, fallback=0.25
|
|
109
|
+
)
|
|
55
110
|
steps.append(CompiledStep(
|
|
56
111
|
tool="set_track_volume",
|
|
57
|
-
params={"track_index": idx, "volume":
|
|
58
|
-
description=f"Pull {pt['name']} (track {idx}) to
|
|
112
|
+
params={"track_index": idx, "volume": target},
|
|
113
|
+
description=f"Pull {pt['name']} (track {idx}) to {target:.2f} for contrast",
|
|
59
114
|
))
|
|
60
|
-
descriptions.append(f"Pull {pt['name']} volume to
|
|
115
|
+
descriptions.append(f"Pull {pt['name']} volume to {target:.2f}")
|
|
61
116
|
|
|
62
117
|
# Step 4: Verify
|
|
63
118
|
steps.append(CompiledStep(
|
|
@@ -114,13 +169,17 @@ def _compile_tighten_low_end(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
|
114
169
|
optional=True,
|
|
115
170
|
))
|
|
116
171
|
|
|
117
|
-
# Step 2: Reduce bass volume slightly
|
|
172
|
+
# Step 2: Reduce bass volume slightly — RELATIVE nudge (P2-21), floored
|
|
173
|
+
# so a bass already sitting quiet doesn't get pulled toward silence.
|
|
174
|
+
target = resolvers.compile_relative_volume(
|
|
175
|
+
bass.get("volume"), -10, floor=0.35, fallback=0.58
|
|
176
|
+
)
|
|
118
177
|
steps.append(CompiledStep(
|
|
119
178
|
tool="set_track_volume",
|
|
120
|
-
params={"track_index": idx, "volume":
|
|
121
|
-
description=f"Reduce {bass['name']} volume to
|
|
179
|
+
params={"track_index": idx, "volume": target},
|
|
180
|
+
description=f"Reduce {bass['name']} volume to {target:.2f} (tighten sub)",
|
|
122
181
|
))
|
|
123
|
-
descriptions.append(f"Reduce {bass['name']} volume to
|
|
182
|
+
descriptions.append(f"Reduce {bass['name']} volume to {target:.2f}")
|
|
124
183
|
|
|
125
184
|
# Step 3: Verify
|
|
126
185
|
steps.append(CompiledStep(
|
|
@@ -146,7 +205,13 @@ def _compile_tighten_low_end(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
|
146
205
|
|
|
147
206
|
|
|
148
207
|
def _compile_widen_stereo(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
149
|
-
"""Compile 'widen_stereo': pan harmonic elements wider, add depth.
|
|
208
|
+
"""Compile 'widen_stereo': pan harmonic elements wider, add depth.
|
|
209
|
+
|
|
210
|
+
Fallback: when no lead/harmonic tracks are role-classified (e.g., tracks
|
|
211
|
+
are named "Q-Call"/"A-Answer" which don't match role keywords), fall back
|
|
212
|
+
to widening prominent non-drum / non-bass tracks. This prevents the move
|
|
213
|
+
from no-opping on mixes with unusually-named tracks.
|
|
214
|
+
"""
|
|
150
215
|
steps = []
|
|
151
216
|
warnings = []
|
|
152
217
|
descriptions = []
|
|
@@ -155,35 +220,73 @@ def _compile_widen_stereo(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
|
155
220
|
lead_tracks = resolvers.find_tracks_by_role(kernel, ["lead"])
|
|
156
221
|
perc_tracks = resolvers.find_tracks_by_role(kernel, ["percussion"])
|
|
157
222
|
|
|
223
|
+
# Fallback: if no harmonic/lead tracks found via role, use prominent
|
|
224
|
+
# non-drum/non-bass tracks (any role except drums/bass/fx/unknown=skip).
|
|
225
|
+
# This covers real-world mixes where melodic tracks are named after their
|
|
226
|
+
# musical function ("Q-Call", "A-Answer", "Melody 1") rather than role keywords.
|
|
227
|
+
using_fallback = False
|
|
158
228
|
if not chord_tracks and not lead_tracks:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
229
|
+
all_tracks = resolvers.find_tracks_by_role(
|
|
230
|
+
kernel, ["chords", "pad", "lead", "percussion", "fx", "unknown"]
|
|
231
|
+
)
|
|
232
|
+
# Exclude drums and bass by collecting everything non-drum/non-bass
|
|
233
|
+
exclude_roles = {"drums", "bass"}
|
|
234
|
+
all_raw_tracks = kernel.get("session_info", {}).get("tracks", [])
|
|
235
|
+
fallback_tracks = [
|
|
236
|
+
{
|
|
237
|
+
"index": t.get("index", 0),
|
|
238
|
+
"name": t.get("name", ""),
|
|
239
|
+
"role": resolvers.infer_role(t.get("name", "")),
|
|
240
|
+
"volume": t.get("volume"),
|
|
241
|
+
"pan": t.get("pan"),
|
|
242
|
+
"mute": t.get("mute", False),
|
|
243
|
+
"solo": t.get("solo", False),
|
|
244
|
+
}
|
|
245
|
+
for t in all_raw_tracks
|
|
246
|
+
if resolvers.infer_role(t.get("name", "")) not in exclude_roles
|
|
247
|
+
]
|
|
248
|
+
if fallback_tracks:
|
|
249
|
+
# Use up to first 2 as a left/right pair
|
|
250
|
+
chord_tracks = fallback_tracks[:1]
|
|
251
|
+
lead_tracks = fallback_tracks[1:2]
|
|
252
|
+
using_fallback = True
|
|
253
|
+
warnings.append(
|
|
254
|
+
"No harmonic or lead tracks found by role — "
|
|
255
|
+
"falling back to widening prominent non-drum/non-bass tracks: "
|
|
256
|
+
+ ", ".join(t["name"] for t in fallback_tracks[:2])
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
warnings.append("No harmonic, lead, or wideable tracks found — no changes")
|
|
260
|
+
|
|
261
|
+
# Pan chords/first-fallback left
|
|
162
262
|
for ct in chord_tracks[:1]:
|
|
163
263
|
steps.append(CompiledStep(
|
|
164
264
|
tool="set_track_pan",
|
|
165
265
|
params={"track_index": ct["index"], "pan": -0.35},
|
|
166
|
-
description=f"Pan {ct['name']} left (-35%) for width"
|
|
266
|
+
description=f"Pan {ct['name']} left (-35%) for width"
|
|
267
|
+
+ (" [fallback]" if using_fallback else ""),
|
|
167
268
|
))
|
|
168
269
|
descriptions.append(f"Pan {ct['name']} left 35%")
|
|
169
270
|
|
|
170
|
-
# Pan lead right
|
|
271
|
+
# Pan lead/second-fallback right
|
|
171
272
|
for lt in lead_tracks[:1]:
|
|
172
273
|
steps.append(CompiledStep(
|
|
173
274
|
tool="set_track_pan",
|
|
174
275
|
params={"track_index": lt["index"], "pan": 0.30},
|
|
175
|
-
description=f"Pan {lt['name']} right (+30%) for width"
|
|
276
|
+
description=f"Pan {lt['name']} right (+30%) for width"
|
|
277
|
+
+ (" [fallback]" if using_fallback else ""),
|
|
176
278
|
))
|
|
177
279
|
descriptions.append(f"Pan {lt['name']} right 30%")
|
|
178
280
|
|
|
179
|
-
# Pan perc slightly
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
281
|
+
# Pan perc slightly (only when using primary role classification)
|
|
282
|
+
if not using_fallback:
|
|
283
|
+
for pt in perc_tracks[:1]:
|
|
284
|
+
steps.append(CompiledStep(
|
|
285
|
+
tool="set_track_pan",
|
|
286
|
+
params={"track_index": pt["index"], "pan": 0.15},
|
|
287
|
+
description=f"Pan {pt['name']} slightly right (+15%)",
|
|
288
|
+
))
|
|
289
|
+
descriptions.append(f"Pan {pt['name']} slightly right")
|
|
187
290
|
|
|
188
291
|
# Verify
|
|
189
292
|
steps.append(CompiledStep(
|
|
@@ -217,13 +320,17 @@ def _compile_darken_mix(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
|
217
320
|
bright_tracks = resolvers.find_tracks_by_role(kernel, ["lead", "chords", "percussion"])
|
|
218
321
|
|
|
219
322
|
for bt in bright_tracks[:2]: # Max 2 tracks
|
|
220
|
-
# Reduce volume slightly to darken
|
|
323
|
+
# Reduce volume slightly to darken — RELATIVE nudge (P2-21), floored
|
|
324
|
+
# so an already-quiet bright track doesn't get pulled to silence.
|
|
325
|
+
target = resolvers.compile_relative_volume(
|
|
326
|
+
bt.get("volume"), -10, floor=0.20, fallback=0.40
|
|
327
|
+
)
|
|
221
328
|
steps.append(CompiledStep(
|
|
222
329
|
tool="set_track_volume",
|
|
223
|
-
params={"track_index": bt["index"], "volume":
|
|
224
|
-
description=f"Pull {bt['name']} to
|
|
330
|
+
params={"track_index": bt["index"], "volume": target},
|
|
331
|
+
description=f"Pull {bt['name']} to {target:.2f} for darker tone",
|
|
225
332
|
))
|
|
226
|
-
descriptions.append(f"Darken {bt['name']} to
|
|
333
|
+
descriptions.append(f"Darken {bt['name']} to {target:.2f}")
|
|
227
334
|
|
|
228
335
|
steps.append(CompiledStep(
|
|
229
336
|
tool="get_track_meters",
|
|
@@ -288,12 +395,16 @@ def _compile_reduce_repetition(move: SemanticMove, kernel: dict) -> CompiledPlan
|
|
|
288
395
|
|
|
289
396
|
|
|
290
397
|
def _compile_make_kick_bass_lock(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
291
|
-
"""Compile 'make_kick_bass_lock': carve space between kick and bass.
|
|
292
|
-
|
|
293
|
-
Strategy
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
398
|
+
"""Compile 'make_kick_bass_lock': carve frequency space between kick and bass.
|
|
399
|
+
|
|
400
|
+
Strategy (real frequency separation + timing):
|
|
401
|
+
1. Read bass EQ/filter state first so we know what's already there.
|
|
402
|
+
2. Insert an EQ Eight on the bass track (or use an existing one) and
|
|
403
|
+
apply a high-pass shelf dip in the kick's fundamental band (40-80 Hz)
|
|
404
|
+
to clear sub space for the kick.
|
|
405
|
+
3. Set up a sidechain compressor on the bass keyed from the kick track
|
|
406
|
+
so bass ducks on every kick hit — the defining "lock" gesture.
|
|
407
|
+
4. Optional: minor volume trim only if needed after the above two steps.
|
|
297
408
|
"""
|
|
298
409
|
steps: list[CompiledStep] = []
|
|
299
410
|
warnings: list[str] = []
|
|
@@ -319,18 +430,124 @@ def _compile_make_kick_bass_lock(move: SemanticMove, kernel: dict) -> CompiledPl
|
|
|
319
430
|
|
|
320
431
|
if bass_tracks:
|
|
321
432
|
bass = bass_tracks[0]
|
|
322
|
-
|
|
433
|
+
bass_idx = bass["index"]
|
|
434
|
+
|
|
435
|
+
# Step: read bass device chain state before touching it
|
|
323
436
|
steps.append(CompiledStep(
|
|
324
|
-
tool="
|
|
325
|
-
params={"track_index":
|
|
326
|
-
description=f"
|
|
437
|
+
tool="get_device_parameters",
|
|
438
|
+
params={"track_index": bass_idx, "device_index": 0},
|
|
439
|
+
description=f"Read {bass['name']} device chain state before carving",
|
|
440
|
+
verify_after=False,
|
|
441
|
+
optional=True,
|
|
327
442
|
))
|
|
328
|
-
|
|
443
|
+
|
|
444
|
+
# Frequency separation: carve a high-pass into an EQ on the bass so the
|
|
445
|
+
# kick's sub-fundamental has room. We only write EQ band parameters when
|
|
446
|
+
# the kernel already exposes a resolvable EQ device on this track — a
|
|
447
|
+
# device inserted in THIS plan has no known index at compile time, so a
|
|
448
|
+
# device_index=0 write would be a blind (wrong-device) target. See
|
|
449
|
+
# tests/test_compiler_safety_contract.py for the invariant this honors.
|
|
450
|
+
eq_idx = _find_eq_device_index(_kernel_track(kernel, bass_idx))
|
|
451
|
+
if eq_idx is not None:
|
|
452
|
+
# EQ confirmed present in the kernel snapshot → safe to carve it.
|
|
453
|
+
# EQ Eight band 1: "1 FilterType" (0 = Low Cut / High-pass),
|
|
454
|
+
# "1 Frequency" (Hz).
|
|
455
|
+
steps.append(CompiledStep(
|
|
456
|
+
tool="set_device_parameter",
|
|
457
|
+
params={
|
|
458
|
+
"track_index": bass_idx,
|
|
459
|
+
"device_index": eq_idx,
|
|
460
|
+
"parameter_name": "1 FilterType",
|
|
461
|
+
"value": 0.0, # Low Cut / High-pass
|
|
462
|
+
},
|
|
463
|
+
description=f"Set EQ band 1 to High-Pass on {bass['name']} (device {eq_idx})",
|
|
464
|
+
))
|
|
465
|
+
steps.append(CompiledStep(
|
|
466
|
+
tool="set_device_parameter",
|
|
467
|
+
params={
|
|
468
|
+
"track_index": bass_idx,
|
|
469
|
+
"device_index": eq_idx,
|
|
470
|
+
"parameter_name": "1 Frequency",
|
|
471
|
+
"value": 60.0, # 60 Hz — clears kick sub without losing bass body
|
|
472
|
+
},
|
|
473
|
+
description=f"Set HP cutoff to 60 Hz on {bass['name']} EQ (device {eq_idx})",
|
|
474
|
+
))
|
|
475
|
+
descriptions.append("HP carve @ 60 Hz on existing EQ")
|
|
476
|
+
else:
|
|
477
|
+
# No resolvable EQ → insert one as a scaffold. The HP-carve must be
|
|
478
|
+
# applied AFTER insertion (the new device's index isn't known until
|
|
479
|
+
# runtime); the sidechain below provides the timing lock regardless.
|
|
480
|
+
steps.append(CompiledStep(
|
|
481
|
+
tool="insert_device",
|
|
482
|
+
params={
|
|
483
|
+
"track_index": bass_idx,
|
|
484
|
+
"device_name": "EQ Eight",
|
|
485
|
+
"position": 0,
|
|
486
|
+
},
|
|
487
|
+
description=f"Insert EQ Eight on {bass['name']} for kick-sub carve",
|
|
488
|
+
))
|
|
489
|
+
descriptions.append(f"EQ Eight on {bass['name']}")
|
|
490
|
+
warnings.append(
|
|
491
|
+
f"Apply a band-1 high-pass (~60 Hz) to the new EQ on "
|
|
492
|
+
f"{bass['name']} after insertion — device index is not "
|
|
493
|
+
"resolvable at plan time."
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Timing lock: sidechain a compressor on the bass, keyed from the kick,
|
|
497
|
+
# so the bass ducks on every kick hit. The real compressor_set_sidechain
|
|
498
|
+
# tool only ROUTES the detector input on an EXISTING compressor
|
|
499
|
+
# (signature: track_index, device_index, source_type, source_channel —
|
|
500
|
+
# it does NOT set threshold/ratio/attack/release). So resolve a
|
|
501
|
+
# compressor from the kernel snapshot or insert one as a scaffold (its
|
|
502
|
+
# post-insert index isn't known at plan time → no blind device writes).
|
|
503
|
+
if kick_tracks:
|
|
504
|
+
kick = kick_tracks[0]
|
|
505
|
+
comp_idx = _find_compressor_device_index(_kernel_track(kernel, bass_idx))
|
|
506
|
+
if comp_idx is not None:
|
|
507
|
+
steps.append(CompiledStep(
|
|
508
|
+
tool="set_device_parameter",
|
|
509
|
+
params={
|
|
510
|
+
"track_index": bass_idx,
|
|
511
|
+
"device_index": comp_idx,
|
|
512
|
+
"parameter_name": "S/C On",
|
|
513
|
+
"value": 1.0,
|
|
514
|
+
},
|
|
515
|
+
description=f"Enable sidechain on {bass['name']} compressor (device {comp_idx})",
|
|
516
|
+
))
|
|
517
|
+
steps.append(CompiledStep(
|
|
518
|
+
tool="compressor_set_sidechain",
|
|
519
|
+
params={
|
|
520
|
+
"track_index": bass_idx,
|
|
521
|
+
"device_index": comp_idx,
|
|
522
|
+
"source_type": kick["name"],
|
|
523
|
+
"source_channel": "Post FX",
|
|
524
|
+
},
|
|
525
|
+
description=(
|
|
526
|
+
f"Route {kick['name']} into {bass['name']} compressor "
|
|
527
|
+
"sidechain — bass ducks on every kick hit"
|
|
528
|
+
),
|
|
529
|
+
))
|
|
530
|
+
descriptions.append(f"Sidechain {bass['name']} ← {kick['name']}")
|
|
531
|
+
else:
|
|
532
|
+
steps.append(CompiledStep(
|
|
533
|
+
tool="insert_device",
|
|
534
|
+
params={
|
|
535
|
+
"track_index": bass_idx,
|
|
536
|
+
"device_name": "Compressor",
|
|
537
|
+
},
|
|
538
|
+
description=f"Insert Compressor on {bass['name']} for kick sidechain",
|
|
539
|
+
))
|
|
540
|
+
descriptions.append(f"Compressor on {bass['name']}")
|
|
541
|
+
warnings.append(
|
|
542
|
+
f"After inserting the Compressor on {bass['name']}, enable "
|
|
543
|
+
f"'S/C On' and route {kick['name']} (Post FX) into its "
|
|
544
|
+
"sidechain — device index is not resolvable at plan time."
|
|
545
|
+
)
|
|
329
546
|
|
|
330
547
|
steps.append(CompiledStep(
|
|
331
548
|
tool="get_track_meters",
|
|
332
549
|
params={"include_stereo": True},
|
|
333
|
-
description="Verify kick and bass both still producing audio",
|
|
550
|
+
description="Verify kick and bass both still producing audio after carve",
|
|
334
551
|
))
|
|
335
552
|
|
|
336
553
|
return CompiledPlan(
|
|
@@ -367,23 +584,31 @@ def _compile_create_buildup_tension(move: SemanticMove, kernel: dict) -> Compile
|
|
|
367
584
|
if not perc_tracks and not harmony_tracks:
|
|
368
585
|
warnings.append("No percussion or harmony tracks found — cannot build tension")
|
|
369
586
|
|
|
370
|
-
# Raise perc for energy
|
|
587
|
+
# Raise perc for energy — RELATIVE nudge (P2-21), capped so an already
|
|
588
|
+
# hot perc bus doesn't clip.
|
|
371
589
|
for pt in perc_tracks[:1]:
|
|
590
|
+
target = resolvers.compile_relative_volume(
|
|
591
|
+
pt.get("volume"), 10, cap=0.90, fallback=0.78
|
|
592
|
+
)
|
|
372
593
|
steps.append(CompiledStep(
|
|
373
594
|
tool="set_track_volume",
|
|
374
|
-
params={"track_index": pt["index"], "volume":
|
|
375
|
-
description=f"Push {pt['name']} to
|
|
595
|
+
params={"track_index": pt["index"], "volume": target},
|
|
596
|
+
description=f"Push {pt['name']} to {target:.2f} for rising energy",
|
|
376
597
|
))
|
|
377
|
-
descriptions.append(f"Push {pt['name']} to
|
|
598
|
+
descriptions.append(f"Push {pt['name']} to {target:.2f}")
|
|
378
599
|
|
|
379
|
-
# Pull harmony slightly to amplify perc contrast
|
|
600
|
+
# Pull harmony slightly to amplify perc contrast — RELATIVE nudge,
|
|
601
|
+
# floored so a quiet harmony bed doesn't get pulled to silence.
|
|
380
602
|
for ht in harmony_tracks[:1]:
|
|
603
|
+
target = resolvers.compile_relative_volume(
|
|
604
|
+
ht.get("volume"), -12, floor=0.20, fallback=0.35
|
|
605
|
+
)
|
|
381
606
|
steps.append(CompiledStep(
|
|
382
607
|
tool="set_track_volume",
|
|
383
|
-
params={"track_index": ht["index"], "volume":
|
|
384
|
-
description=f"Pull {ht['name']} to
|
|
608
|
+
params={"track_index": ht["index"], "volume": target},
|
|
609
|
+
description=f"Pull {ht['name']} to {target:.2f} to create harmonic vacuum before drop",
|
|
385
610
|
))
|
|
386
|
-
descriptions.append(f"Pull {ht['name']} to
|
|
611
|
+
descriptions.append(f"Pull {ht['name']} to {target:.2f}")
|
|
387
612
|
|
|
388
613
|
steps.append(CompiledStep(
|
|
389
614
|
tool="get_track_meters",
|
|
@@ -19,21 +19,33 @@ def _compile_recover_energy(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
|
19
19
|
drum_tracks = resolvers.find_tracks_by_role(kernel, ["drums", "percussion"])
|
|
20
20
|
bass_tracks = resolvers.find_tracks_by_role(kernel, ["bass"])
|
|
21
21
|
|
|
22
|
+
# RELATIVE nudge (P2-21 pattern, ported from resolvers.compile_relative_volume
|
|
23
|
+
# / mix_compilers.py): "recover" means push UP from wherever the track
|
|
24
|
+
# already sits, not overwrite it with a flat absolute level — a drum bus
|
|
25
|
+
# already sitting hot must not get pulled DOWN to 0.70. Live-set-safe:
|
|
26
|
+
# small +12% nudge, capped just above the historical fallback so a
|
|
27
|
+
# recovering track lands in the same ballpark it always did.
|
|
22
28
|
for dt in drum_tracks[:1]:
|
|
29
|
+
target = resolvers.compile_relative_volume(
|
|
30
|
+
dt.get("volume"), 12, cap=0.78, fallback=0.70
|
|
31
|
+
)
|
|
23
32
|
steps.append(CompiledStep(
|
|
24
33
|
tool="set_track_volume",
|
|
25
|
-
params={"track_index": dt["index"], "volume":
|
|
26
|
-
description=f"Restore {dt['name']} to
|
|
34
|
+
params={"track_index": dt["index"], "volume": target},
|
|
35
|
+
description=f"Restore {dt['name']} to {target:.2f} for energy recovery",
|
|
27
36
|
))
|
|
28
|
-
descriptions.append(f"Restore {dt['name']} to
|
|
37
|
+
descriptions.append(f"Restore {dt['name']} to {target:.2f}")
|
|
29
38
|
|
|
30
39
|
for bt in bass_tracks[:1]:
|
|
40
|
+
target = resolvers.compile_relative_volume(
|
|
41
|
+
bt.get("volume"), 12, cap=0.68, fallback=0.60
|
|
42
|
+
)
|
|
31
43
|
steps.append(CompiledStep(
|
|
32
44
|
tool="set_track_volume",
|
|
33
|
-
params={"track_index": bt["index"], "volume":
|
|
34
|
-
description=f"Restore {bt['name']} to
|
|
45
|
+
params={"track_index": bt["index"], "volume": target},
|
|
46
|
+
description=f"Restore {bt['name']} to {target:.2f}",
|
|
35
47
|
))
|
|
36
|
-
descriptions.append(f"Restore {bt['name']} to
|
|
48
|
+
descriptions.append(f"Restore {bt['name']} to {target:.2f}")
|
|
37
49
|
|
|
38
50
|
# Pull reverb back to tighten
|
|
39
51
|
pad_tracks = resolvers.find_tracks_by_role(kernel, ["pad", "chords"])
|
|
@@ -69,13 +81,19 @@ def _compile_decompress_tension(move: SemanticMove, kernel: dict) -> CompiledPla
|
|
|
69
81
|
lead_tracks = resolvers.find_tracks_by_role(kernel, ["lead", "chords"])
|
|
70
82
|
pad_tracks = resolvers.find_tracks_by_role(kernel, ["pad"])
|
|
71
83
|
|
|
84
|
+
# RELATIVE nudge (P2-21 pattern) — "decompress" means pull DOWN from the
|
|
85
|
+
# current level, floored so an already-quiet lead doesn't get pulled
|
|
86
|
+
# toward silence. Small -12% nudge, live-set-safe.
|
|
72
87
|
for lt in lead_tracks[:2]:
|
|
88
|
+
target = resolvers.compile_relative_volume(
|
|
89
|
+
lt.get("volume"), -12, floor=0.20, fallback=0.35
|
|
90
|
+
)
|
|
73
91
|
steps.append(CompiledStep(
|
|
74
92
|
tool="set_track_volume",
|
|
75
|
-
params={"track_index": lt["index"], "volume":
|
|
76
|
-
description=f"Pull {lt['name']} to
|
|
93
|
+
params={"track_index": lt["index"], "volume": target},
|
|
94
|
+
description=f"Pull {lt['name']} to {target:.2f} for decompression",
|
|
77
95
|
))
|
|
78
|
-
descriptions.append(f"Pull {lt['name']} to
|
|
96
|
+
descriptions.append(f"Pull {lt['name']} to {target:.2f}")
|
|
79
97
|
|
|
80
98
|
for pt in pad_tracks[:1]:
|
|
81
99
|
steps.append(CompiledStep(
|
|
@@ -121,7 +139,9 @@ def _compile_safe_spotlight(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
|
121
139
|
spotlight_idx = spotlight.get("index", 0)
|
|
122
140
|
spotlight_name = spotlight.get("name", f"Track {spotlight_idx}")
|
|
123
141
|
|
|
124
|
-
# Pull non-spotlight audio tracks
|
|
142
|
+
# Pull non-spotlight audio tracks — RELATIVE nudge (P2-21 pattern),
|
|
143
|
+
# floored so an already-quiet background track doesn't get pulled
|
|
144
|
+
# toward silence.
|
|
125
145
|
for track in all_tracks:
|
|
126
146
|
idx = track.get("index", 0)
|
|
127
147
|
name = track.get("name", "")
|
|
@@ -129,17 +149,24 @@ def _compile_safe_spotlight(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
|
129
149
|
continue
|
|
130
150
|
if track.get("type") in ("return", "master"):
|
|
131
151
|
continue
|
|
152
|
+
target = resolvers.compile_relative_volume(
|
|
153
|
+
track.get("volume"), -15, floor=0.15, fallback=0.30
|
|
154
|
+
)
|
|
132
155
|
steps.append(CompiledStep(
|
|
133
156
|
tool="set_track_volume",
|
|
134
|
-
params={"track_index": idx, "volume":
|
|
135
|
-
description=f"Pull {name} to
|
|
157
|
+
params={"track_index": idx, "volume": target},
|
|
158
|
+
description=f"Pull {name} to {target:.2f} (background)",
|
|
136
159
|
))
|
|
137
160
|
|
|
138
|
-
# Push spotlight
|
|
161
|
+
# Push spotlight — RELATIVE nudge, capped just above the historical
|
|
162
|
+
# fallback so the spotlight lands in the same ballpark it always did.
|
|
163
|
+
spotlight_target = resolvers.compile_relative_volume(
|
|
164
|
+
spotlight.get("volume"), 10, cap=0.85, fallback=0.82
|
|
165
|
+
)
|
|
139
166
|
steps.append(CompiledStep(
|
|
140
167
|
tool="set_track_volume",
|
|
141
|
-
params={"track_index": spotlight_idx, "volume":
|
|
142
|
-
description=f"Push spotlight {spotlight_name} to
|
|
168
|
+
params={"track_index": spotlight_idx, "volume": spotlight_target},
|
|
169
|
+
description=f"Push spotlight {spotlight_name} to {spotlight_target:.2f}",
|
|
143
170
|
))
|
|
144
171
|
descriptions.append(f"Spotlight {spotlight_name}")
|
|
145
172
|
|
|
@@ -176,10 +203,17 @@ def _compile_emergency_simplify(move: SemanticMove, kernel: dict) -> CompiledPla
|
|
|
176
203
|
continue
|
|
177
204
|
if idx in keep_indices:
|
|
178
205
|
continue
|
|
206
|
+
# RELATIVE nudge (P2-21 pattern) — "emergency simplify" genuinely
|
|
207
|
+
# means strip aggressively, so the delta is intentionally large
|
|
208
|
+
# (-25%), but still bounded (floor=0.05) and direction-safe: a
|
|
209
|
+
# track already quieter than 0.05 doesn't get nudged back UP.
|
|
210
|
+
target = resolvers.compile_relative_volume(
|
|
211
|
+
track.get("volume"), -25, floor=0.05, fallback=0.10
|
|
212
|
+
)
|
|
179
213
|
steps.append(CompiledStep(
|
|
180
214
|
tool="set_track_volume",
|
|
181
|
-
params={"track_index": idx, "volume":
|
|
182
|
-
description=f"Strip {name} to
|
|
215
|
+
params={"track_index": idx, "volume": target},
|
|
216
|
+
description=f"Strip {name} to {target:.2f} (emergency simplify)",
|
|
183
217
|
))
|
|
184
218
|
|
|
185
219
|
descriptions.append("Strip to drums+bass only")
|
|
@@ -124,3 +124,48 @@ def adjust_volume(current: float, delta_percent: float) -> float:
|
|
|
124
124
|
"""Adjust a volume by a percentage. delta_percent=5 means +5%."""
|
|
125
125
|
new = current + (delta_percent / 100.0)
|
|
126
126
|
return clamp_volume(new)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def compile_relative_volume(
|
|
130
|
+
current: Optional[float],
|
|
131
|
+
delta_percent: float,
|
|
132
|
+
*,
|
|
133
|
+
cap: Optional[float] = None,
|
|
134
|
+
floor: Optional[float] = None,
|
|
135
|
+
fallback: float,
|
|
136
|
+
) -> float:
|
|
137
|
+
"""Compile a bounded RELATIVE volume nudge from a track's current level.
|
|
138
|
+
|
|
139
|
+
P2-21 fix: mix/sound-design/transition compilers used to write an
|
|
140
|
+
ABSOLUTE volume regardless of what was already on the track — e.g.
|
|
141
|
+
"make it punchier" could turn a hot drum bus DOWN by blindly writing
|
|
142
|
+
a flat 0.75. This computes current + delta_percent% instead (via
|
|
143
|
+
adjust_volume), then clamps to an optional cap/floor so the move's
|
|
144
|
+
musical intent (push toward louder / pull toward quieter) still holds
|
|
145
|
+
even when starting from an unusual level.
|
|
146
|
+
|
|
147
|
+
`current` is None when the connected Remote Script predates the
|
|
148
|
+
`volume` field in get_session_info (see transport.py get_session_info)
|
|
149
|
+
— in that case this falls back to the historical absolute value so
|
|
150
|
+
the compiler degrades gracefully instead of crashing or guessing.
|
|
151
|
+
|
|
152
|
+
The cap/floor bound the SIZE of the nudge — they must never flip its
|
|
153
|
+
DIRECTION. A track already louder than `cap` must not get pulled back
|
|
154
|
+
down to the cap when the move means to push it UP (that would
|
|
155
|
+
reintroduce the P2-21 bug in a smaller way); symmetrically a track
|
|
156
|
+
already quieter than `floor` must not get nudged back UP when the
|
|
157
|
+
move means to pull it DOWN. So after clamping to [floor, cap], the
|
|
158
|
+
result is re-clamped against `current` in the direction of travel.
|
|
159
|
+
"""
|
|
160
|
+
if current is None:
|
|
161
|
+
return fallback
|
|
162
|
+
target = adjust_volume(current, delta_percent)
|
|
163
|
+
if cap is not None:
|
|
164
|
+
target = min(target, cap)
|
|
165
|
+
if floor is not None:
|
|
166
|
+
target = max(target, floor)
|
|
167
|
+
if delta_percent > 0:
|
|
168
|
+
target = max(target, current)
|
|
169
|
+
elif delta_percent < 0:
|
|
170
|
+
target = min(target, current)
|
|
171
|
+
return clamp_volume(target)
|
|
@@ -33,11 +33,15 @@ def _compile_add_warmth(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
|
33
33
|
idx = t["index"]
|
|
34
34
|
name = t["name"]
|
|
35
35
|
|
|
36
|
-
# Boost volume slightly for perceived warmth
|
|
36
|
+
# Boost volume slightly for perceived warmth — RELATIVE nudge
|
|
37
|
+
# (P2-21), capped so an already-hot track isn't pushed toward clip.
|
|
38
|
+
target = resolvers.compile_relative_volume(
|
|
39
|
+
t.get("volume"), 6, cap=0.80, fallback=0.65
|
|
40
|
+
)
|
|
37
41
|
steps.append(CompiledStep(
|
|
38
42
|
tool="set_track_volume",
|
|
39
|
-
params={"track_index": idx, "volume":
|
|
40
|
-
description=f"Boost {name}
|
|
43
|
+
params={"track_index": idx, "volume": target},
|
|
44
|
+
description=f"Boost {name} to {target:.2f} for warmth",
|
|
41
45
|
))
|
|
42
46
|
|
|
43
47
|
# Add reverb send for depth/warmth perception
|
|
@@ -138,11 +142,15 @@ def _compile_shape_transients(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
|
138
142
|
idx = dt["index"]
|
|
139
143
|
name = dt["name"]
|
|
140
144
|
|
|
141
|
-
# Push volume for transient punch
|
|
145
|
+
# Push volume for transient punch — RELATIVE nudge (P2-21), capped
|
|
146
|
+
# so an already-hot drum bus isn't pushed toward clip.
|
|
147
|
+
target = resolvers.compile_relative_volume(
|
|
148
|
+
dt.get("volume"), 8, cap=0.85, fallback=0.75
|
|
149
|
+
)
|
|
142
150
|
steps.append(CompiledStep(
|
|
143
151
|
tool="set_track_volume",
|
|
144
|
-
params={"track_index": idx, "volume":
|
|
145
|
-
description=f"Push {name} to
|
|
152
|
+
params={"track_index": idx, "volume": target},
|
|
153
|
+
description=f"Push {name} to {target:.2f} for transient punch",
|
|
146
154
|
))
|
|
147
155
|
descriptions.append(f"Push {name} for punch")
|
|
148
156
|
|