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,427 @@
|
|
|
1
|
+
"""Layer planner — convert CompositionIntent into LayerSpec list.
|
|
2
|
+
|
|
3
|
+
Pure computation. Determines which layers to create, what to search for,
|
|
4
|
+
which techniques to use, and how to arrange sections. No I/O.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .prompt_parser import CompositionIntent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ── Data Model ─────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class LayerSpec:
|
|
20
|
+
"""Specification for a single layer in a composition."""
|
|
21
|
+
|
|
22
|
+
role: str # "drums", "bass", "lead", "pad", "texture", "vocal", "percussion", "fx"
|
|
23
|
+
search_query: str # Splice search query
|
|
24
|
+
splice_filters: dict = field(default_factory=dict) # key, bpm_range, genre, tags, sample_type
|
|
25
|
+
technique_id: str = "" # from the 29-technique library
|
|
26
|
+
processing: list[dict] = field(default_factory=list) # devices to add + param targets
|
|
27
|
+
volume_db: float = 0.0 # mix level
|
|
28
|
+
pan: float = 0.0 # -1.0 to 1.0
|
|
29
|
+
sections: list[str] = field(default_factory=list) # which arrangement sections
|
|
30
|
+
priority: int = 5 # download order (1=first, 10=last)
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
return {
|
|
34
|
+
"role": self.role,
|
|
35
|
+
"search_query": self.search_query,
|
|
36
|
+
"splice_filters": self.splice_filters,
|
|
37
|
+
"technique_id": self.technique_id,
|
|
38
|
+
"processing": self.processing,
|
|
39
|
+
"volume_db": self.volume_db,
|
|
40
|
+
"pan": self.pan,
|
|
41
|
+
"sections": self.sections,
|
|
42
|
+
"priority": self.priority,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── Role Templates ─────────────────────────────────────────────────
|
|
47
|
+
# role → default config used to build LayerSpec
|
|
48
|
+
|
|
49
|
+
_ROLE_TEMPLATES: dict[str, dict] = {
|
|
50
|
+
"drums": {
|
|
51
|
+
"query_template": "{genre} drums {tempo}bpm",
|
|
52
|
+
"sample_type": "loop",
|
|
53
|
+
"technique_id": "slice_and_sequence",
|
|
54
|
+
"processing": [
|
|
55
|
+
{"name": "EQ Eight", "params": {"1 Filter Type A": "highpass", "1 Frequency A": 30.0}},
|
|
56
|
+
{"name": "Compressor", "params": {"Threshold": -12.0, "Ratio": 4.0}},
|
|
57
|
+
],
|
|
58
|
+
"volume_db": -3.0,
|
|
59
|
+
"pan": 0.0,
|
|
60
|
+
"priority": 1,
|
|
61
|
+
},
|
|
62
|
+
"bass": {
|
|
63
|
+
"query_template": "{genre} bass {key} oneshot",
|
|
64
|
+
"sample_type": "oneshot",
|
|
65
|
+
"technique_id": "key_matched_layer",
|
|
66
|
+
"processing": [
|
|
67
|
+
{"name": "Saturator", "params": {"Drive": 6.0}},
|
|
68
|
+
{"name": "EQ Eight", "params": {"1 Filter Type A": "highpass", "1 Frequency A": 30.0}},
|
|
69
|
+
],
|
|
70
|
+
"volume_db": -5.0,
|
|
71
|
+
"pan": 0.0,
|
|
72
|
+
"priority": 2,
|
|
73
|
+
},
|
|
74
|
+
"lead": {
|
|
75
|
+
"query_template": "{genre} {mood} melody {key}",
|
|
76
|
+
"sample_type": "loop",
|
|
77
|
+
"technique_id": "counterpoint_from_chops",
|
|
78
|
+
"processing": [
|
|
79
|
+
{"name": "Auto Filter", "params": {"Frequency": 2000.0, "Resonance": 0.3}},
|
|
80
|
+
{"name": "Delay", "params": {"Feedback": 0.35}},
|
|
81
|
+
],
|
|
82
|
+
"volume_db": -6.0,
|
|
83
|
+
"pan": 0.0,
|
|
84
|
+
"priority": 4,
|
|
85
|
+
},
|
|
86
|
+
"pad": {
|
|
87
|
+
"query_template": "{mood} pad {key}",
|
|
88
|
+
"sample_type": "loop",
|
|
89
|
+
"technique_id": "extreme_stretch",
|
|
90
|
+
"processing": [
|
|
91
|
+
{"name": "Reverb", "params": {"Decay Time": 4.0, "Dry/Wet": 0.6}},
|
|
92
|
+
{"name": "Chorus-Ensemble", "params": {"Rate 1": 0.5}},
|
|
93
|
+
],
|
|
94
|
+
"volume_db": -10.0,
|
|
95
|
+
"pan": 0.0,
|
|
96
|
+
"priority": 5,
|
|
97
|
+
},
|
|
98
|
+
"texture": {
|
|
99
|
+
"query_template": "{mood} texture ambient",
|
|
100
|
+
"sample_type": "loop",
|
|
101
|
+
"technique_id": "granular_scatter",
|
|
102
|
+
"processing": [
|
|
103
|
+
{"name": "Grain Delay", "params": {"Frequency": 1000.0, "Dry/Wet": 0.5}},
|
|
104
|
+
{"name": "Reverb", "params": {"Decay Time": 6.0, "Dry/Wet": 0.7}},
|
|
105
|
+
],
|
|
106
|
+
"volume_db": -15.0,
|
|
107
|
+
"pan": 0.0,
|
|
108
|
+
"priority": 6,
|
|
109
|
+
},
|
|
110
|
+
"vocal": {
|
|
111
|
+
"query_template": "vocal {mood} {key}",
|
|
112
|
+
"sample_type": "loop",
|
|
113
|
+
"technique_id": "vocal_chop_rhythm",
|
|
114
|
+
"processing": [
|
|
115
|
+
{"name": "Auto Filter", "params": {"Frequency": 3000.0}},
|
|
116
|
+
{"name": "Reverb", "params": {"Decay Time": 2.5, "Dry/Wet": 0.4}},
|
|
117
|
+
],
|
|
118
|
+
"volume_db": -8.0,
|
|
119
|
+
"pan": 0.0,
|
|
120
|
+
"priority": 7,
|
|
121
|
+
},
|
|
122
|
+
"percussion": {
|
|
123
|
+
"query_template": "{genre} percussion loop",
|
|
124
|
+
"sample_type": "loop",
|
|
125
|
+
"technique_id": "ghost_note_texture",
|
|
126
|
+
"processing": [
|
|
127
|
+
{"name": "EQ Eight", "params": {"1 Filter Type A": "highpass", "1 Frequency A": 200.0}},
|
|
128
|
+
{"name": "Compressor", "params": {"Threshold": -15.0, "Ratio": 3.0}},
|
|
129
|
+
],
|
|
130
|
+
"volume_db": -12.0,
|
|
131
|
+
"pan": 0.0,
|
|
132
|
+
"priority": 3,
|
|
133
|
+
},
|
|
134
|
+
"fx": {
|
|
135
|
+
"query_template": "{genre} riser fx",
|
|
136
|
+
"sample_type": "oneshot",
|
|
137
|
+
"technique_id": "one_sample_challenge",
|
|
138
|
+
"processing": [],
|
|
139
|
+
"volume_db": -6.0,
|
|
140
|
+
"pan": 0.0,
|
|
141
|
+
"priority": 8,
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ── Role Selection per Genre + Energy ──────────────────────────────
|
|
147
|
+
# Define which roles appear at different energy levels per genre.
|
|
148
|
+
|
|
149
|
+
_GENRE_ROLE_PRIORITY: dict[str, list[str]] = {
|
|
150
|
+
# Roles listed in order of priority (first added, last dropped)
|
|
151
|
+
"techno": ["drums", "bass", "percussion", "lead", "texture", "vocal", "fx"],
|
|
152
|
+
"house": ["drums", "bass", "lead", "pad", "vocal", "texture"],
|
|
153
|
+
"hip hop": ["drums", "bass", "lead", "vocal", "texture", "fx"],
|
|
154
|
+
"ambient": ["pad", "texture", "vocal", "lead", "percussion"],
|
|
155
|
+
"drum and bass": ["drums", "bass", "lead", "percussion", "texture", "vocal", "fx"],
|
|
156
|
+
"trap": ["drums", "bass", "lead", "vocal", "fx", "texture"],
|
|
157
|
+
"lo-fi": ["drums", "bass", "pad", "texture", "vocal"],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_DEFAULT_ROLE_PRIORITY = ["drums", "bass", "lead", "pad", "texture", "vocal", "percussion", "fx"]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── Section Templates ──────────────────────────────────────────────
|
|
164
|
+
# Each section: name, bar count, which roles play (with optional volume offset)
|
|
165
|
+
|
|
166
|
+
SECTION_TEMPLATES: dict[str, list[dict]] = {
|
|
167
|
+
"techno": [
|
|
168
|
+
{"name": "Intro", "bars": 8, "layers": ["drums:-6dB", "texture"]},
|
|
169
|
+
{"name": "Build", "bars": 8, "layers": ["drums", "bass", "percussion"]},
|
|
170
|
+
{"name": "Drop", "bars": 16, "layers": ["drums", "bass", "lead", "percussion", "texture"]},
|
|
171
|
+
{"name": "Breakdown", "bars": 8, "layers": ["texture", "vocal", "pad"]},
|
|
172
|
+
{"name": "Drop 2", "bars": 16, "layers": ["drums", "bass", "lead", "percussion", "vocal", "texture"]},
|
|
173
|
+
{"name": "Outro", "bars": 8, "layers": ["drums:-6dB", "texture", "pad"]},
|
|
174
|
+
],
|
|
175
|
+
"house": [
|
|
176
|
+
{"name": "Intro", "bars": 8, "layers": ["drums:-6dB", "pad"]},
|
|
177
|
+
{"name": "Verse", "bars": 16, "layers": ["drums", "bass", "pad"]},
|
|
178
|
+
{"name": "Drop", "bars": 16, "layers": ["drums", "bass", "lead", "vocal"]},
|
|
179
|
+
{"name": "Breakdown", "bars": 8, "layers": ["pad", "texture", "vocal"]},
|
|
180
|
+
{"name": "Drop 2", "bars": 16, "layers": ["drums", "bass", "lead", "vocal", "texture"]},
|
|
181
|
+
{"name": "Outro", "bars": 8, "layers": ["drums:-6dB", "pad"]},
|
|
182
|
+
],
|
|
183
|
+
"hip hop": [
|
|
184
|
+
{"name": "Intro", "bars": 4, "layers": ["texture"]},
|
|
185
|
+
{"name": "Verse", "bars": 16, "layers": ["drums", "bass", "texture"]},
|
|
186
|
+
{"name": "Hook", "bars": 8, "layers": ["drums", "bass", "lead", "vocal"]},
|
|
187
|
+
{"name": "Verse 2", "bars": 16, "layers": ["drums", "bass", "percussion", "texture"]},
|
|
188
|
+
{"name": "Hook 2", "bars": 8, "layers": ["drums", "bass", "lead", "vocal", "fx"]},
|
|
189
|
+
{"name": "Outro", "bars": 4, "layers": ["texture", "vocal:-10dB"]},
|
|
190
|
+
],
|
|
191
|
+
"ambient": [
|
|
192
|
+
{"name": "Opening", "bars": 16, "layers": ["pad", "texture"]},
|
|
193
|
+
{"name": "Evolve", "bars": 16, "layers": ["pad", "texture", "vocal"]},
|
|
194
|
+
{"name": "Peak", "bars": 16, "layers": ["pad", "texture", "vocal", "lead"]},
|
|
195
|
+
{"name": "Dissolve", "bars": 16, "layers": ["pad", "texture"]},
|
|
196
|
+
],
|
|
197
|
+
"drum and bass": [
|
|
198
|
+
{"name": "Intro", "bars": 8, "layers": ["texture", "percussion:-6dB"]},
|
|
199
|
+
{"name": "Build", "bars": 8, "layers": ["drums:-6dB", "bass", "percussion"]},
|
|
200
|
+
{"name": "Drop", "bars": 16, "layers": ["drums", "bass", "lead", "percussion", "texture"]},
|
|
201
|
+
{"name": "Breakdown", "bars": 8, "layers": ["texture", "vocal", "pad"]},
|
|
202
|
+
{"name": "Drop 2", "bars": 16, "layers": ["drums", "bass", "lead", "percussion", "vocal", "fx"]},
|
|
203
|
+
{"name": "Outro", "bars": 8, "layers": ["drums:-6dB", "texture"]},
|
|
204
|
+
],
|
|
205
|
+
"trap": [
|
|
206
|
+
{"name": "Intro", "bars": 4, "layers": ["texture"]},
|
|
207
|
+
{"name": "Verse", "bars": 16, "layers": ["drums", "bass", "texture"]},
|
|
208
|
+
{"name": "Drop", "bars": 8, "layers": ["drums", "bass", "lead", "vocal", "fx"]},
|
|
209
|
+
{"name": "Verse 2", "bars": 16, "layers": ["drums", "bass", "texture", "vocal"]},
|
|
210
|
+
{"name": "Drop 2", "bars": 8, "layers": ["drums", "bass", "lead", "vocal", "fx"]},
|
|
211
|
+
{"name": "Outro", "bars": 4, "layers": ["texture:-6dB"]},
|
|
212
|
+
],
|
|
213
|
+
"lo-fi": [
|
|
214
|
+
{"name": "Intro", "bars": 4, "layers": ["texture", "pad"]},
|
|
215
|
+
{"name": "A", "bars": 16, "layers": ["drums", "bass", "pad", "texture"]},
|
|
216
|
+
{"name": "B", "bars": 16, "layers": ["drums", "bass", "pad", "vocal"]},
|
|
217
|
+
{"name": "A2", "bars": 16, "layers": ["drums", "bass", "pad", "texture"]},
|
|
218
|
+
{"name": "Outro", "bars": 8, "layers": ["pad", "texture"]},
|
|
219
|
+
],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# Fallback template for unknown genres
|
|
223
|
+
_DEFAULT_SECTION_TEMPLATE: list[dict] = [
|
|
224
|
+
{"name": "Intro", "bars": 8, "layers": ["texture"]},
|
|
225
|
+
{"name": "Build", "bars": 8, "layers": ["drums", "bass"]},
|
|
226
|
+
{"name": "Main", "bars": 16, "layers": ["drums", "bass", "lead", "texture"]},
|
|
227
|
+
{"name": "Breakdown", "bars": 8, "layers": ["pad", "texture"]},
|
|
228
|
+
{"name": "Main 2", "bars": 16, "layers": ["drums", "bass", "lead", "vocal", "texture"]},
|
|
229
|
+
{"name": "Outro", "bars": 8, "layers": ["drums:-6dB", "texture"]},
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ── Planner Functions ──────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
def _build_search_query(template: str, intent: CompositionIntent) -> str:
|
|
236
|
+
"""Fill a query template with intent fields."""
|
|
237
|
+
return template.format(
|
|
238
|
+
genre=intent.genre or "electronic",
|
|
239
|
+
mood=intent.mood or "",
|
|
240
|
+
key=intent.key or "",
|
|
241
|
+
tempo=intent.tempo or 120,
|
|
242
|
+
).strip()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _build_splice_filters(
|
|
246
|
+
intent: CompositionIntent,
|
|
247
|
+
sample_type: str,
|
|
248
|
+
) -> dict:
|
|
249
|
+
"""Build Splice filter dict from intent."""
|
|
250
|
+
filters: dict = {}
|
|
251
|
+
|
|
252
|
+
# Key → Splice format (lowercase root, separate chord_type)
|
|
253
|
+
if intent.key:
|
|
254
|
+
key = intent.key
|
|
255
|
+
if key.endswith("m") and len(key) >= 2:
|
|
256
|
+
root = key[:-1].lower()
|
|
257
|
+
filters["chord_type"] = "minor"
|
|
258
|
+
else:
|
|
259
|
+
root = key.lower()
|
|
260
|
+
filters["chord_type"] = "major"
|
|
261
|
+
filters["key"] = root
|
|
262
|
+
|
|
263
|
+
# BPM range (+-5)
|
|
264
|
+
if intent.tempo:
|
|
265
|
+
filters["bpm_min"] = max(1, intent.tempo - 5)
|
|
266
|
+
filters["bpm_max"] = intent.tempo + 5
|
|
267
|
+
|
|
268
|
+
if intent.genre:
|
|
269
|
+
filters["genre"] = intent.genre
|
|
270
|
+
|
|
271
|
+
if sample_type:
|
|
272
|
+
filters["sample_type"] = sample_type
|
|
273
|
+
|
|
274
|
+
return filters
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _select_roles(intent: CompositionIntent) -> list[str]:
|
|
278
|
+
"""Select which roles to include based on genre, energy, and explicit elements."""
|
|
279
|
+
role_priority = _GENRE_ROLE_PRIORITY.get(intent.genre, _DEFAULT_ROLE_PRIORITY)
|
|
280
|
+
|
|
281
|
+
# How many layers to pick
|
|
282
|
+
count = intent.layer_count or 5
|
|
283
|
+
|
|
284
|
+
# Start with the top N roles by priority
|
|
285
|
+
roles = list(role_priority[:count])
|
|
286
|
+
|
|
287
|
+
# Add any explicitly requested elements as roles
|
|
288
|
+
element_to_role = {
|
|
289
|
+
"vocal": "vocal",
|
|
290
|
+
"808": "bass",
|
|
291
|
+
"bass": "bass",
|
|
292
|
+
"drums": "drums",
|
|
293
|
+
"percussion": "percussion",
|
|
294
|
+
"pad": "pad",
|
|
295
|
+
"texture": "texture",
|
|
296
|
+
"fx": "fx",
|
|
297
|
+
"strings": "pad", # strings map to pad role
|
|
298
|
+
"piano": "lead", # piano maps to lead role
|
|
299
|
+
"guitar": "lead",
|
|
300
|
+
"brass": "lead",
|
|
301
|
+
"synth": "lead",
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for element in intent.explicit_elements:
|
|
305
|
+
role = element_to_role.get(element)
|
|
306
|
+
if role and role not in roles:
|
|
307
|
+
roles.append(role)
|
|
308
|
+
|
|
309
|
+
return roles
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def plan_layers(intent: CompositionIntent) -> list[LayerSpec]:
|
|
313
|
+
"""Convert a CompositionIntent into a list of LayerSpec.
|
|
314
|
+
|
|
315
|
+
Each LayerSpec describes one track to create: what to search for,
|
|
316
|
+
which technique to use, processing chain, and mix settings.
|
|
317
|
+
"""
|
|
318
|
+
roles = _select_roles(intent)
|
|
319
|
+
sections = plan_sections(intent)
|
|
320
|
+
section_names = [s["name"] for s in sections]
|
|
321
|
+
|
|
322
|
+
layers: list[LayerSpec] = []
|
|
323
|
+
|
|
324
|
+
for role in roles:
|
|
325
|
+
template = _ROLE_TEMPLATES.get(role)
|
|
326
|
+
if not template:
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
# Build search query
|
|
330
|
+
query = _build_search_query(template["query_template"], intent)
|
|
331
|
+
|
|
332
|
+
# Add descriptors to query for richer searches
|
|
333
|
+
if intent.descriptors:
|
|
334
|
+
query += " " + " ".join(intent.descriptors[:2])
|
|
335
|
+
|
|
336
|
+
# Build Splice filters
|
|
337
|
+
splice_filters = _build_splice_filters(intent, template["sample_type"])
|
|
338
|
+
|
|
339
|
+
# Determine which sections this role appears in
|
|
340
|
+
role_sections: list[str] = []
|
|
341
|
+
for section in sections:
|
|
342
|
+
section_layers = section.get("layers", [])
|
|
343
|
+
for layer_ref in section_layers:
|
|
344
|
+
# Parse "drums:-6dB" → "drums"
|
|
345
|
+
layer_role = layer_ref.split(":")[0]
|
|
346
|
+
if layer_role == role:
|
|
347
|
+
role_sections.append(section["name"])
|
|
348
|
+
break
|
|
349
|
+
# If no section template match, include in all sections
|
|
350
|
+
if not role_sections:
|
|
351
|
+
role_sections = section_names
|
|
352
|
+
|
|
353
|
+
# Pan spread for stereo width
|
|
354
|
+
pan = _compute_pan(role, intent.energy)
|
|
355
|
+
|
|
356
|
+
layer = LayerSpec(
|
|
357
|
+
role=role,
|
|
358
|
+
search_query=query,
|
|
359
|
+
splice_filters=splice_filters,
|
|
360
|
+
technique_id=template["technique_id"],
|
|
361
|
+
processing=list(template["processing"]), # copy
|
|
362
|
+
volume_db=template["volume_db"],
|
|
363
|
+
pan=pan,
|
|
364
|
+
sections=role_sections,
|
|
365
|
+
priority=template["priority"],
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
layers.append(layer)
|
|
369
|
+
|
|
370
|
+
# Sort by priority (drums first, fx last)
|
|
371
|
+
layers.sort(key=lambda l: l.priority)
|
|
372
|
+
|
|
373
|
+
return layers
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def plan_sections(intent: CompositionIntent) -> list[dict]:
|
|
377
|
+
"""Plan arrangement sections based on genre and duration.
|
|
378
|
+
|
|
379
|
+
Returns a list of dicts: {name, bars, layers, start_bar}.
|
|
380
|
+
"""
|
|
381
|
+
template = SECTION_TEMPLATES.get(intent.genre, _DEFAULT_SECTION_TEMPLATE)
|
|
382
|
+
|
|
383
|
+
# Scale sections to fit duration_bars
|
|
384
|
+
total_template_bars = sum(s["bars"] for s in template)
|
|
385
|
+
if total_template_bars == 0:
|
|
386
|
+
total_template_bars = 64
|
|
387
|
+
|
|
388
|
+
scale = intent.duration_bars / total_template_bars
|
|
389
|
+
|
|
390
|
+
sections: list[dict] = []
|
|
391
|
+
current_bar = 0
|
|
392
|
+
|
|
393
|
+
for section in template:
|
|
394
|
+
scaled_bars = max(4, round(section["bars"] * scale))
|
|
395
|
+
# Round to nearest 4 bars
|
|
396
|
+
scaled_bars = max(4, (scaled_bars // 4) * 4)
|
|
397
|
+
|
|
398
|
+
sections.append({
|
|
399
|
+
"name": section["name"],
|
|
400
|
+
"bars": scaled_bars,
|
|
401
|
+
"layers": list(section["layers"]),
|
|
402
|
+
"start_bar": current_bar,
|
|
403
|
+
})
|
|
404
|
+
current_bar += scaled_bars
|
|
405
|
+
|
|
406
|
+
return sections
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _compute_pan(role: str, energy: float) -> float:
|
|
410
|
+
"""Compute pan position for a role.
|
|
411
|
+
|
|
412
|
+
Core elements (drums, bass) stay centered.
|
|
413
|
+
Support elements get wider spread at higher energy.
|
|
414
|
+
"""
|
|
415
|
+
_PAN_MAP = {
|
|
416
|
+
"drums": 0.0,
|
|
417
|
+
"bass": 0.0,
|
|
418
|
+
"lead": 0.0,
|
|
419
|
+
"pad": 0.0,
|
|
420
|
+
"vocal": 0.0,
|
|
421
|
+
"percussion": 0.3,
|
|
422
|
+
"texture": -0.3,
|
|
423
|
+
"fx": 0.4,
|
|
424
|
+
}
|
|
425
|
+
base_pan = _PAN_MAP.get(role, 0.0)
|
|
426
|
+
# Widen slightly with energy
|
|
427
|
+
return base_pan * (0.5 + 0.5 * energy)
|