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
|
@@ -75,13 +75,37 @@ class TasteGraph:
|
|
|
75
75
|
# Device preferences
|
|
76
76
|
device_affinities: dict[str, DeviceAffinity] = field(default_factory=dict)
|
|
77
77
|
|
|
78
|
-
#
|
|
79
|
-
|
|
78
|
+
# PR8 — per-goal-mode novelty bands. Canonical source of truth.
|
|
79
|
+
# Keys are goal modes ("improve", "explore", or any string a caller
|
|
80
|
+
# supplies). novelty_band (below, as a property) reads/writes
|
|
81
|
+
# novelty_bands["improve"] — one storage, two access paths.
|
|
82
|
+
novelty_bands: dict = field(
|
|
83
|
+
default_factory=lambda: {"improve": 0.5, "explore": 0.5}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# PR8 — when True, rank_moves returns uniform taste scores (0.5) to
|
|
87
|
+
# bypass taste filtering during branch generation. Callers flip this
|
|
88
|
+
# for fresh / surprise-me mode so novelty survives to post-hoc ranking.
|
|
89
|
+
bypass_taste_in_generation: bool = False
|
|
80
90
|
|
|
81
91
|
# Total evidence count (how many decisions informed this graph)
|
|
82
92
|
evidence_count: int = 0
|
|
83
93
|
last_updated_ms: int = 0
|
|
84
94
|
|
|
95
|
+
@property
|
|
96
|
+
def novelty_band(self) -> float:
|
|
97
|
+
"""Legacy flat novelty band — mirrors novelty_bands["improve"].
|
|
98
|
+
|
|
99
|
+
Kept for back-compat with callers that set/read novelty_band
|
|
100
|
+
directly. Setting this property writes through to the bands dict
|
|
101
|
+
so there's no dual source of truth.
|
|
102
|
+
"""
|
|
103
|
+
return self.novelty_bands.get("improve", 0.5)
|
|
104
|
+
|
|
105
|
+
@novelty_band.setter
|
|
106
|
+
def novelty_band(self, value: float) -> None:
|
|
107
|
+
self.novelty_bands["improve"] = float(value)
|
|
108
|
+
|
|
85
109
|
def to_dict(self) -> dict:
|
|
86
110
|
return {
|
|
87
111
|
"dimension_weights": self.dimension_weights,
|
|
@@ -96,7 +120,11 @@ class TasteGraph:
|
|
|
96
120
|
key=lambda x: -x[1].affinity,
|
|
97
121
|
)[:10] # Top 10 only
|
|
98
122
|
},
|
|
123
|
+
# novelty_band kept for legacy consumers that read it directly;
|
|
124
|
+
# novelty_bands is the canonical per-goal-mode shape going forward.
|
|
99
125
|
"novelty_band": round(self.novelty_band, 3),
|
|
126
|
+
"novelty_bands": {k: round(v, 3) for k, v in self.novelty_bands.items()},
|
|
127
|
+
"bypass_taste_in_generation": self.bypass_taste_in_generation,
|
|
100
128
|
"evidence_count": self.evidence_count,
|
|
101
129
|
}
|
|
102
130
|
|
|
@@ -154,21 +182,53 @@ class TasteGraph:
|
|
|
154
182
|
self.evidence_count += 1
|
|
155
183
|
self.last_updated_ms = now
|
|
156
184
|
|
|
157
|
-
def update_novelty_from_experiment(
|
|
158
|
-
|
|
185
|
+
def update_novelty_from_experiment(
|
|
186
|
+
self, chose_bold: bool, goal_mode: str = "improve",
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Shift novelty band for a given goal mode based on experiment choices.
|
|
189
|
+
|
|
190
|
+
PR8: goal_mode defaults to "improve" so legacy callers land on the
|
|
191
|
+
same band they updated before. Pass "explore" to shift the
|
|
192
|
+
exploration-mode band without touching improve-mode preference.
|
|
193
|
+
(novelty_band is a property view over novelty_bands["improve"], so
|
|
194
|
+
improve-mode updates automatically surface there too.)
|
|
195
|
+
"""
|
|
196
|
+
current = self.novelty_bands.get(goal_mode, 0.5)
|
|
159
197
|
if chose_bold:
|
|
160
|
-
|
|
198
|
+
new_val = min(1.0, current + 0.05)
|
|
161
199
|
else:
|
|
162
|
-
|
|
200
|
+
new_val = max(0.0, current - 0.05)
|
|
201
|
+
self.novelty_bands[goal_mode] = new_val
|
|
163
202
|
|
|
164
203
|
# ── Ranking ──────────────────────────────────────────────────────
|
|
165
204
|
|
|
166
|
-
def rank_moves(
|
|
205
|
+
def rank_moves(
|
|
206
|
+
self,
|
|
207
|
+
move_specs: list[dict],
|
|
208
|
+
goal_mode: str = "improve",
|
|
209
|
+
) -> list[dict]:
|
|
167
210
|
"""Rank a list of semantic move dicts by taste fit.
|
|
168
211
|
|
|
169
212
|
Each move dict should have: move_id, family, targets, risk_level.
|
|
170
213
|
Returns the same dicts with added 'taste_score' field, sorted desc.
|
|
214
|
+
|
|
215
|
+
PR8 additions:
|
|
216
|
+
goal_mode (str, default "improve"): which novelty band to use for
|
|
217
|
+
risk alignment. "improve" respects the user's conservative history;
|
|
218
|
+
"explore" uses the explore-mode band so past timid choices don't
|
|
219
|
+
punish surprise-me branch generation.
|
|
220
|
+
bypass_taste_in_generation (instance flag): when True, every move
|
|
221
|
+
scores a uniform 0.5. Used during branch generation so taste
|
|
222
|
+
doesn't prune novelty before the user has a chance to audition.
|
|
223
|
+
Ranking order is preserved from input when this flag is on.
|
|
171
224
|
"""
|
|
225
|
+
if self.bypass_taste_in_generation:
|
|
226
|
+
return [dict(move, taste_score=0.5) for move in move_specs]
|
|
227
|
+
|
|
228
|
+
# Read the band for the requested mode. Falls back to the improve
|
|
229
|
+
# band (via self.novelty_band property) when the mode is unknown.
|
|
230
|
+
novelty_band = self.novelty_bands.get(goal_mode, self.novelty_band)
|
|
231
|
+
|
|
172
232
|
ranked = []
|
|
173
233
|
for move in move_specs:
|
|
174
234
|
taste_score = 0.5 # Neutral baseline
|
|
@@ -190,10 +250,10 @@ class TasteGraph:
|
|
|
190
250
|
if dim in self.dimension_avoidances:
|
|
191
251
|
taste_score -= 0.3
|
|
192
252
|
|
|
193
|
-
# Novelty/risk alignment
|
|
253
|
+
# Novelty/risk alignment (PR8: per-mode band)
|
|
194
254
|
risk = move.get("risk_level", "low")
|
|
195
255
|
risk_val = {"low": 0.2, "medium": 0.5, "high": 0.8}.get(risk, 0.5)
|
|
196
|
-
novelty_match = 1.0 - abs(risk_val -
|
|
256
|
+
novelty_match = 1.0 - abs(risk_val - novelty_band)
|
|
197
257
|
taste_score += novelty_match * 0.1
|
|
198
258
|
|
|
199
259
|
# Clamp
|
|
@@ -297,8 +357,21 @@ def build_taste_graph(
|
|
|
297
357
|
if total > 0:
|
|
298
358
|
fam.score = round((fam.kept_count - fam.undone_count) / total, 3)
|
|
299
359
|
|
|
300
|
-
# Novelty band
|
|
301
|
-
|
|
360
|
+
# Novelty band — migrate from flat float if present, OR read
|
|
361
|
+
# per-mode dict if newer persistence format has it (PR8).
|
|
362
|
+
persisted_band = persisted.get("novelty_band", 0.5)
|
|
363
|
+
persisted_bands = persisted.get("novelty_bands")
|
|
364
|
+
if isinstance(persisted_bands, dict) and persisted_bands:
|
|
365
|
+
graph.novelty_bands = {
|
|
366
|
+
k: float(v) for k, v in persisted_bands.items() if isinstance(v, (int, float))
|
|
367
|
+
}
|
|
368
|
+
# Ensure both canonical keys are present with sensible defaults.
|
|
369
|
+
graph.novelty_bands.setdefault("improve", persisted_band)
|
|
370
|
+
graph.novelty_bands.setdefault("explore", persisted_band)
|
|
371
|
+
graph.novelty_band = graph.novelty_bands["improve"]
|
|
372
|
+
else:
|
|
373
|
+
graph.novelty_band = persisted_band
|
|
374
|
+
graph.novelty_bands = {"improve": persisted_band, "explore": persisted_band}
|
|
302
375
|
|
|
303
376
|
# Device affinities
|
|
304
377
|
for dev_name, dev_data in persisted.get("device_affinities", {}).items():
|
|
@@ -48,15 +48,29 @@ class PersistentTasteStore:
|
|
|
48
48
|
return data
|
|
49
49
|
self._store.update(_update)
|
|
50
50
|
|
|
51
|
-
def update_novelty(self, chose_bold: bool) -> None:
|
|
52
|
-
"""Update novelty band from experiment choice.
|
|
51
|
+
def update_novelty(self, chose_bold: bool, goal_mode: str = "improve") -> None:
|
|
52
|
+
"""Update novelty band from experiment choice for a given goal mode.
|
|
53
|
+
|
|
54
|
+
PR8: goal_mode defaults to "improve" so legacy callers land on the
|
|
55
|
+
same band they updated before. The per-mode dict ``novelty_bands``
|
|
56
|
+
is maintained alongside the flat ``novelty_band`` field; the flat
|
|
57
|
+
field mirrors the "improve" band.
|
|
58
|
+
"""
|
|
53
59
|
def _update(data: dict) -> dict:
|
|
54
60
|
data = data if data.get("version") == 1 else self._default()
|
|
55
|
-
|
|
61
|
+
# Ensure the per-mode dict exists (migrating from legacy shape).
|
|
62
|
+
bands = data.get("novelty_bands")
|
|
63
|
+
if not isinstance(bands, dict) or not bands:
|
|
64
|
+
flat = data.get("novelty_band", 0.5)
|
|
65
|
+
bands = {"improve": flat, "explore": flat}
|
|
66
|
+
current = bands.get(goal_mode, 0.5)
|
|
56
67
|
if chose_bold:
|
|
57
|
-
|
|
68
|
+
bands[goal_mode] = min(1.0, current + 0.05)
|
|
58
69
|
else:
|
|
59
|
-
|
|
70
|
+
bands[goal_mode] = max(0.0, current - 0.05)
|
|
71
|
+
data["novelty_bands"] = bands
|
|
72
|
+
# Mirror the improve band onto the flat field for back-compat.
|
|
73
|
+
data["novelty_band"] = bands.get("improve", 0.5)
|
|
60
74
|
data["evidence_count"] = data.get("evidence_count", 0) + 1
|
|
61
75
|
return data
|
|
62
76
|
self._store.update(_update)
|
|
@@ -114,6 +128,8 @@ class PersistentTasteStore:
|
|
|
114
128
|
"version": 1,
|
|
115
129
|
"move_outcomes": {},
|
|
116
130
|
"novelty_band": 0.5,
|
|
131
|
+
# PR8 — per-goal-mode novelty bands; novelty_band mirrors "improve"
|
|
132
|
+
"novelty_bands": {"improve": 0.5, "explore": 0.5},
|
|
117
133
|
"device_affinities": {},
|
|
118
134
|
"anti_preferences": [],
|
|
119
135
|
"dimension_weights": {},
|
|
@@ -45,6 +45,37 @@ class SessionKernel:
|
|
|
45
45
|
recommended_engines: list = field(default_factory=list)
|
|
46
46
|
recommended_workflow: str = ""
|
|
47
47
|
|
|
48
|
+
# ── Creative controls (PR2 — branch-native migration) ──────────────
|
|
49
|
+
# All optional. Producers (Wonder, synthesis_brain, composer) read these
|
|
50
|
+
# to bias branch generation. Pre-PR2 callers leave them at defaults and
|
|
51
|
+
# nothing changes. PR6 (Wonder refactor) and PR9 (synthesis_brain) start
|
|
52
|
+
# reading them in earnest.
|
|
53
|
+
|
|
54
|
+
# 0.0 = conservative / don't surprise me; 1.0 = surprise me.
|
|
55
|
+
# Distinct from aggression (which is about execution boldness).
|
|
56
|
+
freshness: float = 0.5
|
|
57
|
+
|
|
58
|
+
# Shorthand producer philosophy tag. The sample_engine already uses
|
|
59
|
+
# "surgeon" / "alchemist" (see livepilot-sample-engine); synth work
|
|
60
|
+
# may add "sculptor". Empty string = producer picks a default.
|
|
61
|
+
creativity_profile: str = ""
|
|
62
|
+
|
|
63
|
+
# Caller-asserted sacred elements. Normally sacred elements come from
|
|
64
|
+
# song_brain; this lets the user or a skill override. Shape matches
|
|
65
|
+
# song_brain.sacred_elements entries: {element_type, description, salience}.
|
|
66
|
+
sacred_elements: list = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
# Hints for synthesis_brain: which tracks/devices to focus on and what
|
|
69
|
+
# target timbre to aim for. Shape is open in PR2 and will be firmed up
|
|
70
|
+
# when PR9 adds the first adapters.
|
|
71
|
+
# {
|
|
72
|
+
# "track_indices": [int, ...],
|
|
73
|
+
# "device_paths": ["track/Wavetable", ...],
|
|
74
|
+
# "target_timbre": {"brightness": +0.3, "width": +0.2, ...},
|
|
75
|
+
# "preferred_devices": ["Wavetable", "Operator", ...],
|
|
76
|
+
# }
|
|
77
|
+
synth_hints: dict = field(default_factory=dict)
|
|
78
|
+
|
|
48
79
|
def to_dict(self) -> dict:
|
|
49
80
|
return asdict(self)
|
|
50
81
|
|
|
@@ -60,12 +91,23 @@ def build_session_kernel(
|
|
|
60
91
|
taste_graph: Optional[dict] = None,
|
|
61
92
|
anti_preferences: Optional[list] = None,
|
|
62
93
|
protected_dimensions: Optional[dict] = None,
|
|
94
|
+
# PR2 — creative controls. All optional; legacy callers unaffected.
|
|
95
|
+
freshness: float = 0.5,
|
|
96
|
+
creativity_profile: str = "",
|
|
97
|
+
sacred_elements: Optional[list] = None,
|
|
98
|
+
synth_hints: Optional[dict] = None,
|
|
63
99
|
) -> SessionKernel:
|
|
64
100
|
"""Build a SessionKernel from raw data.
|
|
65
101
|
|
|
66
102
|
All optional fields degrade gracefully to empty defaults.
|
|
67
103
|
The kernel_id is deterministic from the core inputs so it's stable
|
|
68
104
|
within the same turn context.
|
|
105
|
+
|
|
106
|
+
The PR2 creative-control fields (freshness, creativity_profile,
|
|
107
|
+
sacred_elements, synth_hints) are intentionally excluded from the
|
|
108
|
+
kernel_id hash so existing callers see no identity changes. Producers
|
|
109
|
+
that need these fields to influence identity can compose their own
|
|
110
|
+
derived id downstream.
|
|
69
111
|
"""
|
|
70
112
|
# Deterministic kernel_id from inputs
|
|
71
113
|
id_seed = json.dumps(
|
|
@@ -93,4 +135,8 @@ def build_session_kernel(
|
|
|
93
135
|
taste_graph=taste_graph or {},
|
|
94
136
|
anti_preferences=anti_preferences or [],
|
|
95
137
|
protected_dimensions=protected_dimensions or {},
|
|
138
|
+
freshness=freshness,
|
|
139
|
+
creativity_profile=creativity_profile,
|
|
140
|
+
sacred_elements=sacred_elements or [],
|
|
141
|
+
synth_hints=synth_hints or {},
|
|
96
142
|
)
|
|
@@ -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
|
package/mcp_server/server.py
CHANGED
|
@@ -305,6 +305,7 @@ from .device_forge import tools as device_forge_tools # noqa: F401, E40
|
|
|
305
305
|
from .sample_engine import tools as sample_engine_tools # noqa: F401, E402
|
|
306
306
|
from .atlas import tools as atlas_tools # noqa: F401, E402
|
|
307
307
|
from .composer import tools as composer_tools # noqa: F401, E402
|
|
308
|
+
from .synthesis_brain import tools as synthesis_brain_tools # noqa: F401, E402
|
|
308
309
|
from .tools import diagnostics # noqa: F401, E402
|
|
309
310
|
from .tools import miditool # noqa: F401, E402
|
|
310
311
|
|
|
@@ -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
|
+
]
|