RAM2118 commited on
Commit
5d29a69
Β·
verified Β·
1 Parent(s): 13b72e6

Upload emotional_engine.py

Browse files
Files changed (1) hide show
  1. emotional_engine.py +289 -0
emotional_engine.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Emotional Context Engine for Harmonic Catalyst
3
+ Version: 1.1.0 (Phase 1 - Wired to Generation)
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from typing import List
8
+
9
+
10
+ @dataclass
11
+ class EmotionalContext:
12
+ """Stores emotional parameters for a section"""
13
+ energy: float = 0.5
14
+ density: str = 'Medium'
15
+ role: str = 'Support'
16
+ movement: str = 'Flowing'
17
+
18
+
19
+ # Section types for dropdown
20
+ SECTION_TYPES = [
21
+ 'Intro',
22
+ 'Verse',
23
+ 'Pre-Chorus',
24
+ 'Chorus',
25
+ 'Post-Chorus',
26
+ 'Bridge',
27
+ 'Breakdown',
28
+ 'Drop',
29
+ 'Outro',
30
+ 'Custom'
31
+ ]
32
+
33
+ # Sections that get numbered
34
+ NUMBERED_SECTIONS = ['Verse', 'Pre-Chorus', 'Chorus', 'Post-Chorus', 'Drop']
35
+
36
+ # Sections that stay single (no number)
37
+ SINGLE_SECTIONS = ['Intro', 'Bridge', 'Breakdown', 'Outro']
38
+
39
+ # Emotional presets
40
+ EMOTIONAL_PRESETS = {
41
+ "Sparse Intro": {
42
+ 'energy': 0.1,
43
+ 'density': 'Sparse',
44
+ 'role': 'Ambient',
45
+ 'movement': 'Static',
46
+ 'description': 'Minimal, atmospheric opening'
47
+ },
48
+ "Supportive Verse": {
49
+ 'energy': 0.4,
50
+ 'density': 'Medium',
51
+ 'role': 'Support',
52
+ 'movement': 'Flowing',
53
+ 'description': 'Behind vocals, smooth transitions'
54
+ },
55
+ "Building Pre-Chorus": {
56
+ 'energy': 0.6,
57
+ 'density': 'Medium',
58
+ 'role': 'Lead',
59
+ 'movement': 'Flowing',
60
+ 'description': 'Increasing tension toward chorus'
61
+ },
62
+ "Explosive Chorus": {
63
+ 'energy': 0.9,
64
+ 'density': 'Thick',
65
+ 'role': 'Lead',
66
+ 'movement': 'Agitated',
67
+ 'description': 'Maximum impact and fullness'
68
+ },
69
+ "Breakdown Bridge": {
70
+ 'energy': 0.3,
71
+ 'density': 'Sparse',
72
+ 'role': 'Ambient',
73
+ 'movement': 'Static',
74
+ 'description': 'Stripped-down, suspended feel'
75
+ },
76
+ "Resolved Outro": {
77
+ 'energy': 0.2,
78
+ 'density': 'Sparse',
79
+ 'role': 'Ambient',
80
+ 'movement': 'Static',
81
+ 'description': 'Peaceful, conclusive ending'
82
+ }
83
+ }
84
+
85
+ # Default preset for each section type
86
+ SECTION_TYPE_DEFAULTS = {
87
+ 'Intro': 'Sparse Intro',
88
+ 'Verse': 'Supportive Verse',
89
+ 'Pre-Chorus': 'Building Pre-Chorus',
90
+ 'Chorus': 'Explosive Chorus',
91
+ 'Post-Chorus': 'Explosive Chorus',
92
+ 'Bridge': 'Breakdown Bridge',
93
+ 'Breakdown': 'Breakdown Bridge',
94
+ 'Drop': 'Explosive Chorus',
95
+ 'Outro': 'Resolved Outro',
96
+ 'Custom': 'Supportive Verse'
97
+ }
98
+
99
+
100
+ class SectionNamer:
101
+ """Handles auto-numbering of sections"""
102
+
103
+ @staticmethod
104
+ def generate_name(section_type: str, existing_sections: List[dict]) -> str:
105
+ if section_type == 'Custom':
106
+ return 'Custom Section'
107
+
108
+ if section_type in SINGLE_SECTIONS:
109
+ return section_type
110
+
111
+ if section_type in NUMBERED_SECTIONS:
112
+ count = sum(
113
+ 1 for s in existing_sections
114
+ if s.get('section_type') == section_type
115
+ )
116
+ return f"{section_type} {count + 1}"
117
+
118
+ return section_type
119
+
120
+ @staticmethod
121
+ def get_default_preset(section_type: str) -> str:
122
+ return SECTION_TYPE_DEFAULTS.get(section_type, 'Supportive Verse')
123
+
124
+
125
+ class EmotionalAdapter:
126
+ """Adapts voicings based on emotional context"""
127
+
128
+ @staticmethod
129
+ def adapt_solo_piano(lh: List[int], rh: List[int], emotional_ctx: EmotionalContext) -> tuple:
130
+ """
131
+ Adapt voicing for solo piano performance.
132
+
133
+ Args:
134
+ lh: Left hand MIDI notes
135
+ rh: Right hand MIDI notes
136
+ emotional_ctx: Emotional parameters
137
+
138
+ Returns:
139
+ Tuple of (adapted_lh, adapted_rh)
140
+ """
141
+ lh = list(lh) if lh else []
142
+ rh = list(rh) if rh else []
143
+
144
+ # ─────────────────────────────────────────────────────
145
+ # ENERGY ADAPTATION
146
+ # ─────────────────────────────────────────────────────
147
+
148
+ if emotional_ctx.energy < 0.3:
149
+ # Low energy: Quieter, thinner
150
+ if len(rh) > 3:
151
+ rh = rh[:3]
152
+ if len(lh) > 1:
153
+ lh = lh[:1]
154
+
155
+ elif emotional_ctx.energy > 0.7:
156
+ # High energy: Fuller, doubled
157
+ if lh and len(lh) < 3:
158
+ lh.append(lh[0] + 12) # Add octave
159
+ if rh and len(rh) < 6:
160
+ rh.append(rh[0] + 12) # Add octave doubling
161
+
162
+ # ─────────────────────────────────────────────────────
163
+ # DENSITY ADAPTATION
164
+ # ─────────────────────────────────────────────��───────
165
+
166
+ if emotional_ctx.density == 'Sparse':
167
+ lh = lh[:1] if lh else []
168
+ rh = rh[:2] if len(rh) > 2 else rh
169
+
170
+ elif emotional_ctx.density == 'Thick':
171
+ if lh and len(lh) < 2:
172
+ lh.append(lh[0] + 12)
173
+ if rh and len(rh) < 5:
174
+ lowest = min(rh) if rh else 60
175
+ rh.append(lowest + 12)
176
+ if len(rh) < 6 and rh:
177
+ highest = max(rh)
178
+ rh.append(highest + 12)
179
+
180
+ # Clean up
181
+ lh = sorted(list(set(lh))) if lh else []
182
+ rh = sorted(list(set(rh))) if rh else []
183
+
184
+ return lh, rh
185
+
186
+ @staticmethod
187
+ def adapt_arrangement(lh: List[int], rh: List[int], emotional_ctx: EmotionalContext) -> tuple:
188
+ """
189
+ Adapt voicing for full band arrangement.
190
+
191
+ Args:
192
+ lh: Left hand (Bass Part) MIDI notes
193
+ rh: Right hand (Chord Part) MIDI notes
194
+ emotional_ctx: Emotional parameters
195
+
196
+ Returns:
197
+ Tuple of (adapted_lh, adapted_rh)
198
+ """
199
+ lh = list(lh) if lh else []
200
+ rh = list(rh) if rh else []
201
+
202
+ # ─────────────────────────────────────────────────────
203
+ # ENERGY ADAPTATION
204
+ # ─────────────────────────────────────────────────────
205
+
206
+ if emotional_ctx.energy < 0.3:
207
+ # Low energy: Minimal arrangement
208
+ lh = lh[:1] if lh else []
209
+ rh = rh[:2] if len(rh) > 2 else rh
210
+
211
+ elif emotional_ctx.energy > 0.7:
212
+ # High energy: Full, spread arrangement
213
+ if lh:
214
+ root = lh[0]
215
+ if len(lh) < 3:
216
+ lh = [root, root + 12, root + 19] # Root, octave, 12th
217
+
218
+ if rh and len(rh) < 6:
219
+ rh_sorted = sorted(rh)
220
+ rh.append(rh_sorted[0] + 12)
221
+ if len(rh_sorted) > 1:
222
+ rh.append(rh_sorted[1] + 12)
223
+
224
+ # ─────────────────────────────────────────────────────
225
+ # ROLE ADAPTATION
226
+ # ─────────────────────────────────────────────────────
227
+
228
+ if emotional_ctx.role == 'Support':
229
+ # Stay out of vocal range (D4 to B4 = MIDI 62-71)
230
+ vocal_zone = range(62, 72)
231
+ rh = [n + 12 if n in vocal_zone else n for n in rh]
232
+
233
+ elif emotional_ctx.role == 'Ambient':
234
+ # Very high or very low
235
+ if emotional_ctx.energy < 0.5:
236
+ # Quiet ambient = high shimmer
237
+ if rh:
238
+ rh = [n + 24 for n in rh[:2]]
239
+ lh = lh[:1] if lh else []
240
+ else:
241
+ # Louder ambient = deep
242
+ rh = rh[:3] if len(rh) > 3 else rh
243
+
244
+ # ─────────────────────────────────────────────────────
245
+ # DENSITY ADAPTATION
246
+ # ─────────────────────────────────────────────────────
247
+
248
+ if emotional_ctx.density == 'Sparse':
249
+ lh = lh[:1] if lh else []
250
+ rh = rh[:2] if len(rh) > 2 else rh
251
+
252
+ elif emotional_ctx.density == 'Thick':
253
+ if lh and len(lh) < 3:
254
+ lh.append(lh[0] + 12)
255
+ if rh and len(rh) < 6:
256
+ lowest = min(rh) if rh else 60
257
+ highest = max(rh) if rh else 72
258
+ rh.append(lowest + 12)
259
+ rh.append(highest + 12)
260
+
261
+ # Clean up
262
+ lh = sorted(list(set(lh))) if lh else []
263
+ rh = sorted(list(set(rh))) if rh else []
264
+
265
+ return lh, rh
266
+
267
+ @staticmethod
268
+ def get_voice_leading_settings(movement: str) -> dict:
269
+ """Returns voice leading settings based on movement style"""
270
+ settings = {
271
+ 'Static': {
272
+ 'enabled': False,
273
+ 'max_movement': 24
274
+ },
275
+ 'Flowing': {
276
+ 'enabled': True,
277
+ 'max_movement': 5
278
+ },
279
+ 'Agitated': {
280
+ 'enabled': True,
281
+ 'max_movement': 12
282
+ }
283
+ }
284
+ return settings.get(movement, settings['Flowing'])
285
+
286
+
287
+ def get_section_types():
288
+ """Returns list of section types"""
289
+ return SECTION_TYPES.copy()