livepilot 1.12.2 → 1.13.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 +82 -0
- package/README.md +3 -3
- 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 +32 -0
- package/mcp_server/branches/types.py +230 -0
- package/mcp_server/composer/__init__.py +10 -1
- package/mcp_server/composer/branch_producer.py +229 -0
- package/mcp_server/evaluation/policy.py +129 -2
- package/mcp_server/experiment/engine.py +47 -11
- package/mcp_server/experiment/models.py +72 -7
- package/mcp_server/experiment/tools.py +231 -35
- 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/synthesis_brain/__init__.py +53 -0
- package/mcp_server/synthesis_brain/adapters/__init__.py +34 -0
- package/mcp_server/synthesis_brain/adapters/analog.py +167 -0
- package/mcp_server/synthesis_brain/adapters/base.py +86 -0
- package/mcp_server/synthesis_brain/adapters/drift.py +166 -0
- package/mcp_server/synthesis_brain/adapters/meld.py +151 -0
- package/mcp_server/synthesis_brain/adapters/operator.py +169 -0
- package/mcp_server/synthesis_brain/adapters/wavetable.py +228 -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/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 +2 -2
|
@@ -7,6 +7,8 @@ Tools:
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
10
12
|
from fastmcp import Context
|
|
11
13
|
|
|
12
14
|
from ..server import mcp
|
|
@@ -79,6 +81,10 @@ def get_session_kernel(
|
|
|
79
81
|
request_text: str = "",
|
|
80
82
|
mode: str = "improve",
|
|
81
83
|
aggression: float = 0.5,
|
|
84
|
+
freshness: float = 0.5,
|
|
85
|
+
creativity_profile: str = "",
|
|
86
|
+
sacred_elements: Optional[list] = None,
|
|
87
|
+
synth_hints: Optional[dict] = None,
|
|
82
88
|
) -> dict:
|
|
83
89
|
"""Build the unified turn snapshot for V2 orchestration.
|
|
84
90
|
|
|
@@ -86,11 +92,27 @@ def get_session_kernel(
|
|
|
86
92
|
Assembles: session info, capability state, action ledger, taste profile,
|
|
87
93
|
anti-preferences, and session memory into one canonical snapshot.
|
|
88
94
|
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
Core params:
|
|
96
|
+
mode: observe | improve | explore | finish | diagnose
|
|
97
|
+
aggression: 0.0 (subtle) to 1.0 (bold) — execution boldness.
|
|
98
|
+
|
|
99
|
+
Creative controls (PR2 — branch-native migration, optional):
|
|
100
|
+
freshness: 0.0 (don't surprise me) to 1.0 (surprise me). Read by
|
|
101
|
+
producers (Wonder, synthesis_brain, composer) to bias branch
|
|
102
|
+
generation. Distinct from aggression, which is about applying
|
|
103
|
+
a single move boldly; freshness is about how far to roam.
|
|
104
|
+
creativity_profile: shorthand producer philosophy tag. Known values
|
|
105
|
+
include "surgeon" (targeted), "alchemist" (transformative),
|
|
106
|
+
"sculptor" (synthesis-focused). Empty ⇒ producer picks a default.
|
|
107
|
+
sacred_elements: caller-asserted list of sacred elements that
|
|
108
|
+
override or augment what song_brain infers. Shape matches
|
|
109
|
+
song_brain entries: {element_type, description, salience}.
|
|
110
|
+
synth_hints: focus hints for synthesis_brain; shape is open in PR2
|
|
111
|
+
and firms up in PR9. Typical keys: track_indices, device_paths,
|
|
112
|
+
target_timbre, preferred_devices.
|
|
91
113
|
|
|
92
114
|
Returns: SessionKernel dict with kernel_id, session topology, capabilities,
|
|
93
|
-
memory context, and
|
|
115
|
+
memory context, routing hints, and (if provided) creative controls.
|
|
94
116
|
"""
|
|
95
117
|
from .session_kernel import build_session_kernel
|
|
96
118
|
|
|
@@ -179,6 +201,10 @@ def get_session_kernel(
|
|
|
179
201
|
session_memory=session_mem,
|
|
180
202
|
taste_graph=taste_graph,
|
|
181
203
|
anti_preferences=anti_prefs,
|
|
204
|
+
freshness=freshness,
|
|
205
|
+
creativity_profile=creativity_profile,
|
|
206
|
+
sacred_elements=sacred_elements,
|
|
207
|
+
synth_hints=synth_hints,
|
|
182
208
|
)
|
|
183
209
|
|
|
184
210
|
# Populate routing hints from conductor when request context is available
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Synthesis brain — native-synth-aware branch production.
|
|
2
|
+
|
|
3
|
+
Parallel subsystem to mcp_server.sound_design. Where sound_design reasons
|
|
4
|
+
at the block-type level (oscillator / filter / envelope / ...), the
|
|
5
|
+
synthesis brain reasons at the native-device level with per-adapter
|
|
6
|
+
knowledge of Wavetable / Operator / Analog / Drift / Meld parameter
|
|
7
|
+
spaces, modulation graphs, and articulation profiles.
|
|
8
|
+
|
|
9
|
+
Each adapter implements the SynthAdapter protocol:
|
|
10
|
+
- device_name: the Ableton device name it claims
|
|
11
|
+
- extract_profile(device_parameters): read a SynthProfile from live params
|
|
12
|
+
- propose_branches(profile, target, kernel): emit BranchSeed objects plus
|
|
13
|
+
pre-compiled plans (set_device_parameter / batch_set_parameters steps)
|
|
14
|
+
|
|
15
|
+
PR9 ships Wavetable and Operator adapters. PR10 adds Analog, Drift, Meld
|
|
16
|
+
and render-based timbre extraction on top of ``capture_audio``.
|
|
17
|
+
|
|
18
|
+
No MCP @tool() decorators in this PR — the subsystem is callable from
|
|
19
|
+
Python (Wonder's generate_branch_seeds, composer, etc.). PR12 wires
|
|
20
|
+
dedicated MCP tools and does the tool-count metadata sweep in one pass.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .models import (
|
|
24
|
+
SynthProfile,
|
|
25
|
+
TimbralFingerprint,
|
|
26
|
+
ModulationGraph,
|
|
27
|
+
ArticulationProfile,
|
|
28
|
+
OPAQUE,
|
|
29
|
+
NATIVE,
|
|
30
|
+
)
|
|
31
|
+
from .adapters import get_adapter, SynthAdapter
|
|
32
|
+
from .engine import (
|
|
33
|
+
analyze_synth_patch,
|
|
34
|
+
propose_synth_branches,
|
|
35
|
+
supported_devices,
|
|
36
|
+
)
|
|
37
|
+
from .timbre import extract_timbre_fingerprint, diff_fingerprint
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"SynthProfile",
|
|
41
|
+
"TimbralFingerprint",
|
|
42
|
+
"ModulationGraph",
|
|
43
|
+
"ArticulationProfile",
|
|
44
|
+
"OPAQUE",
|
|
45
|
+
"NATIVE",
|
|
46
|
+
"SynthAdapter",
|
|
47
|
+
"get_adapter",
|
|
48
|
+
"analyze_synth_patch",
|
|
49
|
+
"propose_synth_branches",
|
|
50
|
+
"supported_devices",
|
|
51
|
+
"extract_timbre_fingerprint",
|
|
52
|
+
"diff_fingerprint",
|
|
53
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Synth adapter registry.
|
|
2
|
+
|
|
3
|
+
Each adapter is a Python class implementing the SynthAdapter protocol.
|
|
4
|
+
Registration is explicit — adapters add themselves to ``_REGISTRY`` at
|
|
5
|
+
module import time. ``get_adapter(device_name)`` returns the registered
|
|
6
|
+
adapter or None.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .base import SynthAdapter, _REGISTRY, register_adapter
|
|
12
|
+
from . import wavetable as _wavetable # noqa: F401 — import for registration
|
|
13
|
+
from . import operator as _operator # noqa: F401
|
|
14
|
+
from . import analog as _analog # noqa: F401
|
|
15
|
+
from . import drift as _drift # noqa: F401
|
|
16
|
+
from . import meld as _meld # noqa: F401
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_adapter(device_name: str) -> SynthAdapter | None:
|
|
20
|
+
"""Return the adapter for a given Ableton device name, or None."""
|
|
21
|
+
return _REGISTRY.get(device_name)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def registered_devices() -> list[str]:
|
|
25
|
+
"""List device names this package has an adapter for."""
|
|
26
|
+
return sorted(_REGISTRY.keys())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"SynthAdapter",
|
|
31
|
+
"get_adapter",
|
|
32
|
+
"register_adapter",
|
|
33
|
+
"registered_devices",
|
|
34
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
kernel = kernel or {}
|
|
108
|
+
freshness = float(kernel.get("freshness", 0.5) or 0.5)
|
|
109
|
+
track = profile.track_index
|
|
110
|
+
device = profile.device_index
|
|
111
|
+
|
|
112
|
+
# Skip proposal if already plucky — avoids doubling-down on the
|
|
113
|
+
# same treatment.
|
|
114
|
+
if any("Already plucky" in n for n in profile.notes):
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
current_env = float(profile.parameter_state.get("F1 Env Amount", 0.0) or 0.0)
|
|
118
|
+
# Target filter-env amount scales with freshness.
|
|
119
|
+
new_env = min(1.0, max(current_env, 0.45 if freshness < 0.5 else 0.65))
|
|
120
|
+
current_decay = float(profile.parameter_state.get("F1 Env D", 0.5) or 0.5)
|
|
121
|
+
new_decay = min(current_decay, 0.25 if freshness < 0.5 else 0.15)
|
|
122
|
+
|
|
123
|
+
seed = freeform_seed(
|
|
124
|
+
seed_id=_short_id("an_plk", f"{track}:{device}:{new_env:.2f}:{new_decay:.2f}"),
|
|
125
|
+
hypothesis=(
|
|
126
|
+
f"Analog filter-pluck: Env Amount → {new_env:.2f}, "
|
|
127
|
+
f"Decay → {new_decay:.2f} for attack character"
|
|
128
|
+
),
|
|
129
|
+
source="synthesis",
|
|
130
|
+
novelty_label="strong" if freshness < 0.7 else "unexpected",
|
|
131
|
+
risk_label="low",
|
|
132
|
+
affected_scope={
|
|
133
|
+
"track_indices": [track],
|
|
134
|
+
"device_paths": [f"track/{track}/device/{device}"],
|
|
135
|
+
},
|
|
136
|
+
distinctness_reason="only Analog seed that couples Filter Env + Decay",
|
|
137
|
+
)
|
|
138
|
+
plan = {
|
|
139
|
+
"steps": [
|
|
140
|
+
{
|
|
141
|
+
"tool": "set_device_parameter",
|
|
142
|
+
"params": {
|
|
143
|
+
"track_index": track,
|
|
144
|
+
"device_index": device,
|
|
145
|
+
"parameter_name": "F1 Env Amount",
|
|
146
|
+
"value": round(new_env, 3),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"tool": "set_device_parameter",
|
|
151
|
+
"params": {
|
|
152
|
+
"track_index": track,
|
|
153
|
+
"device_index": device,
|
|
154
|
+
"parameter_name": "F1 Env D",
|
|
155
|
+
"value": round(new_decay, 3),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
"step_count": 2,
|
|
160
|
+
"summary": f"F1 Env Amount → {new_env:.2f}, F1 Env D → {new_decay:.2f}",
|
|
161
|
+
}
|
|
162
|
+
return [(seed, plan)]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _short_id(prefix: str, key: str) -> str:
|
|
166
|
+
h = hashlib.sha256(f"{prefix}:{key}".encode()).hexdigest()[:10]
|
|
167
|
+
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,166 @@
|
|
|
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
|
+
freshness = float(kernel.get("freshness", 0.5) or 0.5)
|
|
109
|
+
track = profile.track_index
|
|
110
|
+
device = profile.device_index
|
|
111
|
+
|
|
112
|
+
current_char = float(profile.parameter_state.get("Character", 0.0) or 0.0)
|
|
113
|
+
current_sub = float(profile.parameter_state.get("Sub Level", 0.0) or 0.0)
|
|
114
|
+
|
|
115
|
+
# Toggle character in the opposite direction to create contrast.
|
|
116
|
+
new_char = min(1.0, max(current_char + 0.3, 0.3)) if current_char <= 0.5 else max(0.0, current_char - 0.3)
|
|
117
|
+
# Move sub slightly toward 0.3 if we're starting outside 0.2-0.4
|
|
118
|
+
target_sub = 0.3
|
|
119
|
+
new_sub = current_sub + (target_sub - current_sub) * (0.5 if freshness < 0.5 else 0.8)
|
|
120
|
+
new_sub = round(max(0.0, min(1.0, new_sub)), 3)
|
|
121
|
+
|
|
122
|
+
seed = freeform_seed(
|
|
123
|
+
seed_id=_short_id("dr_chr", f"{track}:{device}:{new_char:.2f}:{new_sub:.2f}"),
|
|
124
|
+
hypothesis=(
|
|
125
|
+
f"Drift character blend: Character → {new_char:.2f}, "
|
|
126
|
+
f"Sub Level → {new_sub:.2f} for a different core tone"
|
|
127
|
+
),
|
|
128
|
+
source="synthesis",
|
|
129
|
+
novelty_label="strong",
|
|
130
|
+
risk_label="low",
|
|
131
|
+
affected_scope={
|
|
132
|
+
"track_indices": [track],
|
|
133
|
+
"device_paths": [f"track/{track}/device/{device}"],
|
|
134
|
+
},
|
|
135
|
+
distinctness_reason="only Drift seed that shifts Character + Sub balance",
|
|
136
|
+
)
|
|
137
|
+
plan = {
|
|
138
|
+
"steps": [
|
|
139
|
+
{
|
|
140
|
+
"tool": "set_device_parameter",
|
|
141
|
+
"params": {
|
|
142
|
+
"track_index": track,
|
|
143
|
+
"device_index": device,
|
|
144
|
+
"parameter_name": "Character",
|
|
145
|
+
"value": round(new_char, 3),
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"tool": "set_device_parameter",
|
|
150
|
+
"params": {
|
|
151
|
+
"track_index": track,
|
|
152
|
+
"device_index": device,
|
|
153
|
+
"parameter_name": "Sub Level",
|
|
154
|
+
"value": new_sub,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
"step_count": 2,
|
|
159
|
+
"summary": f"Character → {new_char:.2f}, Sub Level → {new_sub:.2f}",
|
|
160
|
+
}
|
|
161
|
+
return [(seed, plan)]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _short_id(prefix: str, key: str) -> str:
|
|
165
|
+
h = hashlib.sha256(f"{prefix}:{key}".encode()).hexdigest()[:10]
|
|
166
|
+
return f"{prefix}_{h}"
|