livepilot 1.9.23 → 1.10.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/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +119 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +144 -13
- package/bin/livepilot.js +87 -0
- package/installer/codex.js +147 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +21 -4
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
- package/livepilot/skills/livepilot-core/references/overview.md +13 -9
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
- package/livepilot/skills/livepilot-devices/SKILL.md +16 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +19 -5
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +15 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +2 -2
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +357 -0
- package/mcp_server/atlas/device_atlas.json +44067 -0
- package/mcp_server/atlas/enrichments/__init__.py +111 -0
- package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
- package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
- package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
- package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
- package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
- package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
- package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
- package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
- package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
- package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
- package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
- package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
- package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
- package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
- package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
- package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
- package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
- package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
- package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
- package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
- package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
- package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
- package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
- package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
- package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
- package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
- package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
- package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
- package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
- package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
- package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
- package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
- package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
- package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
- package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
- package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
- package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
- package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
- package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
- package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
- package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
- package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
- package/mcp_server/atlas/scanner.py +236 -0
- package/mcp_server/atlas/tools.py +224 -0
- package/mcp_server/composer/__init__.py +1 -0
- package/mcp_server/composer/engine.py +452 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/tools.py +201 -0
- package/mcp_server/connection.py +53 -8
- package/mcp_server/corpus/__init__.py +377 -0
- package/mcp_server/device_forge/__init__.py +1 -0
- package/mcp_server/device_forge/builder.py +377 -0
- package/mcp_server/device_forge/models.py +142 -0
- package/mcp_server/device_forge/templates.py +483 -0
- package/mcp_server/device_forge/tools.py +162 -0
- package/mcp_server/hook_hunter/analyzer.py +23 -0
- package/mcp_server/hook_hunter/models.py +1 -0
- package/mcp_server/hook_hunter/tools.py +4 -2
- package/mcp_server/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_graph.py +68 -1
- package/mcp_server/memory/tools.py +15 -4
- package/mcp_server/musical_intelligence/detectors.py +14 -1
- package/mcp_server/musical_intelligence/tools.py +11 -8
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -0
- package/mcp_server/preview_studio/models.py +1 -0
- package/mcp_server/preview_studio/tools.py +56 -13
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +137 -0
- package/mcp_server/runtime/execution_router.py +143 -0
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/remote_commands.py +87 -0
- package/mcp_server/runtime/tools.py +18 -4
- package/mcp_server/sample_engine/__init__.py +1 -0
- package/mcp_server/sample_engine/analyzer.py +216 -0
- package/mcp_server/sample_engine/critics.py +390 -0
- package/mcp_server/sample_engine/models.py +193 -0
- package/mcp_server/sample_engine/moves.py +127 -0
- package/mcp_server/sample_engine/planner.py +186 -0
- package/mcp_server/sample_engine/sources.py +540 -0
- package/mcp_server/sample_engine/techniques.py +908 -0
- package/mcp_server/sample_engine/tools.py +442 -0
- package/mcp_server/semantic_moves/__init__.py +3 -0
- package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
- package/mcp_server/semantic_moves/mix_moves.py +41 -41
- package/mcp_server/semantic_moves/performance_moves.py +13 -13
- package/mcp_server/semantic_moves/sample_compilers.py +372 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
- package/mcp_server/semantic_moves/tools.py +18 -17
- package/mcp_server/semantic_moves/transition_moves.py +16 -16
- package/mcp_server/server.py +51 -0
- package/mcp_server/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -0
- package/mcp_server/session_continuity/tracker.py +29 -1
- package/mcp_server/song_brain/builder.py +28 -1
- package/mcp_server/song_brain/models.py +4 -0
- package/mcp_server/song_brain/tools.py +20 -2
- package/mcp_server/sound_design/critics.py +89 -1
- package/mcp_server/splice_client/__init__.py +1 -0
- package/mcp_server/splice_client/client.py +347 -0
- package/mcp_server/splice_client/models.py +96 -0
- package/mcp_server/splice_client/protos/__init__.py +1 -0
- package/mcp_server/splice_client/protos/app_pb2.py +319 -0
- package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
- package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
- package/mcp_server/tools/arrangement.py +69 -0
- package/mcp_server/tools/automation.py +15 -2
- package/mcp_server/tools/devices.py +117 -6
- package/mcp_server/tools/notes.py +37 -4
- package/mcp_server/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +85 -1
- package/mcp_server/wonder_mode/tools.py +6 -1
- package/package.json +12 -2
- package/remote_script/LivePilot/__init__.py +8 -1
- package/remote_script/LivePilot/arrangement.py +114 -0
- package/remote_script/LivePilot/browser.py +56 -1
- package/remote_script/LivePilot/devices.py +236 -6
- package/remote_script/LivePilot/mixing.py +8 -3
- package/remote_script/LivePilot/server.py +5 -1
- package/remote_script/LivePilot/transport.py +3 -0
- package/remote_script/LivePilot/version_detect.py +78 -0
- package/scripts/sync_metadata.py +132 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""gen~ DSP building block library — GenExpr code as reusable templates.
|
|
2
|
+
|
|
3
|
+
Each template is a complete, working gen~ codebox program with parameters.
|
|
4
|
+
Templates are organized by category and can be used directly with
|
|
5
|
+
generate_m4l_effect or as starting points for custom devices.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .models import GenExprParam, GenExprTemplate, UNIT_STYLE_HERTZ, UNIT_STYLE_PERCENT, UNIT_STYLE_FLOAT
|
|
11
|
+
|
|
12
|
+
TEMPLATES: dict[str, GenExprTemplate] = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _register(t: GenExprTemplate) -> GenExprTemplate:
|
|
16
|
+
TEMPLATES[t.template_id] = t
|
|
17
|
+
return t
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_template(template_id: str) -> GenExprTemplate | None:
|
|
21
|
+
return TEMPLATES.get(template_id)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def list_templates(category: str = "") -> list[dict]:
|
|
25
|
+
items = list(TEMPLATES.values())
|
|
26
|
+
if category:
|
|
27
|
+
items = [t for t in items if t.category == category]
|
|
28
|
+
return [t.to_dict() for t in items]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def list_categories() -> list[str]:
|
|
32
|
+
return sorted({t.category for t in TEMPLATES.values()})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
36
|
+
# UTILITY
|
|
37
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
_register(GenExprTemplate(
|
|
40
|
+
template_id="passthrough",
|
|
41
|
+
name="Passthrough",
|
|
42
|
+
description="Clean audio passthrough — useful as a starting skeleton",
|
|
43
|
+
category="utility",
|
|
44
|
+
code="out1 = in1;",
|
|
45
|
+
params=[],
|
|
46
|
+
))
|
|
47
|
+
|
|
48
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
49
|
+
# CHAOS
|
|
50
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
51
|
+
|
|
52
|
+
_register(GenExprTemplate(
|
|
53
|
+
template_id="lorenz_attractor",
|
|
54
|
+
name="Lorenz Attractor",
|
|
55
|
+
description="Three coupled differential equations producing chaotic modulation. "
|
|
56
|
+
"Smooth but unpredictable — great for filter cutoff, pan, and send modulation.",
|
|
57
|
+
category="chaos",
|
|
58
|
+
code="""\
|
|
59
|
+
Param sigma(10);
|
|
60
|
+
Param rho(28);
|
|
61
|
+
Param beta(2.667);
|
|
62
|
+
Param speed(0.001);
|
|
63
|
+
|
|
64
|
+
History x(0.1);
|
|
65
|
+
History y(0);
|
|
66
|
+
History z(0);
|
|
67
|
+
|
|
68
|
+
dx = sigma * (y - x);
|
|
69
|
+
dy = x * (rho - z) - y;
|
|
70
|
+
dz = x * y - beta * z;
|
|
71
|
+
|
|
72
|
+
x += dx * speed;
|
|
73
|
+
y += dy * speed;
|
|
74
|
+
z += dz * speed;
|
|
75
|
+
|
|
76
|
+
out1 = x * 0.02;""",
|
|
77
|
+
params=[
|
|
78
|
+
GenExprParam(name="sigma", default=10, min_val=0.1, max_val=50),
|
|
79
|
+
GenExprParam(name="rho", default=28, min_val=0.1, max_val=100),
|
|
80
|
+
GenExprParam(name="beta", default=2.667, min_val=0.1, max_val=10),
|
|
81
|
+
GenExprParam(name="speed", default=0.001, min_val=0.0001, max_val=0.01),
|
|
82
|
+
],
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
_register(GenExprTemplate(
|
|
86
|
+
template_id="henon_map",
|
|
87
|
+
name="Henon Map",
|
|
88
|
+
description="Discrete chaotic map — produces noise textures at some parameters, "
|
|
89
|
+
"pitched tones at others. The edge-of-chaos zone is musically rich.",
|
|
90
|
+
category="chaos",
|
|
91
|
+
code="""\
|
|
92
|
+
Param a(1.4);
|
|
93
|
+
Param b(0.3);
|
|
94
|
+
Param speed(0.01);
|
|
95
|
+
|
|
96
|
+
History x(0.1);
|
|
97
|
+
History y(0);
|
|
98
|
+
|
|
99
|
+
nx = 1 - a * x * x + y;
|
|
100
|
+
ny = b * x;
|
|
101
|
+
|
|
102
|
+
x = nx;
|
|
103
|
+
y = ny;
|
|
104
|
+
|
|
105
|
+
out1 = x * speed;""",
|
|
106
|
+
params=[
|
|
107
|
+
GenExprParam(name="a", default=1.4, min_val=0.1, max_val=2.0),
|
|
108
|
+
GenExprParam(name="b", default=0.3, min_val=0.0, max_val=1.0),
|
|
109
|
+
GenExprParam(name="speed", default=0.01, min_val=0.001, max_val=0.1),
|
|
110
|
+
],
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
114
|
+
# SYNTHESIS
|
|
115
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
_register(GenExprTemplate(
|
|
118
|
+
template_id="karplus_strong",
|
|
119
|
+
name="Karplus-Strong String",
|
|
120
|
+
description="Plucked string physical model — noise burst excitation through "
|
|
121
|
+
"a filtered delay line. Classic digital waveguide synthesis.",
|
|
122
|
+
category="synthesis",
|
|
123
|
+
code="""\
|
|
124
|
+
Param freq(220);
|
|
125
|
+
Param damping(0.996);
|
|
126
|
+
Param brightness(0.5);
|
|
127
|
+
|
|
128
|
+
delaySamples = samplerate / max(freq, 20);
|
|
129
|
+
History h1(0);
|
|
130
|
+
|
|
131
|
+
trigger = in1 > 0.5;
|
|
132
|
+
excitation = noise() * trigger;
|
|
133
|
+
|
|
134
|
+
delayed = delay(h1, delaySamples);
|
|
135
|
+
filtered = delayed * brightness + h1 * (1 - brightness);
|
|
136
|
+
h1 = excitation + filtered * damping;
|
|
137
|
+
|
|
138
|
+
out1 = h1;""",
|
|
139
|
+
params=[
|
|
140
|
+
GenExprParam(name="freq", default=220, min_val=20, max_val=20000, unit_style=UNIT_STYLE_HERTZ),
|
|
141
|
+
GenExprParam(name="damping", default=0.996, min_val=0.9, max_val=0.9999),
|
|
142
|
+
GenExprParam(name="brightness", default=0.5, min_val=0.0, max_val=1.0),
|
|
143
|
+
],
|
|
144
|
+
))
|
|
145
|
+
|
|
146
|
+
_register(GenExprTemplate(
|
|
147
|
+
template_id="karplus_strong_reverb",
|
|
148
|
+
name="Karplus-Strong with Reverb Feedback",
|
|
149
|
+
description="KS variant with allpass diffusion in the feedback loop instead of simple "
|
|
150
|
+
"lowpass. Produces sustained, evolving, richly harmonic tones.",
|
|
151
|
+
category="synthesis",
|
|
152
|
+
code="""\
|
|
153
|
+
Param freq(220);
|
|
154
|
+
Param damping(0.995);
|
|
155
|
+
Param diffusion(0.5);
|
|
156
|
+
|
|
157
|
+
delaySamples = samplerate / max(freq, 20);
|
|
158
|
+
History h1(0);
|
|
159
|
+
History ap1(0);
|
|
160
|
+
History ap2(0);
|
|
161
|
+
|
|
162
|
+
trigger = in1 > 0.5;
|
|
163
|
+
excitation = noise() * trigger;
|
|
164
|
+
|
|
165
|
+
delayed = delay(h1, delaySamples);
|
|
166
|
+
|
|
167
|
+
// Two allpass stages for diffusion
|
|
168
|
+
ap1_in = delayed + ap1 * diffusion;
|
|
169
|
+
ap1_out = ap1 * -diffusion + delay(ap1_in, 37);
|
|
170
|
+
ap1 = ap1_in;
|
|
171
|
+
|
|
172
|
+
ap2_in = ap1_out + ap2 * diffusion;
|
|
173
|
+
ap2_out = ap2 * -diffusion + delay(ap2_in, 113);
|
|
174
|
+
ap2 = ap2_in;
|
|
175
|
+
|
|
176
|
+
filtered = (ap2_out + delayed) * 0.5;
|
|
177
|
+
h1 = excitation + filtered * damping;
|
|
178
|
+
|
|
179
|
+
out1 = h1;""",
|
|
180
|
+
params=[
|
|
181
|
+
GenExprParam(name="freq", default=220, min_val=20, max_val=20000, unit_style=UNIT_STYLE_HERTZ),
|
|
182
|
+
GenExprParam(name="damping", default=0.995, min_val=0.9, max_val=0.9999),
|
|
183
|
+
GenExprParam(name="diffusion", default=0.5, min_val=0.0, max_val=0.9),
|
|
184
|
+
],
|
|
185
|
+
))
|
|
186
|
+
|
|
187
|
+
_register(GenExprTemplate(
|
|
188
|
+
template_id="phase_distortion",
|
|
189
|
+
name="Phase Distortion Synth",
|
|
190
|
+
description="CZ-style phase distortion synthesis — a phasor's shape is warped "
|
|
191
|
+
"before reading a cosine table, creating rich timbral evolution.",
|
|
192
|
+
category="synthesis",
|
|
193
|
+
code="""\
|
|
194
|
+
Param freq(440);
|
|
195
|
+
Param distortion(0.5);
|
|
196
|
+
|
|
197
|
+
History phase(0);
|
|
198
|
+
|
|
199
|
+
phase += freq / samplerate;
|
|
200
|
+
phase = wrap(phase, 0, 1);
|
|
201
|
+
|
|
202
|
+
// Distort the phase curve
|
|
203
|
+
warped = phase + distortion * sin(phase * TWOPI) * 0.5;
|
|
204
|
+
warped = wrap(warped, 0, 1);
|
|
205
|
+
|
|
206
|
+
out1 = cos(warped * TWOPI) * 0.5;""",
|
|
207
|
+
params=[
|
|
208
|
+
GenExprParam(name="freq", default=440, min_val=20, max_val=20000, unit_style=UNIT_STYLE_HERTZ),
|
|
209
|
+
GenExprParam(name="distortion", default=0.5, min_val=0.0, max_val=1.0),
|
|
210
|
+
],
|
|
211
|
+
))
|
|
212
|
+
|
|
213
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
214
|
+
# DISTORTION
|
|
215
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
216
|
+
|
|
217
|
+
_register(GenExprTemplate(
|
|
218
|
+
template_id="wavefolder",
|
|
219
|
+
name="Wavefolder",
|
|
220
|
+
description="Buchla-style wavefolder — folds the waveform back on itself, "
|
|
221
|
+
"generating rich harmonic series. Multiple fold stages for complexity.",
|
|
222
|
+
category="distortion",
|
|
223
|
+
code="""\
|
|
224
|
+
Param drive(2);
|
|
225
|
+
Param symmetry(0.5);
|
|
226
|
+
Param mix(0.75);
|
|
227
|
+
|
|
228
|
+
input = in1 * drive;
|
|
229
|
+
|
|
230
|
+
// Three fold stages
|
|
231
|
+
folded = fold(input + symmetry - 0.5, -1, 1);
|
|
232
|
+
folded = fold(folded * 1.5, -1, 1);
|
|
233
|
+
folded = fold(folded * 1.2, -1, 1);
|
|
234
|
+
|
|
235
|
+
out1 = in1 * (1 - mix) + folded * mix;""",
|
|
236
|
+
params=[
|
|
237
|
+
GenExprParam(name="drive", default=2, min_val=1, max_val=20),
|
|
238
|
+
GenExprParam(name="symmetry", default=0.5, min_val=0.0, max_val=1.0),
|
|
239
|
+
GenExprParam(name="mix", default=0.75, min_val=0.0, max_val=1.0, unit_style=UNIT_STYLE_PERCENT),
|
|
240
|
+
],
|
|
241
|
+
))
|
|
242
|
+
|
|
243
|
+
_register(GenExprTemplate(
|
|
244
|
+
template_id="bitcrusher",
|
|
245
|
+
name="Bitcrusher",
|
|
246
|
+
description="Sample-rate and bit-depth reducer — from subtle aliasing to full "
|
|
247
|
+
"digital destruction. Modulate rate for glitch textures.",
|
|
248
|
+
category="distortion",
|
|
249
|
+
code="""\
|
|
250
|
+
Param rate(0.5);
|
|
251
|
+
Param bits(8);
|
|
252
|
+
|
|
253
|
+
// Sample rate reduction via sample-and-hold
|
|
254
|
+
reduced = latch(in1, phasor(samplerate * max(rate, 0.01)));
|
|
255
|
+
|
|
256
|
+
// Bit depth reduction
|
|
257
|
+
levels = pow(2, max(bits, 1));
|
|
258
|
+
crushed = floor(reduced * levels + 0.5) / levels;
|
|
259
|
+
|
|
260
|
+
out1 = crushed;""",
|
|
261
|
+
params=[
|
|
262
|
+
GenExprParam(name="rate", default=0.5, min_val=0.01, max_val=1.0),
|
|
263
|
+
GenExprParam(name="bits", default=8, min_val=1, max_val=16),
|
|
264
|
+
],
|
|
265
|
+
))
|
|
266
|
+
|
|
267
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
268
|
+
# MODULATION
|
|
269
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
270
|
+
|
|
271
|
+
_register(GenExprTemplate(
|
|
272
|
+
template_id="ring_modulator",
|
|
273
|
+
name="Ring Modulator",
|
|
274
|
+
description="Classic ring modulation — input multiplied by a carrier oscillator. "
|
|
275
|
+
"Use sub-audio rates for tremolo, audio rates for metallic sidebands.",
|
|
276
|
+
category="modulation",
|
|
277
|
+
code="""\
|
|
278
|
+
Param freq(100);
|
|
279
|
+
Param depth(1.0);
|
|
280
|
+
Param mix(0.5);
|
|
281
|
+
|
|
282
|
+
carrier = cycle(freq);
|
|
283
|
+
modulated = in1 * (1 - depth + depth * carrier);
|
|
284
|
+
|
|
285
|
+
out1 = in1 * (1 - mix) + modulated * mix;""",
|
|
286
|
+
params=[
|
|
287
|
+
GenExprParam(name="freq", default=100, min_val=0.1, max_val=20000, unit_style=UNIT_STYLE_HERTZ),
|
|
288
|
+
GenExprParam(name="depth", default=1.0, min_val=0.0, max_val=1.0),
|
|
289
|
+
GenExprParam(name="mix", default=0.5, min_val=0.0, max_val=1.0, unit_style=UNIT_STYLE_PERCENT),
|
|
290
|
+
],
|
|
291
|
+
))
|
|
292
|
+
|
|
293
|
+
_register(GenExprTemplate(
|
|
294
|
+
template_id="chorus",
|
|
295
|
+
name="Chorus",
|
|
296
|
+
description="Multi-voice micro-delay chorus — three slightly detuned delay taps "
|
|
297
|
+
"create width and shimmer without muddiness.",
|
|
298
|
+
category="modulation",
|
|
299
|
+
code="""\
|
|
300
|
+
Param rate(0.5);
|
|
301
|
+
Param depth(0.003);
|
|
302
|
+
Param mix(0.5);
|
|
303
|
+
|
|
304
|
+
base_delay = 0.01 * samplerate;
|
|
305
|
+
mod1 = cycle(rate) * depth * samplerate;
|
|
306
|
+
mod2 = cycle(rate * 1.1) * depth * samplerate * 0.8;
|
|
307
|
+
mod3 = cycle(rate * 0.9) * depth * samplerate * 1.2;
|
|
308
|
+
|
|
309
|
+
d1 = delay(in1, base_delay + mod1);
|
|
310
|
+
d2 = delay(in1, base_delay + mod2);
|
|
311
|
+
d3 = delay(in1, base_delay + mod3);
|
|
312
|
+
|
|
313
|
+
wet = (d1 + d2 + d3) / 3;
|
|
314
|
+
|
|
315
|
+
out1 = in1 * (1 - mix) + wet * mix;""",
|
|
316
|
+
params=[
|
|
317
|
+
GenExprParam(name="rate", default=0.5, min_val=0.05, max_val=5, unit_style=UNIT_STYLE_HERTZ),
|
|
318
|
+
GenExprParam(name="depth", default=0.003, min_val=0.0, max_val=0.02),
|
|
319
|
+
GenExprParam(name="mix", default=0.5, min_val=0.0, max_val=1.0, unit_style=UNIT_STYLE_PERCENT),
|
|
320
|
+
],
|
|
321
|
+
))
|
|
322
|
+
|
|
323
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
324
|
+
# DELAY
|
|
325
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
326
|
+
|
|
327
|
+
_register(GenExprTemplate(
|
|
328
|
+
template_id="feedback_delay",
|
|
329
|
+
name="Feedback Delay",
|
|
330
|
+
description="Simple delay line with filtered feedback — the backbone of "
|
|
331
|
+
"echo effects, dub delays, and ambient washes.",
|
|
332
|
+
category="delay",
|
|
333
|
+
code="""\
|
|
334
|
+
Param time(0.25);
|
|
335
|
+
Param feedback(0.5);
|
|
336
|
+
Param damping(0.7);
|
|
337
|
+
|
|
338
|
+
History prev_out(0);
|
|
339
|
+
|
|
340
|
+
delay_samples = max(time, 0.001) * samplerate;
|
|
341
|
+
delayed = delay(in1 + prev_out * feedback, delay_samples);
|
|
342
|
+
|
|
343
|
+
// One-pole lowpass in feedback path
|
|
344
|
+
filtered = delayed * damping + prev_out * (1 - damping);
|
|
345
|
+
prev_out = filtered;
|
|
346
|
+
|
|
347
|
+
out1 = in1 * 0.5 + filtered * 0.5;""",
|
|
348
|
+
params=[
|
|
349
|
+
GenExprParam(name="time", default=0.25, min_val=0.001, max_val=2.0),
|
|
350
|
+
GenExprParam(name="feedback", default=0.5, min_val=0.0, max_val=0.95),
|
|
351
|
+
GenExprParam(name="damping", default=0.7, min_val=0.0, max_val=1.0),
|
|
352
|
+
],
|
|
353
|
+
))
|
|
354
|
+
|
|
355
|
+
_register(GenExprTemplate(
|
|
356
|
+
template_id="feedback_delay_network",
|
|
357
|
+
name="Feedback Delay Network",
|
|
358
|
+
description="4-line FDN with Hadamard-like cross-coupling matrix. Creates dense, "
|
|
359
|
+
"diffuse reverb tails. Tune delay times to musical intervals for harmonic reverb.",
|
|
360
|
+
category="delay",
|
|
361
|
+
code="""\
|
|
362
|
+
Param size(0.05);
|
|
363
|
+
Param feedback(0.7);
|
|
364
|
+
Param damping(0.8);
|
|
365
|
+
|
|
366
|
+
History a(0);
|
|
367
|
+
History b(0);
|
|
368
|
+
History c(0);
|
|
369
|
+
History d(0);
|
|
370
|
+
|
|
371
|
+
base = max(size, 0.001) * samplerate;
|
|
372
|
+
da = delay(a, base * 1.0);
|
|
373
|
+
db = delay(b, base * 1.347);
|
|
374
|
+
dc = delay(c, base * 1.573);
|
|
375
|
+
dd = delay(d, base * 1.811);
|
|
376
|
+
|
|
377
|
+
// Hadamard-like mixing matrix (preserves energy)
|
|
378
|
+
na = ( da + db + dc + dd) * 0.5;
|
|
379
|
+
nb = ( da - db + dc - dd) * 0.5;
|
|
380
|
+
nc = ( da + db - dc - dd) * 0.5;
|
|
381
|
+
nd = ( da - db - dc + dd) * 0.5;
|
|
382
|
+
|
|
383
|
+
// Damping + feedback + input injection
|
|
384
|
+
a = (na * damping) * feedback + in1;
|
|
385
|
+
b = (nb * damping) * feedback;
|
|
386
|
+
c = (nc * damping) * feedback;
|
|
387
|
+
d = (nd * damping) * feedback;
|
|
388
|
+
|
|
389
|
+
out1 = (da + db + dc + dd) * 0.25;""",
|
|
390
|
+
params=[
|
|
391
|
+
GenExprParam(name="size", default=0.05, min_val=0.001, max_val=0.5),
|
|
392
|
+
GenExprParam(name="feedback", default=0.7, min_val=0.0, max_val=0.95),
|
|
393
|
+
GenExprParam(name="damping", default=0.8, min_val=0.0, max_val=1.0),
|
|
394
|
+
],
|
|
395
|
+
))
|
|
396
|
+
|
|
397
|
+
_register(GenExprTemplate(
|
|
398
|
+
template_id="granular_delay",
|
|
399
|
+
name="Granular Delay",
|
|
400
|
+
description="Buffer-based granular delay — reads from a circular delay buffer "
|
|
401
|
+
"at variable positions and grain sizes for textural delays.",
|
|
402
|
+
category="delay",
|
|
403
|
+
code="""\
|
|
404
|
+
Param position(0.5);
|
|
405
|
+
Param grain_size(0.05);
|
|
406
|
+
Param density(10);
|
|
407
|
+
Param mix(0.5);
|
|
408
|
+
|
|
409
|
+
grain_phase = phasor(density);
|
|
410
|
+
window = sin(grain_phase * 3.14159);
|
|
411
|
+
|
|
412
|
+
delay_samples = position * samplerate;
|
|
413
|
+
grain_samples = max(grain_size, 0.001) * samplerate;
|
|
414
|
+
read_pos = delay_samples + grain_phase * grain_samples;
|
|
415
|
+
|
|
416
|
+
wet = delay(in1, max(read_pos, 1)) * window;
|
|
417
|
+
|
|
418
|
+
out1 = in1 * (1 - mix) + wet * mix;""",
|
|
419
|
+
params=[
|
|
420
|
+
GenExprParam(name="position", default=0.5, min_val=0.0, max_val=2.0),
|
|
421
|
+
GenExprParam(name="grain_size", default=0.05, min_val=0.001, max_val=0.5),
|
|
422
|
+
GenExprParam(name="density", default=10, min_val=1, max_val=100),
|
|
423
|
+
GenExprParam(name="mix", default=0.5, min_val=0.0, max_val=1.0, unit_style=UNIT_STYLE_PERCENT),
|
|
424
|
+
],
|
|
425
|
+
))
|
|
426
|
+
|
|
427
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
428
|
+
# FILTER
|
|
429
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
430
|
+
|
|
431
|
+
_register(GenExprTemplate(
|
|
432
|
+
template_id="resonator",
|
|
433
|
+
name="Comb Resonator",
|
|
434
|
+
description="Tuned comb filter that resonates at a specific frequency — "
|
|
435
|
+
"feed it noise for metallic tones, or audio for pitched resonance.",
|
|
436
|
+
category="filter",
|
|
437
|
+
code="""\
|
|
438
|
+
Param freq(440);
|
|
439
|
+
Param resonance(0.9);
|
|
440
|
+
Param mix(0.5);
|
|
441
|
+
|
|
442
|
+
History fb(0);
|
|
443
|
+
|
|
444
|
+
delay_samples = samplerate / max(freq, 20);
|
|
445
|
+
delayed = delay(fb, delay_samples);
|
|
446
|
+
fb = in1 + delayed * min(resonance, 0.999);
|
|
447
|
+
|
|
448
|
+
out1 = in1 * (1 - mix) + fb * mix;""",
|
|
449
|
+
params=[
|
|
450
|
+
GenExprParam(name="freq", default=440, min_val=20, max_val=20000, unit_style=UNIT_STYLE_HERTZ),
|
|
451
|
+
GenExprParam(name="resonance", default=0.9, min_val=0.0, max_val=0.999),
|
|
452
|
+
GenExprParam(name="mix", default=0.5, min_val=0.0, max_val=1.0, unit_style=UNIT_STYLE_PERCENT),
|
|
453
|
+
],
|
|
454
|
+
))
|
|
455
|
+
|
|
456
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
457
|
+
# TEXTURE
|
|
458
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
459
|
+
|
|
460
|
+
_register(GenExprTemplate(
|
|
461
|
+
template_id="stochastic_resonance",
|
|
462
|
+
name="Stochastic Resonance",
|
|
463
|
+
description="Noise + threshold + feedback system — adding controlled noise to a "
|
|
464
|
+
"nonlinear threshold creates granular textures and organic movement.",
|
|
465
|
+
category="texture",
|
|
466
|
+
code="""\
|
|
467
|
+
Param threshold(0.3);
|
|
468
|
+
Param noise_amount(0.2);
|
|
469
|
+
Param feedback(0.5);
|
|
470
|
+
|
|
471
|
+
History state(0);
|
|
472
|
+
|
|
473
|
+
noisy = in1 + noise() * noise_amount + state * feedback;
|
|
474
|
+
triggered = noisy > threshold;
|
|
475
|
+
state = triggered * 0.8 + state * 0.2;
|
|
476
|
+
|
|
477
|
+
out1 = (triggered * 2 - 1) * 0.5;""",
|
|
478
|
+
params=[
|
|
479
|
+
GenExprParam(name="threshold", default=0.3, min_val=0.0, max_val=1.0),
|
|
480
|
+
GenExprParam(name="noise_amount", default=0.2, min_val=0.0, max_val=1.0),
|
|
481
|
+
GenExprParam(name="feedback", default=0.5, min_val=0.0, max_val=0.95),
|
|
482
|
+
],
|
|
483
|
+
))
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Device Forge MCP tools — generate M4L devices from gen~ code.
|
|
2
|
+
|
|
3
|
+
3 tools for the device_forge domain.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from fastmcp import Context
|
|
12
|
+
|
|
13
|
+
from ..server import mcp
|
|
14
|
+
from .models import DeviceSpec, DeviceType, GenExprParam
|
|
15
|
+
from .builder import build_device, ABLETON_USER_LIBRARY, _SUBDIR_MAP
|
|
16
|
+
from .templates import get_template, list_templates, list_categories
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.tool()
|
|
20
|
+
async def generate_m4l_effect(
|
|
21
|
+
ctx: Context,
|
|
22
|
+
name: str,
|
|
23
|
+
gen_code: str,
|
|
24
|
+
description: str = "Generated by LivePilot Device Forge",
|
|
25
|
+
device_type: str = "audio_effect",
|
|
26
|
+
params: list[dict] | None = None,
|
|
27
|
+
install: bool = True,
|
|
28
|
+
) -> dict:
|
|
29
|
+
"""Generate a Max for Live device from gen~ codebox code.
|
|
30
|
+
|
|
31
|
+
The gen_code parameter accepts GenExpr DSP code that will be compiled
|
|
32
|
+
at runtime by Max's gen~ engine. Safety clipping is automatically added.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
name: Device name (used for filename and display)
|
|
36
|
+
gen_code: GenExpr source code for the gen~ codebox
|
|
37
|
+
description: Device description
|
|
38
|
+
device_type: "audio_effect", "midi_effect", "instrument", "midi_generator", or "midi_transformation"
|
|
39
|
+
params: List of parameter dicts with keys: name, default, min_val, max_val, unit_style
|
|
40
|
+
install: If True, copy to Ableton User Library automatically
|
|
41
|
+
"""
|
|
42
|
+
type_map = {
|
|
43
|
+
"audio_effect": DeviceType.AUDIO_EFFECT,
|
|
44
|
+
"midi_effect": DeviceType.MIDI_EFFECT,
|
|
45
|
+
"instrument": DeviceType.INSTRUMENT,
|
|
46
|
+
"midi_generator": DeviceType.MIDI_GENERATOR,
|
|
47
|
+
"midi_transformation": DeviceType.MIDI_TRANSFORMATION,
|
|
48
|
+
}
|
|
49
|
+
dt = type_map.get(device_type)
|
|
50
|
+
if dt is None:
|
|
51
|
+
return {"error": f"Invalid device_type: {device_type}. Use: audio_effect, midi_effect, instrument"}
|
|
52
|
+
|
|
53
|
+
parsed_params = []
|
|
54
|
+
if params:
|
|
55
|
+
for p in params:
|
|
56
|
+
parsed_params.append(GenExprParam(
|
|
57
|
+
name=p["name"],
|
|
58
|
+
default=p.get("default", 0.5),
|
|
59
|
+
min_val=p.get("min_val", 0.0),
|
|
60
|
+
max_val=p.get("max_val", 1.0),
|
|
61
|
+
unit_style=p.get("unit_style", 1),
|
|
62
|
+
))
|
|
63
|
+
|
|
64
|
+
spec = DeviceSpec(
|
|
65
|
+
name=name,
|
|
66
|
+
device_type=dt,
|
|
67
|
+
gen_code=gen_code,
|
|
68
|
+
description=description,
|
|
69
|
+
params=parsed_params,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if install:
|
|
73
|
+
subdir = _SUBDIR_MAP[dt]
|
|
74
|
+
output_dir = ABLETON_USER_LIBRARY / subdir
|
|
75
|
+
path = build_device(spec, output_dir=output_dir)
|
|
76
|
+
else:
|
|
77
|
+
path = build_device(spec, output_dir=tempfile.mkdtemp(prefix="livepilot_forge_"))
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
"status": "created",
|
|
81
|
+
"name": spec.name,
|
|
82
|
+
"filename": spec.safe_filename,
|
|
83
|
+
"path": str(path),
|
|
84
|
+
"device_type": device_type,
|
|
85
|
+
"installed": install,
|
|
86
|
+
"param_count": len(parsed_params),
|
|
87
|
+
"hint": "Device will appear in Ableton's browser. Use find_and_load_device to load it onto a track.",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@mcp.tool()
|
|
92
|
+
async def list_genexpr_templates(
|
|
93
|
+
ctx: Context,
|
|
94
|
+
category: str = "",
|
|
95
|
+
) -> dict:
|
|
96
|
+
"""List available gen~ DSP building block templates.
|
|
97
|
+
|
|
98
|
+
Templates are pre-built GenExpr algorithms that can be used directly
|
|
99
|
+
or as starting points for custom devices. Each template has working
|
|
100
|
+
code, parameters, and descriptions.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
category: Filter by category (chaos, delay, distortion, filter, modulation, synthesis, texture, utility). Empty = all.
|
|
104
|
+
"""
|
|
105
|
+
templates = list_templates(category=category)
|
|
106
|
+
categories = list_categories()
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"templates": templates,
|
|
110
|
+
"count": len(templates),
|
|
111
|
+
"categories": sorted(categories),
|
|
112
|
+
"hint": "Use generate_m4l_effect with a template's code to create a device, or write custom GenExpr.",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@mcp.tool()
|
|
117
|
+
async def install_m4l_device(
|
|
118
|
+
ctx: Context,
|
|
119
|
+
source_path: str,
|
|
120
|
+
) -> dict:
|
|
121
|
+
"""Copy a .amxd file to Ableton's User Library.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
source_path: Path to the .amxd file to install
|
|
125
|
+
"""
|
|
126
|
+
src = Path(source_path)
|
|
127
|
+
if not src.exists():
|
|
128
|
+
return {"error": f"File not found: {source_path}"}
|
|
129
|
+
if src.suffix != ".amxd":
|
|
130
|
+
return {"error": f"Not an .amxd file: {source_path}"}
|
|
131
|
+
|
|
132
|
+
data = src.read_bytes()
|
|
133
|
+
if len(data) < 12 or data[:4] != b"ampf":
|
|
134
|
+
return {"error": "Invalid .amxd file — missing ampf header"}
|
|
135
|
+
|
|
136
|
+
marker = data[8:12]
|
|
137
|
+
type_map = {
|
|
138
|
+
b"aaaa": (DeviceType.AUDIO_EFFECT, "audio_effect"),
|
|
139
|
+
b"mmmm": (DeviceType.MIDI_EFFECT, "midi_effect"),
|
|
140
|
+
b"iiii": (DeviceType.INSTRUMENT, "instrument"),
|
|
141
|
+
b"nagg": (DeviceType.MIDI_GENERATOR, "midi_generator"),
|
|
142
|
+
b"natt": (DeviceType.MIDI_TRANSFORMATION, "midi_transformation"),
|
|
143
|
+
}
|
|
144
|
+
dt_info = type_map.get(marker)
|
|
145
|
+
if dt_info is None:
|
|
146
|
+
return {"error": f"Unknown device type marker: {marker}"}
|
|
147
|
+
|
|
148
|
+
dt, type_name = dt_info
|
|
149
|
+
subdir = _SUBDIR_MAP[dt]
|
|
150
|
+
dest_dir = ABLETON_USER_LIBRARY / subdir
|
|
151
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
|
|
153
|
+
dest = dest_dir / src.name
|
|
154
|
+
dest.write_bytes(data)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"status": "installed",
|
|
158
|
+
"filename": src.name,
|
|
159
|
+
"device_type": type_name,
|
|
160
|
+
"path": str(dest),
|
|
161
|
+
"hint": "Device now available in Ableton's browser.",
|
|
162
|
+
}
|
|
@@ -82,9 +82,32 @@ def find_hook_candidates(
|
|
|
82
82
|
development_potential=0.5,
|
|
83
83
|
))
|
|
84
84
|
|
|
85
|
+
# 4. Section-placement analysis: boost hooks that appear in payoff sections
|
|
86
|
+
payoff_sections = {
|
|
87
|
+
s.get("label", "").lower()
|
|
88
|
+
for s in (composition.get("sections", []) if composition else [])
|
|
89
|
+
if s.get("is_payoff")
|
|
90
|
+
} or {"chorus", "drop", "hook"}
|
|
91
|
+
|
|
92
|
+
for c in candidates:
|
|
93
|
+
# Check if hook is present in payoff sections (via motif locations)
|
|
94
|
+
if c.hook_type == "melodic" and motif_data:
|
|
95
|
+
for motif in motif_data.get("motifs", []):
|
|
96
|
+
if motif.get("name", "") in c.hook_id:
|
|
97
|
+
# Motif with high recurrence across sections = stronger hook
|
|
98
|
+
c.memorability = min(1.0, c.memorability + motif.get("recurrence", 0) * 0.2)
|
|
99
|
+
|
|
85
100
|
# Score all candidates
|
|
86
101
|
for c in candidates:
|
|
87
102
|
c.salience = _compute_salience(c)
|
|
103
|
+
# Add evidence sources
|
|
104
|
+
c.evidence_sources = []
|
|
105
|
+
if "motif_" in c.hook_id:
|
|
106
|
+
c.evidence_sources.append("motif_recurrence")
|
|
107
|
+
if "track_" in c.hook_id:
|
|
108
|
+
c.evidence_sources.append("track_name")
|
|
109
|
+
if "groove" in c.hook_id:
|
|
110
|
+
c.evidence_sources.append("clip_reuse")
|
|
88
111
|
|
|
89
112
|
# Sort by salience
|
|
90
113
|
candidates.sort(key=lambda c: c.salience, reverse=True)
|
|
@@ -19,6 +19,7 @@ class HookCandidate:
|
|
|
19
19
|
contrast_potential: float = 0.0 # 0-1 how well it stands out
|
|
20
20
|
development_potential: float = 0.0 # 0-1 how much room to develop
|
|
21
21
|
salience: float = 0.0 # composite score
|
|
22
|
+
evidence_sources: list[str] = field(default_factory=list) # what data informed this
|
|
22
23
|
|
|
23
24
|
def to_dict(self) -> dict:
|
|
24
25
|
return asdict(self)
|
|
@@ -53,9 +53,11 @@ def _fetch_tracks_and_scenes(ctx: Context) -> tuple[list[dict], list[dict], dict
|
|
|
53
53
|
except Exception:
|
|
54
54
|
pass
|
|
55
55
|
|
|
56
|
-
# Fetch motif data
|
|
56
|
+
# Fetch motif data — via shared motif service
|
|
57
57
|
try:
|
|
58
|
-
|
|
58
|
+
from ..services.motif_service import get_motif_data, fetch_notes_from_ableton
|
|
59
|
+
notes_by_track = fetch_notes_from_ableton(ableton, tracks)
|
|
60
|
+
motif_data = get_motif_data(notes_by_track)
|
|
59
61
|
except Exception:
|
|
60
62
|
pass # Motif graph requires notes in clips; empty dict is valid fallback
|
|
61
63
|
|