livepilot 1.12.2 → 1.14.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.
- package/CHANGELOG.md +219 -0
- package/README.md +7 -7
- 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/branches/__init__.py +34 -0
- package/mcp_server/branches/types.py +286 -0
- package/mcp_server/composer/__init__.py +10 -1
- package/mcp_server/composer/branch_producer.py +349 -0
- package/mcp_server/composer/tools.py +58 -1
- package/mcp_server/evaluation/policy.py +227 -2
- package/mcp_server/experiment/engine.py +47 -11
- package/mcp_server/experiment/models.py +112 -8
- package/mcp_server/experiment/tools.py +502 -38
- package/mcp_server/memory/taste_graph.py +84 -11
- package/mcp_server/persistence/taste_store.py +21 -5
- package/mcp_server/runtime/session_kernel.py +46 -0
- package/mcp_server/runtime/tools.py +29 -3
- package/mcp_server/server.py +1 -0
- package/mcp_server/synthesis_brain/__init__.py +53 -0
- package/mcp_server/synthesis_brain/adapters/__init__.py +34 -0
- package/mcp_server/synthesis_brain/adapters/analog.py +273 -0
- package/mcp_server/synthesis_brain/adapters/base.py +86 -0
- package/mcp_server/synthesis_brain/adapters/drift.py +271 -0
- package/mcp_server/synthesis_brain/adapters/meld.py +261 -0
- package/mcp_server/synthesis_brain/adapters/operator.py +292 -0
- package/mcp_server/synthesis_brain/adapters/wavetable.py +364 -0
- package/mcp_server/synthesis_brain/engine.py +91 -0
- package/mcp_server/synthesis_brain/models.py +121 -0
- package/mcp_server/synthesis_brain/timbre.py +194 -0
- package/mcp_server/synthesis_brain/tools.py +231 -0
- package/mcp_server/tools/_conductor.py +144 -0
- package/mcp_server/wonder_mode/engine.py +324 -0
- package/mcp_server/wonder_mode/tools.py +153 -1
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +3 -3
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Analog adapter — Ableton's classic two-oscillator subtractive synth.
|
|
2
|
+
|
|
3
|
+
PR10 ships one canned proposer: filter_envelope_variant — pushes Filter
|
|
4
|
+
Envelope Amount while shortening the Filter Decay, producing the
|
|
5
|
+
characteristic "plucked" attack that Analog excels at. Later PRs add
|
|
6
|
+
detune/unison variants and dual-filter variants.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from ...branches import BranchSeed, freeform_seed
|
|
15
|
+
from ..models import (
|
|
16
|
+
SynthProfile,
|
|
17
|
+
TimbralFingerprint,
|
|
18
|
+
ModulationGraph,
|
|
19
|
+
ArticulationProfile,
|
|
20
|
+
NATIVE,
|
|
21
|
+
)
|
|
22
|
+
from .base import register_adapter
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_KNOWN_PARAMS = {
|
|
26
|
+
"Osc1 Shape",
|
|
27
|
+
"Osc2 Shape",
|
|
28
|
+
"Osc1 Tune",
|
|
29
|
+
"Osc2 Tune",
|
|
30
|
+
"F1 Freq",
|
|
31
|
+
"F1 Reso",
|
|
32
|
+
"F1 Env Amount",
|
|
33
|
+
"F1 Env A",
|
|
34
|
+
"F1 Env D",
|
|
35
|
+
"F1 Env S",
|
|
36
|
+
"F1 Env R",
|
|
37
|
+
"A1 Attack",
|
|
38
|
+
"A1 Decay",
|
|
39
|
+
"A1 Sustain",
|
|
40
|
+
"A1 Release",
|
|
41
|
+
"Glide Mode",
|
|
42
|
+
"Glide Time",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@register_adapter
|
|
47
|
+
class AnalogAdapter:
|
|
48
|
+
device_name: str = "Analog"
|
|
49
|
+
|
|
50
|
+
def extract_profile(
|
|
51
|
+
self,
|
|
52
|
+
track_index: int,
|
|
53
|
+
device_index: int,
|
|
54
|
+
parameter_state: dict,
|
|
55
|
+
display_values: Optional[dict] = None,
|
|
56
|
+
role_hint: str = "",
|
|
57
|
+
) -> SynthProfile:
|
|
58
|
+
notes: list[str] = []
|
|
59
|
+
|
|
60
|
+
# Filter-env coupling summary
|
|
61
|
+
env_amount = parameter_state.get("F1 Env Amount", 0.0)
|
|
62
|
+
env_decay = parameter_state.get("F1 Env D", 0.0)
|
|
63
|
+
if env_amount and abs(env_amount) > 0.3 and env_decay and env_decay < 0.3:
|
|
64
|
+
notes.append(
|
|
65
|
+
f"Already plucky: F1 Env Amount={env_amount:.2f}, Decay={env_decay:.2f}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
articulation = ArticulationProfile(
|
|
69
|
+
attack_ms=float(parameter_state.get("A1 Attack", 0.0) or 0.0),
|
|
70
|
+
release_ms=float(parameter_state.get("A1 Release", 0.0) or 0.0),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
mod = ModulationGraph()
|
|
74
|
+
if env_amount and abs(env_amount) > 0.01:
|
|
75
|
+
mod.routes.append({
|
|
76
|
+
"source": "Filter Env",
|
|
77
|
+
"target": "F1 Freq",
|
|
78
|
+
"amount": env_amount,
|
|
79
|
+
"range": None,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
focused_state = {k: v for k, v in parameter_state.items() if k in _KNOWN_PARAMS}
|
|
83
|
+
focused_display = (
|
|
84
|
+
{k: v for k, v in (display_values or {}).items() if k in _KNOWN_PARAMS}
|
|
85
|
+
if display_values else {}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return SynthProfile(
|
|
89
|
+
device_name=self.device_name,
|
|
90
|
+
opacity=NATIVE,
|
|
91
|
+
track_index=track_index,
|
|
92
|
+
device_index=device_index,
|
|
93
|
+
parameter_state=focused_state,
|
|
94
|
+
display_values=focused_display,
|
|
95
|
+
role_hint=role_hint,
|
|
96
|
+
modulation=mod,
|
|
97
|
+
articulation=articulation,
|
|
98
|
+
notes=notes,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def propose_branches(
|
|
102
|
+
self,
|
|
103
|
+
profile: SynthProfile,
|
|
104
|
+
target: TimbralFingerprint,
|
|
105
|
+
kernel: Optional[dict] = None,
|
|
106
|
+
) -> list[tuple[BranchSeed, dict]]:
|
|
107
|
+
# Strategy registry — each candidate strategy's ``applicable()``
|
|
108
|
+
# gates on profile+role+target, and ``build()`` emits (seed, plan).
|
|
109
|
+
# Adapter returns ALL applicable strategies' proposals so Wonder /
|
|
110
|
+
# create_experiment can offer them as branches. Callers cap total
|
|
111
|
+
# via max_seeds.
|
|
112
|
+
kernel = kernel or {}
|
|
113
|
+
results: list[tuple[BranchSeed, dict]] = []
|
|
114
|
+
for strategy_fn in (_strategy_filter_pluck, _strategy_detune_warmth):
|
|
115
|
+
try:
|
|
116
|
+
maybe = strategy_fn(profile, target, kernel, adapter=self)
|
|
117
|
+
except Exception:
|
|
118
|
+
# Never let one strategy's crash kill the rest.
|
|
119
|
+
continue
|
|
120
|
+
if maybe is not None:
|
|
121
|
+
results.append(maybe)
|
|
122
|
+
return results
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ── Strategy registry ────────────────────────────────────────────────
|
|
126
|
+
#
|
|
127
|
+
# Each strategy is a pure function (profile, target, kernel, adapter) →
|
|
128
|
+
# Optional[(BranchSeed, plan_dict)]. A strategy returns None when not
|
|
129
|
+
# applicable to the current profile+target+role combination. This lets
|
|
130
|
+
# the adapter stay thin while the intelligence lives in the strategies.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _strategy_filter_pluck(
|
|
134
|
+
profile: SynthProfile,
|
|
135
|
+
target: TimbralFingerprint,
|
|
136
|
+
kernel: dict,
|
|
137
|
+
adapter,
|
|
138
|
+
) -> Optional[tuple[BranchSeed, dict]]:
|
|
139
|
+
"""Couple Filter Env Amount up + Filter Decay down → attack pluck.
|
|
140
|
+
|
|
141
|
+
Gates: skip when profile already flags 'Already plucky'. Most useful
|
|
142
|
+
when role_hint is "bass", "pluck", "lead", or when target.bite > 0.
|
|
143
|
+
"""
|
|
144
|
+
if any("Already plucky" in n for n in profile.notes):
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
role = (profile.role_hint or "").lower()
|
|
148
|
+
want_bite = target.bite > 0.1 or role in {"bass", "pluck", "lead", "stab"}
|
|
149
|
+
if not want_bite and role in {"pad", "drone"}:
|
|
150
|
+
# Sustained roles actively fight this strategy.
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
freshness = float(kernel.get("freshness", 0.5) or 0.5)
|
|
154
|
+
track = profile.track_index
|
|
155
|
+
device = profile.device_index
|
|
156
|
+
current_env = float(profile.parameter_state.get("F1 Env Amount", 0.0) or 0.0)
|
|
157
|
+
new_env = min(1.0, max(current_env, 0.45 if freshness < 0.5 else 0.65))
|
|
158
|
+
current_decay = float(profile.parameter_state.get("F1 Env D", 0.5) or 0.5)
|
|
159
|
+
new_decay = min(current_decay, 0.25 if freshness < 0.5 else 0.15)
|
|
160
|
+
|
|
161
|
+
seed = freeform_seed(
|
|
162
|
+
seed_id=_short_id("an_plk", f"{track}:{device}:{new_env:.2f}:{new_decay:.2f}"),
|
|
163
|
+
hypothesis=(
|
|
164
|
+
f"Analog filter-pluck: Env Amount → {new_env:.2f}, "
|
|
165
|
+
f"Decay → {new_decay:.2f} for attack character"
|
|
166
|
+
),
|
|
167
|
+
source="synthesis",
|
|
168
|
+
novelty_label="strong" if freshness < 0.7 else "unexpected",
|
|
169
|
+
risk_label="low",
|
|
170
|
+
affected_scope={
|
|
171
|
+
"track_indices": [track],
|
|
172
|
+
"device_paths": [f"track/{track}/device/{device}"],
|
|
173
|
+
},
|
|
174
|
+
distinctness_reason="couples Filter Env Amount + Decay for attack character",
|
|
175
|
+
producer_payload={
|
|
176
|
+
"device_name": adapter.device_name,
|
|
177
|
+
"track_index": track,
|
|
178
|
+
"device_index": device,
|
|
179
|
+
"strategy": "filter_pluck",
|
|
180
|
+
"topology_hint": {
|
|
181
|
+
"role_hint": profile.role_hint,
|
|
182
|
+
"current_env": current_env,
|
|
183
|
+
"new_env": new_env,
|
|
184
|
+
"current_decay": current_decay,
|
|
185
|
+
"new_decay": new_decay,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
plan = {
|
|
190
|
+
"steps": [
|
|
191
|
+
{"tool": "set_device_parameter",
|
|
192
|
+
"params": {"track_index": track, "device_index": device,
|
|
193
|
+
"parameter_name": "F1 Env Amount",
|
|
194
|
+
"value": round(new_env, 3)}},
|
|
195
|
+
{"tool": "set_device_parameter",
|
|
196
|
+
"params": {"track_index": track, "device_index": device,
|
|
197
|
+
"parameter_name": "F1 Env D",
|
|
198
|
+
"value": round(new_decay, 3)}},
|
|
199
|
+
],
|
|
200
|
+
"step_count": 2,
|
|
201
|
+
"summary": f"F1 Env Amount → {new_env:.2f}, F1 Env D → {new_decay:.2f}",
|
|
202
|
+
}
|
|
203
|
+
return (seed, plan)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _strategy_detune_warmth(
|
|
207
|
+
profile: SynthProfile,
|
|
208
|
+
target: TimbralFingerprint,
|
|
209
|
+
kernel: dict,
|
|
210
|
+
adapter,
|
|
211
|
+
) -> Optional[tuple[BranchSeed, dict]]:
|
|
212
|
+
"""Detune Osc2 slightly + lean warmer tone.
|
|
213
|
+
|
|
214
|
+
Gates: applicable when role_hint is "pad" / "lead" / "stab" / "drone"
|
|
215
|
+
or target.warmth > 0. Skip on "pluck"/"bass" to avoid woofiness.
|
|
216
|
+
"""
|
|
217
|
+
role = (profile.role_hint or "").lower()
|
|
218
|
+
if role in {"bass", "pluck", "kick"}:
|
|
219
|
+
return None
|
|
220
|
+
want_warm = target.warmth > 0.1 or role in {"pad", "lead", "stab", "drone"}
|
|
221
|
+
if not want_warm:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
freshness = float(kernel.get("freshness", 0.5) or 0.5)
|
|
225
|
+
track = profile.track_index
|
|
226
|
+
device = profile.device_index
|
|
227
|
+
current_detune = float(profile.parameter_state.get("Osc2 Tune", 0.0) or 0.0)
|
|
228
|
+
# Detune is in semitones by convention on Analog; keep shifts musical.
|
|
229
|
+
step = 0.04 if freshness < 0.5 else 0.09
|
|
230
|
+
new_detune = round(current_detune + step, 3)
|
|
231
|
+
|
|
232
|
+
seed = freeform_seed(
|
|
233
|
+
seed_id=_short_id("an_det", f"{track}:{device}:{new_detune:.3f}"),
|
|
234
|
+
hypothesis=(
|
|
235
|
+
f"Analog detune warmth: Osc2 Tune {current_detune:.3f} → "
|
|
236
|
+
f"{new_detune:.3f} semitones for a wider, lusher body"
|
|
237
|
+
),
|
|
238
|
+
source="synthesis",
|
|
239
|
+
novelty_label="safe",
|
|
240
|
+
risk_label="low",
|
|
241
|
+
affected_scope={
|
|
242
|
+
"track_indices": [track],
|
|
243
|
+
"device_paths": [f"track/{track}/device/{device}"],
|
|
244
|
+
},
|
|
245
|
+
distinctness_reason="slight Osc2 detune for body, no filter changes",
|
|
246
|
+
producer_payload={
|
|
247
|
+
"device_name": adapter.device_name,
|
|
248
|
+
"track_index": track,
|
|
249
|
+
"device_index": device,
|
|
250
|
+
"strategy": "detune_warmth",
|
|
251
|
+
"topology_hint": {
|
|
252
|
+
"role_hint": profile.role_hint,
|
|
253
|
+
"current_detune": current_detune,
|
|
254
|
+
"new_detune": new_detune,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
plan = {
|
|
259
|
+
"steps": [
|
|
260
|
+
{"tool": "set_device_parameter",
|
|
261
|
+
"params": {"track_index": track, "device_index": device,
|
|
262
|
+
"parameter_name": "Osc2 Tune",
|
|
263
|
+
"value": new_detune}},
|
|
264
|
+
],
|
|
265
|
+
"step_count": 1,
|
|
266
|
+
"summary": f"Osc2 Tune → {new_detune:.3f}",
|
|
267
|
+
}
|
|
268
|
+
return (seed, plan)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _short_id(prefix: str, key: str) -> str:
|
|
272
|
+
h = hashlib.sha256(f"{prefix}:{key}".encode()).hexdigest()[:10]
|
|
273
|
+
return f"{prefix}_{h}"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""SynthAdapter base protocol + registry infrastructure.
|
|
2
|
+
|
|
3
|
+
Adapters are plain classes that expose:
|
|
4
|
+
- device_name (class attribute)
|
|
5
|
+
- extract_profile(track_index, device_index, parameter_state, display_values,
|
|
6
|
+
role_hint) -> SynthProfile
|
|
7
|
+
- propose_branches(profile, target, kernel) -> list[tuple[BranchSeed, dict]]
|
|
8
|
+
(each tuple: (seed, compiled_plan_dict))
|
|
9
|
+
|
|
10
|
+
Adapters register themselves via ``register_adapter(cls)`` as a class
|
|
11
|
+
decorator at module-import time, so the registry is populated when
|
|
12
|
+
the adapters package is imported.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Protocol, runtime_checkable, Optional
|
|
18
|
+
|
|
19
|
+
from ...branches import BranchSeed
|
|
20
|
+
from ..models import SynthProfile, TimbralFingerprint
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Adapter registry — populated by register_adapter at import time.
|
|
24
|
+
_REGISTRY: dict[str, "SynthAdapter"] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@runtime_checkable
|
|
28
|
+
class SynthAdapter(Protocol):
|
|
29
|
+
"""Contract every native-synth adapter must satisfy."""
|
|
30
|
+
|
|
31
|
+
device_name: str
|
|
32
|
+
|
|
33
|
+
def extract_profile(
|
|
34
|
+
self,
|
|
35
|
+
track_index: int,
|
|
36
|
+
device_index: int,
|
|
37
|
+
parameter_state: dict,
|
|
38
|
+
display_values: Optional[dict] = None,
|
|
39
|
+
role_hint: str = "",
|
|
40
|
+
) -> SynthProfile:
|
|
41
|
+
"""Build a SynthProfile from raw device parameter state.
|
|
42
|
+
|
|
43
|
+
Must be pure — no I/O. The caller has already fetched parameters
|
|
44
|
+
via get_device_parameters / get_display_values and hands them here.
|
|
45
|
+
"""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def propose_branches(
|
|
49
|
+
self,
|
|
50
|
+
profile: SynthProfile,
|
|
51
|
+
target: TimbralFingerprint,
|
|
52
|
+
kernel: Optional[dict] = None,
|
|
53
|
+
) -> list[tuple[BranchSeed, dict]]:
|
|
54
|
+
"""Emit seed + pre-compiled plan pairs.
|
|
55
|
+
|
|
56
|
+
Each pair: (BranchSeed with source="synthesis", compiled_plan dict
|
|
57
|
+
ready for execution_router.execute_plan_steps_async).
|
|
58
|
+
|
|
59
|
+
The kernel dict may carry freshness / creativity_profile / synth_hints
|
|
60
|
+
(see SessionKernel PR2 additions); adapters read these to bias
|
|
61
|
+
proposals. Pre-PR10, adapters ship canned proposers; later PRs
|
|
62
|
+
extend with render-based verification.
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def register_adapter(cls):
|
|
68
|
+
"""Class decorator — register an adapter under its device_name.
|
|
69
|
+
|
|
70
|
+
Raises ValueError if the class lacks device_name or duplicates an
|
|
71
|
+
existing registration.
|
|
72
|
+
"""
|
|
73
|
+
device_name = getattr(cls, "device_name", None)
|
|
74
|
+
if not device_name:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Adapter {cls.__name__} must define a class attribute "
|
|
77
|
+
f"'device_name' to register"
|
|
78
|
+
)
|
|
79
|
+
if device_name in _REGISTRY:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Duplicate synth adapter registration for device '{device_name}' "
|
|
82
|
+
f"(existing: {type(_REGISTRY[device_name]).__name__}, "
|
|
83
|
+
f"new: {cls.__name__})"
|
|
84
|
+
)
|
|
85
|
+
_REGISTRY[device_name] = cls()
|
|
86
|
+
return cls
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Drift adapter — Ableton 12's modern subtractive synth.
|
|
2
|
+
|
|
3
|
+
Drift has a cleaner parameter set than Analog and pairs oscillator
|
|
4
|
+
shapes with a character wave + sub + noise blend. PR10 ships one
|
|
5
|
+
canned proposer: character_blend — shifts the oscillator wave + sub
|
|
6
|
+
balance to change core tone without touching the filter. Later PRs
|
|
7
|
+
add tuning-table variants and LFO-routing variants.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from ...branches import BranchSeed, freeform_seed
|
|
16
|
+
from ..models import (
|
|
17
|
+
SynthProfile,
|
|
18
|
+
TimbralFingerprint,
|
|
19
|
+
ModulationGraph,
|
|
20
|
+
ArticulationProfile,
|
|
21
|
+
NATIVE,
|
|
22
|
+
)
|
|
23
|
+
from .base import register_adapter
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_KNOWN_PARAMS = {
|
|
27
|
+
"Wave",
|
|
28
|
+
"Character",
|
|
29
|
+
"Tune",
|
|
30
|
+
"Sub Level",
|
|
31
|
+
"Sub Tone",
|
|
32
|
+
"Noise Level",
|
|
33
|
+
"Noise Color",
|
|
34
|
+
"Filter Freq",
|
|
35
|
+
"Filter Res",
|
|
36
|
+
"Filter Env",
|
|
37
|
+
"LFO Rate",
|
|
38
|
+
"LFO Amount",
|
|
39
|
+
"Amp Env A",
|
|
40
|
+
"Amp Env D",
|
|
41
|
+
"Amp Env S",
|
|
42
|
+
"Amp Env R",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@register_adapter
|
|
47
|
+
class DriftAdapter:
|
|
48
|
+
device_name: str = "Drift"
|
|
49
|
+
|
|
50
|
+
def extract_profile(
|
|
51
|
+
self,
|
|
52
|
+
track_index: int,
|
|
53
|
+
device_index: int,
|
|
54
|
+
parameter_state: dict,
|
|
55
|
+
display_values: Optional[dict] = None,
|
|
56
|
+
role_hint: str = "",
|
|
57
|
+
) -> SynthProfile:
|
|
58
|
+
notes: list[str] = []
|
|
59
|
+
|
|
60
|
+
sub = float(parameter_state.get("Sub Level", 0.0) or 0.0)
|
|
61
|
+
noise = float(parameter_state.get("Noise Level", 0.0) or 0.0)
|
|
62
|
+
if sub > 0.5 and role_hint in ("lead", "stab"):
|
|
63
|
+
notes.append(f"Sub Level {sub:.2f} is high for a {role_hint} — check bass clash")
|
|
64
|
+
if noise > 0.5:
|
|
65
|
+
notes.append(f"Noise Level {noise:.2f} — significant noise content")
|
|
66
|
+
|
|
67
|
+
articulation = ArticulationProfile(
|
|
68
|
+
attack_ms=float(parameter_state.get("Amp Env A", 0.0) or 0.0),
|
|
69
|
+
release_ms=float(parameter_state.get("Amp Env R", 0.0) or 0.0),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
mod = ModulationGraph()
|
|
73
|
+
lfo_amount = parameter_state.get("LFO Amount", 0.0)
|
|
74
|
+
if lfo_amount and abs(lfo_amount) > 0.01:
|
|
75
|
+
mod.routes.append({
|
|
76
|
+
"source": "LFO",
|
|
77
|
+
"target": "(inferred)",
|
|
78
|
+
"amount": lfo_amount,
|
|
79
|
+
"range": None,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
focused_state = {k: v for k, v in parameter_state.items() if k in _KNOWN_PARAMS}
|
|
83
|
+
focused_display = (
|
|
84
|
+
{k: v for k, v in (display_values or {}).items() if k in _KNOWN_PARAMS}
|
|
85
|
+
if display_values else {}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return SynthProfile(
|
|
89
|
+
device_name=self.device_name,
|
|
90
|
+
opacity=NATIVE,
|
|
91
|
+
track_index=track_index,
|
|
92
|
+
device_index=device_index,
|
|
93
|
+
parameter_state=focused_state,
|
|
94
|
+
display_values=focused_display,
|
|
95
|
+
role_hint=role_hint,
|
|
96
|
+
modulation=mod,
|
|
97
|
+
articulation=articulation,
|
|
98
|
+
notes=notes,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def propose_branches(
|
|
102
|
+
self,
|
|
103
|
+
profile: SynthProfile,
|
|
104
|
+
target: TimbralFingerprint,
|
|
105
|
+
kernel: Optional[dict] = None,
|
|
106
|
+
) -> list[tuple[BranchSeed, dict]]:
|
|
107
|
+
kernel = kernel or {}
|
|
108
|
+
results: list[tuple[BranchSeed, dict]] = []
|
|
109
|
+
for strategy_fn in (_strategy_character_blend, _strategy_filter_sweep):
|
|
110
|
+
try:
|
|
111
|
+
maybe = strategy_fn(profile, target, kernel, adapter=self)
|
|
112
|
+
except Exception:
|
|
113
|
+
continue
|
|
114
|
+
if maybe is not None:
|
|
115
|
+
results.append(maybe)
|
|
116
|
+
return results
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ── Strategy registry ────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _strategy_character_blend(
|
|
123
|
+
profile: SynthProfile,
|
|
124
|
+
target: TimbralFingerprint,
|
|
125
|
+
kernel: dict,
|
|
126
|
+
adapter,
|
|
127
|
+
) -> Optional[tuple[BranchSeed, dict]]:
|
|
128
|
+
"""Shift Character + Sub balance. Always applicable."""
|
|
129
|
+
freshness = float(kernel.get("freshness", 0.5) or 0.5)
|
|
130
|
+
track = profile.track_index
|
|
131
|
+
device = profile.device_index
|
|
132
|
+
current_char = float(profile.parameter_state.get("Character", 0.0) or 0.0)
|
|
133
|
+
current_sub = float(profile.parameter_state.get("Sub Level", 0.0) or 0.0)
|
|
134
|
+
|
|
135
|
+
new_char = (
|
|
136
|
+
min(1.0, max(current_char + 0.3, 0.3))
|
|
137
|
+
if current_char <= 0.5
|
|
138
|
+
else max(0.0, current_char - 0.3)
|
|
139
|
+
)
|
|
140
|
+
target_sub = 0.3
|
|
141
|
+
new_sub = current_sub + (target_sub - current_sub) * (
|
|
142
|
+
0.5 if freshness < 0.5 else 0.8
|
|
143
|
+
)
|
|
144
|
+
new_sub = round(max(0.0, min(1.0, new_sub)), 3)
|
|
145
|
+
|
|
146
|
+
seed = freeform_seed(
|
|
147
|
+
seed_id=_short_id("dr_chr", f"{track}:{device}:{new_char:.2f}:{new_sub:.2f}"),
|
|
148
|
+
hypothesis=(
|
|
149
|
+
f"Drift character blend: Character → {new_char:.2f}, "
|
|
150
|
+
f"Sub Level → {new_sub:.2f} for a different core tone"
|
|
151
|
+
),
|
|
152
|
+
source="synthesis",
|
|
153
|
+
novelty_label="strong",
|
|
154
|
+
risk_label="low",
|
|
155
|
+
affected_scope={
|
|
156
|
+
"track_indices": [track],
|
|
157
|
+
"device_paths": [f"track/{track}/device/{device}"],
|
|
158
|
+
},
|
|
159
|
+
distinctness_reason="shifts Character + Sub balance",
|
|
160
|
+
producer_payload={
|
|
161
|
+
"device_name": adapter.device_name,
|
|
162
|
+
"track_index": track,
|
|
163
|
+
"device_index": device,
|
|
164
|
+
"strategy": "character_blend",
|
|
165
|
+
"topology_hint": {
|
|
166
|
+
"role_hint": profile.role_hint,
|
|
167
|
+
"current_char": current_char,
|
|
168
|
+
"new_char": round(new_char, 3),
|
|
169
|
+
"current_sub": current_sub,
|
|
170
|
+
"new_sub": new_sub,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
plan = {
|
|
175
|
+
"steps": [
|
|
176
|
+
{"tool": "set_device_parameter",
|
|
177
|
+
"params": {"track_index": track, "device_index": device,
|
|
178
|
+
"parameter_name": "Character",
|
|
179
|
+
"value": round(new_char, 3)}},
|
|
180
|
+
{"tool": "set_device_parameter",
|
|
181
|
+
"params": {"track_index": track, "device_index": device,
|
|
182
|
+
"parameter_name": "Sub Level",
|
|
183
|
+
"value": new_sub}},
|
|
184
|
+
],
|
|
185
|
+
"step_count": 2,
|
|
186
|
+
"summary": f"Character → {new_char:.2f}, Sub Level → {new_sub:.2f}",
|
|
187
|
+
}
|
|
188
|
+
return (seed, plan)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _strategy_filter_sweep(
|
|
192
|
+
profile: SynthProfile,
|
|
193
|
+
target: TimbralFingerprint,
|
|
194
|
+
kernel: dict,
|
|
195
|
+
adapter,
|
|
196
|
+
) -> Optional[tuple[BranchSeed, dict]]:
|
|
197
|
+
"""Sweep Filter Freq toward target brightness.
|
|
198
|
+
|
|
199
|
+
Gates: applicable when target.brightness != 0 OR role_hint suggests
|
|
200
|
+
motion ("lead", "pad", "drone"). Skip when role is "bass" (sub roles
|
|
201
|
+
want a stable low-pass, not a sweep).
|
|
202
|
+
"""
|
|
203
|
+
role = (profile.role_hint or "").lower()
|
|
204
|
+
if role in {"bass", "sub", "kick"}:
|
|
205
|
+
return None
|
|
206
|
+
want_motion = abs(target.brightness) > 0.1 or role in {"lead", "pad", "drone"}
|
|
207
|
+
if not want_motion:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
freshness = float(kernel.get("freshness", 0.5) or 0.5)
|
|
211
|
+
track = profile.track_index
|
|
212
|
+
device = profile.device_index
|
|
213
|
+
# Drift's filter freq is normalized 0-1 in the API; display is Hz.
|
|
214
|
+
current_freq = float(profile.parameter_state.get("Filter Freq", 0.5) or 0.5)
|
|
215
|
+
if target.brightness > 0:
|
|
216
|
+
# Open filter toward bright.
|
|
217
|
+
new_freq = min(1.0, current_freq + (0.15 if freshness < 0.5 else 0.3))
|
|
218
|
+
direction = "open"
|
|
219
|
+
else:
|
|
220
|
+
# Close toward warm.
|
|
221
|
+
new_freq = max(0.0, current_freq - (0.12 if freshness < 0.5 else 0.25))
|
|
222
|
+
direction = "close"
|
|
223
|
+
|
|
224
|
+
if abs(new_freq - current_freq) < 0.03:
|
|
225
|
+
return None # barely any change — skip
|
|
226
|
+
|
|
227
|
+
seed = freeform_seed(
|
|
228
|
+
seed_id=_short_id(
|
|
229
|
+
"dr_flt", f"{track}:{device}:{direction}:{new_freq:.2f}"
|
|
230
|
+
),
|
|
231
|
+
hypothesis=(
|
|
232
|
+
f"Drift filter sweep: Filter Freq {current_freq:.2f} → "
|
|
233
|
+
f"{new_freq:.2f} ({direction}) for a {direction}d voice"
|
|
234
|
+
),
|
|
235
|
+
source="synthesis",
|
|
236
|
+
novelty_label="strong" if freshness < 0.7 else "unexpected",
|
|
237
|
+
risk_label="low",
|
|
238
|
+
affected_scope={
|
|
239
|
+
"track_indices": [track],
|
|
240
|
+
"device_paths": [f"track/{track}/device/{device}"],
|
|
241
|
+
},
|
|
242
|
+
distinctness_reason=f"filter {direction} without touching core oscillator",
|
|
243
|
+
producer_payload={
|
|
244
|
+
"device_name": adapter.device_name,
|
|
245
|
+
"track_index": track,
|
|
246
|
+
"device_index": device,
|
|
247
|
+
"strategy": f"filter_sweep_{direction}",
|
|
248
|
+
"topology_hint": {
|
|
249
|
+
"role_hint": profile.role_hint,
|
|
250
|
+
"target_brightness": target.brightness,
|
|
251
|
+
"current_freq": current_freq,
|
|
252
|
+
"new_freq": round(new_freq, 3),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
plan = {
|
|
257
|
+
"steps": [
|
|
258
|
+
{"tool": "set_device_parameter",
|
|
259
|
+
"params": {"track_index": track, "device_index": device,
|
|
260
|
+
"parameter_name": "Filter Freq",
|
|
261
|
+
"value": round(new_freq, 3)}},
|
|
262
|
+
],
|
|
263
|
+
"step_count": 1,
|
|
264
|
+
"summary": f"Filter Freq {current_freq:.2f} → {new_freq:.2f}",
|
|
265
|
+
}
|
|
266
|
+
return (seed, plan)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _short_id(prefix: str, key: str) -> str:
|
|
270
|
+
h = hashlib.sha256(f"{prefix}:{key}".encode()).hexdigest()[:10]
|
|
271
|
+
return f"{prefix}_{h}"
|