RAM2118 commited on
Commit
39d7f63
·
verified ·
1 Parent(s): 0483495

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +572 -326
app.py CHANGED
@@ -1,6 +1,14 @@
1
-
2
  import streamlit as st
3
  import uuid
 
 
 
 
 
 
 
 
 
4
  from harmonic_engine import (
5
  note_name_to_pc, pc_to_note_name, midi_to_name, midi_to_freq,
6
  parse_chord_symbol, roman_to_chord,
@@ -8,21 +16,99 @@ from harmonic_engine import (
8
  MidiExporter, NOTE_NAMES
9
  )
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  st.set_page_config(page_title="Harmonic Catalyst PRO", page_icon="🎹", layout="wide")
12
 
13
  st.markdown("""
14
  <style>
15
  .stApp {background-color: #0d1117; color: #c9d1d9;}
16
  .stButton>button {
17
- width: 100%; border-radius: 8px;
18
  background: linear-gradient(135deg, #238636, #2ea043);
19
- color: white; font-weight: bold; font-size: 16px; padding: 10px;
20
  }
21
  hr {border: 0; border-top: 1px solid #30363d;}
22
- .chord-header {
23
- font-size: 22px; font-weight: bold; color: #f0f6fc;
24
- border-left: 4px solid #58a6ff; padding-left: 12px; margin-bottom: 8px;
25
- }
26
  .help-box {
27
  background: #161b22;
28
  border-left: 4px solid #58a6ff;
@@ -30,161 +116,52 @@ st.markdown("""
30
  border-radius: 8px;
31
  margin: 1rem 0;
32
  }
33
- .example-box {
34
- background: #1c2128;
35
- border: 1px solid #30363d;
36
- padding: 0.75rem;
37
- border-radius: 6px;
38
- margin: 0.5rem 0;
39
- font-family: monospace;
40
- }
41
  </style>
42
  """, unsafe_allow_html=True)
43
 
44
  st.title("🎹 Harmonic Catalyst PRO")
45
- st.caption("Genre-Aware Chord Voicing Engine • Multi-Section Builder • Professional Mix Planning")
46
 
47
- # Help/Instructions Section
48
- with st.expander("📖 Quick Start Guide (Click to expand)", expanded=False):
49
- st.markdown("""
50
- ### 🎯 What This Tool Does
51
-
52
- Transform chord progressions into **professional piano voicings** across different genres,
53
- with smart voice leading and frequency-aware arrangement planning.
54
-
55
- ### 🚀 How to Use
56
-
57
- 1. **Add a Section** - Click ➕ to create verse, chorus, bridge, etc.
58
- 2. **Enter Chords** - Type your progression (supports 20+ chord types + slash chords!)
59
- 3. **Pick Genre** - Choose from 17 styles (Pop, Jazz, Gospel, Afrobeats, etc.)
60
- 4. **Generate** - See piano voicings, frequency analysis, and download MIDI
61
-
62
- ### 💡 Try These Examples
63
- """)
64
-
65
- col1, col2 = st.columns(2)
66
-
67
- with col1:
68
- st.markdown("""
69
- **🎵 Pop Ballad:**
70
- ```
71
- Section 1 (Verse): C Am F G
72
- Section 2 (Chorus): C Am F G
73
- Genre: Pop → Gospel (for lift!)
74
- ```
75
-
76
- **🎷 Jazz Standard:**
77
- ```
78
- Chords: Dm7 G7 Cmaj7 A7
79
- Genre: Jazz
80
- Tip: Add slash chords for smooth bass!
81
- ```
82
-
83
- **🌍 Afrobeats Vibe:**
84
- ```
85
- Chords: Em D C Bm
86
- Genre: Afrobeats
87
- Tip: Try same chords in Trap for contrast
88
- ```
89
- """)
90
-
91
- with col2:
92
- st.markdown("""
93
- **🎸 Slash Chord Magic:**
94
- ```
95
- C C/B Am Am/G F G/B C
96
- Creates smooth bass line!
97
- Genre: Any (works everywhere)
98
- ```
99
-
100
- **🇮🇳 Bollywood Fusion:**
101
- ```
102
- Section 1: Cmaj7 Am7 (Hindustani Classical)
103
- Section 2: Fmaj7 G7 (Pop)
104
- = Traditional intro → Modern verse
105
- ```
106
-
107
- **🎹 Genre Morphing:**
108
- ```
109
- Same chords, different sections:
110
- Verse: Neo-Soul
111
- Chorus: Gospel
112
- Bridge: Jazz
113
- ```
114
- """)
115
-
116
- st.markdown("""
117
- ### 🎼 Supported Chord Types
118
-
119
- | Type | Example | Type | Example |
120
- |------|---------|------|---------|
121
- | Major | `C`, `Cmaj` | Minor | `Cm`, `Cmin` |
122
- | Major 7th | `Cmaj7` | Minor 7th | `Cm7`, `Cmin7` |
123
- | Dominant 7th | `C7` | Suspended | `Csus2`, `Csus4` |
124
- | Diminished | `Cdim`, `Cdim7` | Augmented | `Caug`, `C+` |
125
- | Add 9 | `Cadd9` | 6th | `C6`, `Cm6` |
126
- | Extended | `C9`, `C11`, `C13` | **Slash** | `C/E`, `Am/G` |
127
-
128
- ### 🎨 All 17 Genres
129
-
130
- **Western:** Pop • Jazz • Gospel • Blues • Classical • RnB • Waltz • Neo-Soul
131
- **Modern:** Afrobeats • Trap • K-Pop • Lo-fi • Funk
132
- **World:** Bossa Nova • Hindustani Classical • Reggae • Latin
133
-
134
- ### 💡 Pro Tips
135
-
136
- - **Use Slash Chords** for smooth bass lines: `C C/B Am Am/G`
137
- - **Genre Morphing**: Same chords, different genres per section = dynamic arrangement
138
- - **Voice Leading** (enabled by default): Connects sections smoothly even across genre changes
139
- - **Frequency Analysis**: Warns if piano clashes with vocals in 160-400Hz zone
140
- - **Duplicate Sections**: Use 📋 button to copy verse → verse 2 instantly
141
-
142
- ### 🎯 Workflow Example: Recreating "Raabta"
143
-
144
- 1. Section 1: "Intro" → Chords: `Cmaj7 Am7` → Genre: Hindustani Classical
145
- 2. Section 2: "Verse" → Chords: `Cmaj7 Am7 Fmaj7 G7` → Genre: Pop
146
- 3. Section 3: "Chorus" → Same chords → Genre: Afrobeats (for modern twist!)
147
- 4. Generate → Download MIDI → Import to DAW → Add your sounds! 🎧
148
- """)
149
 
150
- with st.expander("❓ FAQ & Troubleshooting", expanded=False):
 
 
 
 
151
  st.markdown("""
152
- **Q: What's the difference between genres?**
153
- A: Each genre uses different voicing techniques:
154
- - **Jazz** = Shell voicings (root+7th in LH, 3-7-9-13 in RH)
155
- - **Gospel** = Thick clusters with stacked 4ths and suspensions
156
- - **Afrobeats** = Open 5ths, modern spacious sound
157
-
158
- **Q: When should I use slash chords?**
159
- A: When you want a specific bass note instead of the root:
160
- - `C/E` = C major chord with E in bass (smoother transitions)
161
- - Common in ballads, R&B, and jazz
162
-
163
- **Q: What does "MUD WARNING" mean?**
164
- A: Your piano has notes in 160-400Hz (where vocals sit). The tool auto-shifts them up an octave to keep your mix clean.
165
-
166
- **Q: Can I use Roman numerals?**
167
- A: Yes! Switch to Roman Numeral mode in sidebar, select your key, then use: `I vi IV V`
168
-
169
- **Q: How do I save my work?**
170
- A: Download the MIDI files, then you can reload them in your DAW anytime.
171
-
172
- **Q: Why are voicings different even with same chords?**
173
- A: That's the magic! Each genre has unique voicing rules. `Cmaj7` in Jazz sounds completely different from `Cmaj7` in Gospel.
174
  """)
175
 
 
 
 
 
 
176
  if 'song_sections' not in st.session_state:
177
  st.session_state.song_sections = []
178
 
179
  if 'section_counter' not in st.session_state:
180
  st.session_state.section_counter = 0
181
 
 
 
 
 
 
182
  def add_section():
183
  st.session_state.section_counter += 1
 
 
 
184
  new_section = {
185
  'id': str(uuid.uuid4())[:8],
186
  'number': st.session_state.section_counter,
187
- 'name': f'Section {st.session_state.section_counter}',
 
188
  'chords': '',
189
  'genre': 'Pop',
190
  'lh_octave': 2,
@@ -192,11 +169,13 @@ def add_section():
192
  }
193
  st.session_state.song_sections.append(new_section)
194
 
 
195
  def remove_section(section_id):
196
  st.session_state.song_sections = [
197
  s for s in st.session_state.song_sections if s['id'] != section_id
198
  ]
199
 
 
200
  def duplicate_section(section_id):
201
  section = next(s for s in st.session_state.song_sections if s['id'] == section_id)
202
  st.session_state.section_counter += 1
@@ -207,25 +186,31 @@ def duplicate_section(section_id):
207
  idx = st.session_state.song_sections.index(section)
208
  st.session_state.song_sections.insert(idx + 1, new_section)
209
 
 
210
  def move_section_up(section_id):
211
  sections = st.session_state.song_sections
212
  idx = next(i for i, s in enumerate(sections) if s['id'] == section_id)
213
  if idx > 0:
214
  sections[idx], sections[idx - 1] = sections[idx - 1], sections[idx]
215
 
 
216
  def move_section_down(section_id):
217
  sections = st.session_state.song_sections
218
  idx = next(i for i, s in enumerate(sections) if s['id'] == section_id)
219
  if idx < len(sections) - 1:
220
  sections[idx], sections[idx + 1] = sections[idx + 1], sections[idx]
221
 
 
 
 
 
 
222
  with st.sidebar:
223
  st.header("⚙️ Global Settings")
224
 
225
  input_mode = st.radio(
226
  "Input Mode",
227
- ["Chord Symbols (Cmaj7, Am7...)", "Roman Numerals (I, vi, IV, V)"],
228
- help="Choose how you want to enter chords. Roman numerals require selecting a key."
229
  )
230
 
231
  if "Roman" in input_mode:
@@ -235,50 +220,123 @@ with st.sidebar:
235
  key_pc = 0
236
 
237
  st.divider()
238
- context = st.radio(
239
- "🎚️ Mix Context",
240
- ["Full Band", "Solo Piano"],
241
- help="Full Band = Strict frequency checking. Solo Piano = More freedom in mid-range."
242
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
  st.divider()
245
- st.subheader("🔧 Voice Leading")
246
- use_voice_leading = st.checkbox(
247
- "🔗 Connect sections smoothly",
248
- value=True,
249
- help="Automatically finds smoothest voicings between chords (minimal hand movement)"
250
- )
251
 
252
  st.divider()
253
- use_negative = st.checkbox(
254
- "🌌 Negative Harmony",
255
- value=False,
256
- help="Experimental: Mirror chords around axis (Jacob Collier style)"
257
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  if use_negative:
259
- neg_key = st.selectbox("Mirror Key", NOTE_NAMES, index=0)
 
 
 
 
 
260
  neg_key_pc = note_name_to_pc(neg_key)
261
 
262
  st.divider()
263
- st.markdown("### 💡 Quick Tips")
264
- st.info("""
265
- **Try these now:**
266
- - Enter: `C/E Am/G`
267
- - Mix genres in one song
268
- - Duplicate sections with 📋
269
- - Download MIDI for your DAW
270
- """)
271
 
272
  if st.button("🗑️ Clear All Sections"):
273
  st.session_state.song_sections = []
274
  st.session_state.section_counter = 0
275
  st.rerun()
276
 
 
 
 
 
 
277
  def draw_piano_svg(notes, label, start_midi=36, num_keys=37):
278
- white_key_w = 22
279
- black_key_w = 14
280
- white_key_h = 90
281
- black_key_h = 55
282
  padding = 20
283
  white_pcs = {0, 2, 4, 5, 7, 9, 11}
284
  black_x_offset = {1: 0.65, 3: 1.65, 6: 3.65, 8: 4.65, 10: 5.65}
@@ -287,15 +345,14 @@ def draw_piano_svg(notes, label, start_midi=36, num_keys=37):
287
  w_idx = 0
288
  for i in range(num_keys):
289
  midi = start_midi + i
290
- pc = midi % 12
291
- if pc in white_pcs:
292
  white_positions[midi] = w_idx
293
  w_idx += 1
294
 
295
  total_width = w_idx * (white_key_w + 1) + padding * 2
296
  total_height = white_key_h + 60
297
 
298
- svg = f'<svg width="{total_width}" height="{total_height}" style="background:#161b22; border-radius:10px; padding:10px; border:1px solid #30363d;">'
299
  svg += f'<text x="{padding}" y="18" fill="#8b949e" font-family="monospace" font-size="12" font-weight="bold">{label}</text>'
300
  y_start = 28
301
 
@@ -304,10 +361,10 @@ def draw_piano_svg(notes, label, start_midi=36, num_keys=37):
304
  is_active = midi in notes
305
  fill = "#58a6ff" if is_active else "#e6edf3"
306
  stroke = "#1f6feb" if is_active else "#8b949e"
307
- svg += f'<rect x="{x}" y="{y_start}" width="{white_key_w}" height="{white_key_h}" fill="{fill}" stroke="{stroke}" stroke-width="{"2" if is_active else "1"}" rx="2"/>'
308
  if is_active:
309
  name = midi_to_name(midi)
310
- svg += f'<text x="{x + white_key_w//2}" y="{y_start + white_key_h - 6}" fill="#0d1117" font-family="monospace" font-size="8" text-anchor="middle" font-weight="bold">{name}</text>'
311
 
312
  for i in range(num_keys):
313
  midi = start_midi + i
@@ -317,162 +374,332 @@ def draw_piano_svg(notes, label, start_midi=36, num_keys=37):
317
  x = padding + (black_x_offset[pc] + (octave_offset * 7)) * (white_key_w + 1)
318
  is_active = midi in notes
319
  fill = "#1f6feb" if is_active else "#0d1117"
320
- stroke = "#58a6ff" if is_active else "#30363d"
321
- svg += f'<rect x="{x}" y="{y_start}" width="{black_key_w}" height="{black_key_h}" fill="{fill}" stroke="{stroke}" rx="1"/>'
322
- if is_active:
323
- name = midi_to_name(midi)
324
- svg += f'<text x="{x + black_key_w//2}" y="{y_start + black_key_h + 12}" fill="#58a6ff" font-family="monospace" font-size="7" text-anchor="middle">{name}</text>'
325
 
326
  svg += '</svg>'
327
  return svg
328
 
 
 
 
 
 
329
  st.markdown("---")
330
  st.subheader("🎵 Song Structure Builder")
331
 
332
- # Inline help text
333
- st.markdown("""
334
- <div class="help-box">
335
- <strong>💡 How it works:</strong> Add sections in any order (Verse, Chorus, Bridge, etc.).
336
- Each section can have different chords and genres. Voice leading automatically connects them smoothly!
337
- </div>
338
- """, unsafe_allow_html=True)
339
-
340
  if len(st.session_state.song_sections) == 0:
341
- st.markdown("""
342
- <div class="example-box">
343
- <strong>🎯 Try this example:</strong><br>
344
- 1. Click "Add First Section" below<br>
345
- 2. Name it "Verse", enter chords: <code>Cmaj7 Am7 Fmaj7 G7</code><br>
346
- 3. Select genre: Pop<br>
347
- 4. Add another section with same chords, but choose "Gospel" genre<br>
348
- 5. Click "Generate" and hear the difference!
349
- </div>
350
- """, unsafe_allow_html=True)
351
-
352
  if st.button("➕ Add First Section", type="primary", use_container_width=True):
353
  add_section()
354
  st.rerun()
355
 
 
 
 
 
 
356
  for idx, section in enumerate(st.session_state.song_sections):
357
  with st.container():
358
- col_num, col_name, col_up, col_down, col_dup, col_del = st.columns([0.5, 3, 0.5, 0.5, 0.5, 0.5])
 
359
 
360
  with col_num:
361
- st.markdown(f"<div style='background: linear-gradient(135deg, #238636, #2ea043); border-radius: 8px; padding: 0.5rem; text-align: center; color: white; font-weight: bold;'>#{idx + 1}</div>", unsafe_allow_html=True)
362
 
363
- with col_name:
364
- section['name'] = st.text_input(
365
- "Section Name",
366
- value=section['name'],
367
- key=f"name_{section['id']}",
 
 
 
 
 
 
368
  label_visibility="collapsed",
369
- placeholder="e.g., Verse 1, Chorus, Bridge, Drop...",
370
- help="Give this section a name (Verse, Chorus, etc.)"
371
  )
 
 
 
 
 
 
 
 
372
 
373
- with col_up:
374
- if st.button("⬆️", key=f"up_{section['id']}", help="Move section up", disabled=(idx == 0)):
375
- move_section_up(section['id'])
376
- st.rerun()
377
-
378
- with col_down:
379
- if st.button("⬇️", key=f"down_{section['id']}", help="Move section down", disabled=(idx == len(st.session_state.song_sections) - 1)):
380
- move_section_down(section['id'])
381
- st.rerun()
382
-
383
- with col_dup:
384
- if st.button("📋", key=f"dup_{section['id']}", help="Duplicate this section"):
385
- duplicate_section(section['id'])
386
- st.rerun()
 
 
 
 
 
 
 
387
 
388
- with col_del:
389
- if st.button("🗑️", key=f"del_{section['id']}", help="Delete this section"):
390
- remove_section(section['id'])
391
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
- col1, col2 = st.columns([2, 1])
 
394
 
395
- with col1:
396
- if "Roman" in input_mode:
397
- help_text = "Roman numerals: I ii iii IV V vi vii°. Add 7 for seventh chords (V7, IVmaj7)"
398
- placeholder = "e.g., I vi IV V"
399
- else:
400
- help_text = "Supports: maj, min, 7, maj7, sus2, sus4, add9, 9, 11, 13, dim, aug. Use / for slash chords!"
401
- placeholder = "e.g., Cmaj7 Am7 F/A G or C C/B Am Am/G"
402
-
403
  section['chords'] = st.text_input(
404
  "Chord Progression",
405
  value=section['chords'],
406
  key=f"chords_{section['id']}",
407
  placeholder=placeholder,
408
- help=help_text
409
  )
410
 
411
- with col2:
 
 
 
 
 
 
 
412
  section['genre'] = st.selectbox(
413
- "Genre/Style",
414
- ["Pop", "Jazz", "Gospel", "Blues", "Classical", "RnB", "Waltz", "Afrobeats", "Trap", "Bossa Nova", "Hindustani Classical", "Neo-Soul", "Reggae", "Latin", "K-Pop", "Lo-fi", "Funk"],
415
- index=["Pop", "Jazz", "Gospel", "Blues", "Classical", "RnB", "Waltz", "Afrobeats", "Trap", "Bossa Nova", "Hindustani Classical", "Neo-Soul", "Reggae", "Latin", "K-Pop", "Lo-fi", "Funk"].index(section['genre']),
416
  key=f"genre_{section['id']}",
417
- help="Each genre uses different professional voicing techniques"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  )
419
 
420
- with st.expander("⚙️ Advanced Settings", expanded=False):
421
- col_lh, col_rh = st.columns(2)
422
- with col_lh:
 
423
  section['lh_octave'] = st.slider(
424
- "Left Hand Octave",
425
- 1, 4, section['lh_octave'],
426
  key=f"lh_{section['id']}",
427
- help="Lower = deeper bass. Default: 2"
428
  )
429
- with col_rh:
430
  section['rh_octave'] = st.slider(
431
- "Right Hand Octave",
432
- 3, 6, section['rh_octave'],
433
  key=f"rh_{section['id']}",
434
- help="Higher = brighter sound. Default: 4"
435
  )
436
 
437
  st.markdown("---")
438
 
439
- col_add, col_space = st.columns([1, 3])
440
- with col_add:
441
- if st.button("➕ Add Section", use_container_width=True, help="Add another section to your song"):
 
442
  add_section()
443
  st.rerun()
444
 
445
  st.markdown("---")
446
 
 
 
 
 
 
447
  if st.button("🎹 Generate Full Song", type="primary", use_container_width=True):
448
  if len(st.session_state.song_sections) == 0:
449
- st.error("❌ Please add at least one section first!")
450
  else:
451
  all_results = []
452
  progression_data = []
453
  prev_rh = None
454
 
455
- for section_idx, section in enumerate(st.session_state.song_sections):
456
  if not section['chords'].strip():
457
- st.warning(f"⚠️ Section '{section['name']}' has no chords. Skipping.")
458
  continue
459
 
460
  st.markdown(f"### 🎵 {section['name']}")
461
- st.caption(f"**Genre:** {section['genre']} • **Chords:** {section['chords']}")
462
 
463
  tokens = section['chords'].strip().split()
464
- section_results = []
465
 
466
  for token in tokens:
467
  try:
468
  if "Roman" in input_mode:
469
  root_pc, quality = roman_to_chord(token, key_pc)
470
- chord_label = f"{pc_to_note_name(root_pc)}{quality} ({token})"
471
  slash_bass = None
472
  else:
473
  root_pc, quality, slash_bass = parse_chord_symbol(token)
474
  chord_label = token
475
 
 
 
 
 
 
 
 
 
 
476
  lh, rh = GenreVoicer.voice(
477
  root_pc, quality, section['genre'],
478
  octave_lh=section['lh_octave'],
@@ -480,11 +707,29 @@ if st.button("🎹 Generate Full Song", type="primary", use_container_width=True
480
  slash_bass=slash_bass
481
  )
482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  if use_negative:
484
  lh = NegativeHarmony.mirror_in_key(lh, neg_key_pc)
485
  rh = NegativeHarmony.mirror_in_key(rh, neg_key_pc)
486
 
487
- if use_voice_leading and prev_rh:
 
 
 
 
 
488
  rh = VoiceLeader.lead(prev_rh, rh)
489
 
490
  has_issues, report, mud = SpectralAuditor.audit(lh, rh, context)
@@ -508,81 +753,82 @@ if st.button("🎹 Generate Full Song", type="primary", use_container_width=True
508
  'genre': section['genre']
509
  }
510
 
511
- section_results.append(result)
512
  all_results.append(result)
513
  progression_data.append({'lh': lh, 'rh': rh})
514
 
515
- except Exception as e:
516
- st.error(f"❌ Error parsing '{token}' in {section['name']}: {e}")
517
-
518
- if section_results:
519
- for r in section_results:
520
  cols = st.columns([1.5, 2.5, 2.5])
521
-
522
  with cols[0]:
523
- st.markdown(f"**{r['label']}**")
524
- st.caption(f"🎨 {r['genre']}")
525
- st.code(f"LH: {', '.join(r['lh_names'])}", language=None)
526
- st.code(f"RH: {', '.join(r['rh_names'])}", language=None)
527
- if r['has_issues']:
528
- st.warning(r['report'])
529
  else:
530
- st.success(r['report'])
531
-
532
  with cols[1]:
533
- piano_lh = draw_piano_svg(r['lh'], "LEFT HAND", 24, 36)
534
- st.markdown(piano_lh, unsafe_allow_html=True)
535
-
536
  with cols[2]:
537
- piano_rh = draw_piano_svg(r['rh'], "RIGHT HAND", 48, 37)
538
- st.markdown(piano_rh, unsafe_allow_html=True)
539
-
540
- st.markdown("---")
 
 
541
 
 
542
  if progression_data:
543
- st.subheader("💾 Export Full Song")
544
- st.info("💡 **Tip:** Import these MIDI files into your DAW, assign sounds, and produce your track!")
545
 
546
  try:
 
547
  files = MidiExporter.export(progression_data, filename_prefix="full_song")
548
- col1, col2 = st.columns(2)
549
- with col1:
550
- with open(files['lh'], 'rb') as f:
551
- st.download_button(
552
- "⬇️ Download Left Hand MIDI",
553
- f, file_name="full_song_LH.mid",
554
- mime="audio/midi",
555
- use_container_width=True
556
- )
557
- with col2:
558
- with open(files['rh'], 'rb') as f:
559
- st.download_button(
560
- "⬇️ Download Right Hand MIDI",
561
- f, file_name="full_song_RH.mid",
562
- mime="audio/midi",
563
- use_container_width=True
564
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
  except Exception as e:
566
- st.error(f"❌ MIDI export error: {e}")
567
 
568
- st.subheader("📊 Full Song Summary")
569
- summary_data = []
570
- for r in all_results:
571
- summary_data.append({
572
- 'Section': r['section'],
573
- 'Chord': r['label'],
574
- 'Genre': r['genre'],
575
- 'LH Notes': ", ".join(r['lh_names']),
576
- 'RH Notes': ", ".join(r['rh_names']),
577
- 'Status': '✅ Clear' if not r['has_issues'] else '⚠️ Fixed'
578
- })
579
- st.dataframe(summary_data, use_container_width=True)
580
-
581
- # Footer
582
  st.markdown("---")
583
- st.markdown("""
584
- <div style='text-align: center; color: #8b949e; padding: 2rem 0;'>
585
- <p>🎹 Built for musicians, by musicians • Free & Open Source</p>
586
- <p>Found a bug or have a suggestion? Let us know!</p>
587
- </div>
588
- """, unsafe_allow_html=True)
 
 
1
  import streamlit as st
2
  import uuid
3
+ from emotional_engine import (
4
+ SectionNamer,
5
+ get_section_types,
6
+ EMOTIONAL_PRESETS,
7
+ NUMBERED_SECTIONS,
8
+ SINGLE_SECTIONS,
9
+ EmotionalContext,
10
+ EmotionalAdapter
11
+ )
12
  from harmonic_engine import (
13
  note_name_to_pc, pc_to_note_name, midi_to_name, midi_to_freq,
14
  parse_chord_symbol, roman_to_chord,
 
16
  MidiExporter, NOTE_NAMES
17
  )
18
 
19
+
20
+ # ═══════════════════════════════════════════════════════════════
21
+ # CHORD PRESET LIBRARY
22
+ # ═══════════════════════════════════════════════════════════════
23
+
24
+ CHORD_PRESETS = {
25
+ "Popular Progressions": {
26
+ "I-V-vi-IV (Axis of Awesome)": "I V vi IV",
27
+ "vi-IV-I-V (Emotional Pop)": "vi IV I V",
28
+ "I-IV-V (Classic Rock)": "I IV V",
29
+ "I-vi-IV-V (50s Progression)": "I vi IV V",
30
+ },
31
+ "Pop Progressions": {
32
+ "I-V-vi-IV": "I V vi IV",
33
+ "vi-IV-I-V": "vi IV I V",
34
+ "I-IV-vi-V": "I IV vi V",
35
+ "I-iii-IV-V": "I iii IV V",
36
+ },
37
+ "Jazz Progressions": {
38
+ "ii-V-I (Turnaround)": "ii7 V7 Imaj7",
39
+ "I-vi-ii-V (Rhythm Changes)": "Imaj7 vi7 ii7 V7",
40
+ "iii-vi-ii-V-I": "iii7 vi7 ii7 V7 Imaj7",
41
+ },
42
+ "Gospel Progressions": {
43
+ "I-IV-V-IV (Traditional)": "I IV V IV",
44
+ "I-V-vi-IV-I (Modern)": "I V vi IV I",
45
+ },
46
+ "Blues Progressions": {
47
+ "12-Bar Blues": "I7 I7 I7 I7 IV7 IV7 I7 I7 V7 IV7 I7 V7",
48
+ "8-Bar Blues": "I7 IV7 I7 I7 IV7 IV7 I7 V7",
49
+ },
50
+ "Bollywood Progressions": {
51
+ "I-V-vi-III-IV": "I V vi III IV",
52
+ "i-VII-VI-V (Emotional)": "i VII VI V",
53
+ },
54
+ "Latin Progressions": {
55
+ "i-iv-V (Minor Latin)": "i iv V",
56
+ "I-IV-V-IV (Salsa)": "I IV V IV",
57
+ },
58
+ "Funk Progressions": {
59
+ "i7-IV7 (Two Chord)": "i7 IV7",
60
+ "I7-IV7-V7": "I7 IV7 V7",
61
+ },
62
+ }
63
+
64
+
65
+ def get_preset_chord_symbols(preset_roman, key_root_pc):
66
+ """Convert Roman numeral preset to chord symbols in given key"""
67
+ tokens = preset_roman.split()
68
+ chord_symbols = []
69
+
70
+ for token in tokens:
71
+ try:
72
+ root_pc, quality = roman_to_chord(token, key_root_pc)
73
+ root_name = pc_to_note_name(root_pc)
74
+
75
+ if quality == 'maj':
76
+ symbol = root_name
77
+ elif quality == 'min':
78
+ symbol = f"{root_name}m"
79
+ elif quality == 'maj7':
80
+ symbol = f"{root_name}maj7"
81
+ elif quality == 'min7':
82
+ symbol = f"{root_name}m7"
83
+ elif quality == 'dom7':
84
+ symbol = f"{root_name}7"
85
+ elif quality == 'dim':
86
+ symbol = f"{root_name}dim"
87
+ else:
88
+ symbol = f"{root_name}{quality}"
89
+
90
+ chord_symbols.append(symbol)
91
+ except:
92
+ chord_symbols.append(token)
93
+
94
+ return " ".join(chord_symbols)
95
+
96
+
97
+ # ═══════════════════════════════════════════════════════════════
98
+ # PAGE CONFIG & STYLING
99
+ # ═══════════════════════════════════════════════════════════════
100
+
101
  st.set_page_config(page_title="Harmonic Catalyst PRO", page_icon="🎹", layout="wide")
102
 
103
  st.markdown("""
104
  <style>
105
  .stApp {background-color: #0d1117; color: #c9d1d9;}
106
  .stButton>button {
107
+ border-radius: 8px;
108
  background: linear-gradient(135deg, #238636, #2ea043);
109
+ color: white; font-weight: bold;
110
  }
111
  hr {border: 0; border-top: 1px solid #30363d;}
 
 
 
 
112
  .help-box {
113
  background: #161b22;
114
  border-left: 4px solid #58a6ff;
 
116
  border-radius: 8px;
117
  margin: 1rem 0;
118
  }
 
 
 
 
 
 
 
 
119
  </style>
120
  """, unsafe_allow_html=True)
121
 
122
  st.title("🎹 Harmonic Catalyst PRO")
123
+ st.caption("Genre-Aware Chord Voicing Engine • Multi-Section Builder")
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ # ═══════════════════════════════════════════════════════════════
127
+ # HELP SECTION
128
+ # ═══════════════════════════════════════════════════════════════
129
+
130
+ with st.expander("📖 Quick Start Guide", expanded=False):
131
  st.markdown("""
132
+ ### How to Use
133
+ 1. **Add a Section** - Select type (Verse, Chorus, etc.)
134
+ 2. **Enter Chords** - Type progression or use presets
135
+ 3. **Pick Genre/Voicing** - Choose voicing style
136
+ 4. **Generate** - See voicings and download MIDI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  """)
138
 
139
+
140
+ # ═══════════════════════════════════════════════════════════════
141
+ # SESSION STATE
142
+ # ═══════════════════════════════════════════════════════════════
143
+
144
  if 'song_sections' not in st.session_state:
145
  st.session_state.song_sections = []
146
 
147
  if 'section_counter' not in st.session_state:
148
  st.session_state.section_counter = 0
149
 
150
+
151
+ # ═══════════════════════════════════════════════════════════════
152
+ # SECTION FUNCTIONS
153
+ # ═══════════════════════════════════════════════════════════════
154
+
155
  def add_section():
156
  st.session_state.section_counter += 1
157
+ section_type = 'Verse'
158
+ display_name = SectionNamer.generate_name(section_type, st.session_state.song_sections)
159
+
160
  new_section = {
161
  'id': str(uuid.uuid4())[:8],
162
  'number': st.session_state.section_counter,
163
+ 'section_type': section_type,
164
+ 'name': display_name,
165
  'chords': '',
166
  'genre': 'Pop',
167
  'lh_octave': 2,
 
169
  }
170
  st.session_state.song_sections.append(new_section)
171
 
172
+
173
  def remove_section(section_id):
174
  st.session_state.song_sections = [
175
  s for s in st.session_state.song_sections if s['id'] != section_id
176
  ]
177
 
178
+
179
  def duplicate_section(section_id):
180
  section = next(s for s in st.session_state.song_sections if s['id'] == section_id)
181
  st.session_state.section_counter += 1
 
186
  idx = st.session_state.song_sections.index(section)
187
  st.session_state.song_sections.insert(idx + 1, new_section)
188
 
189
+
190
  def move_section_up(section_id):
191
  sections = st.session_state.song_sections
192
  idx = next(i for i, s in enumerate(sections) if s['id'] == section_id)
193
  if idx > 0:
194
  sections[idx], sections[idx - 1] = sections[idx - 1], sections[idx]
195
 
196
+
197
  def move_section_down(section_id):
198
  sections = st.session_state.song_sections
199
  idx = next(i for i, s in enumerate(sections) if s['id'] == section_id)
200
  if idx < len(sections) - 1:
201
  sections[idx], sections[idx + 1] = sections[idx + 1], sections[idx]
202
 
203
+
204
+ # ═══════════════════════════════════════════════════════════════
205
+ # SIDEBAR
206
+ # ═══════════════════════════════════════════════════════════════
207
+
208
  with st.sidebar:
209
  st.header("⚙️ Global Settings")
210
 
211
  input_mode = st.radio(
212
  "Input Mode",
213
+ ["Chord Symbols (Cmaj7, Am7...)", "Roman Numerals (I, vi, IV, V)"]
 
214
  )
215
 
216
  if "Roman" in input_mode:
 
220
  key_pc = 0
221
 
222
  st.divider()
223
+
224
+ # Mix Context with info button
225
+ mix_col1, mix_col2 = st.columns([4, 1])
226
+ with mix_col1:
227
+ st.markdown("**🎚️ Mix Context**")
228
+ with mix_col2:
229
+ mix_info = st.toggle("ℹ️", key="mix_info", label_visibility="collapsed")
230
+
231
+ context = st.radio("Mix Context", ["Full Band", "Solo Piano"], label_visibility="collapsed")
232
+
233
+ if mix_info:
234
+ if context == "Full Band":
235
+ st.info("""
236
+ **🎚️ FULL BAND MODE**
237
+
238
+ **WHAT IT DOES:**
239
+ Piano is part of a larger arrangement with other instruments. Tool will:
240
+ • Avoid frequency clashes with vocals (300-500Hz)
241
+ • Keep bass notes cleaner for bass guitar
242
+ • Shift muddy notes automatically
243
+
244
+ **WHEN TO USE:**
245
+ • Recording with band
246
+ • Producing full tracks
247
+ • Piano + vocals + drums + bass
248
+
249
+ **EXAMPLE:**
250
+ Your track has bass guitar, drums, and vocals. Full Band mode keeps piano out of their frequency zones.
251
+ """)
252
+ else:
253
+ st.info("""
254
+ **🎹 SOLO PIANO MODE**
255
+
256
+ **WHAT IT DOES:**
257
+ Piano is the main/only instrument. Tool will:
258
+ • Use full frequency range
259
+ • Richer bass voicings
260
+ • No frequency shifting
261
+
262
+ **WHEN TO USE:**
263
+ • Piano covers
264
+ • Piano tutorials
265
+ • Solo piano performances
266
+ • Piano + vocals only
267
+
268
+ **EXAMPLE:**
269
+ You're making a piano cover of a song. Solo Piano mode gives full, rich voicings.
270
+ """)
271
 
272
  st.divider()
273
+
274
+ use_voice_leading = st.checkbox("🔗 Connect sections smoothly", value=True)
 
 
 
 
275
 
276
  st.divider()
277
+
278
+ # Negative Harmony with info button
279
+ neg_col1, neg_col2 = st.columns([4, 1])
280
+ with neg_col1:
281
+ st.markdown("**🌌 Negative Harmony**")
282
+ with neg_col2:
283
+ neg_info = st.toggle("ℹ️", key="neg_info", label_visibility="collapsed")
284
+
285
+ use_negative = st.checkbox("Enable Negative Harmony", value=False, label_visibility="collapsed")
286
+
287
+ if neg_info:
288
+ st.info("""
289
+ **🌌 NEGATIVE HARMONY**
290
+
291
+ **WHAT IT DOES:**
292
+ Mirrors notes around an axis (root + 5th). Creates "shadow" versions of chords. Made famous by Jacob Collier.
293
+
294
+ **HOW IT WORKS:**
295
+ • C major → F minor (mirror image)
296
+ • Happy chords → Sad/mysterious versions
297
+ • Bright → Dark transformation
298
+
299
+ **WHEN TO USE:**
300
+ • Create unexpected chord colors
301
+ • Add tension to progressions
302
+ • Experimental compositions
303
+ • "What if this chord was dark?"
304
+
305
+ **EXAMPLE:**
306
+ Original: C - G - Am - F (happy pop)
307
+ Negative: Fm - Bbm - Ab - Cm (mysterious, cinematic)
308
+
309
+ 💡 **TIP:** Try on chorus for dramatic effect!
310
+ """)
311
+
312
  if use_negative:
313
+ neg_key = st.selectbox(
314
+ "Mirror Key",
315
+ NOTE_NAMES,
316
+ index=0,
317
+ help="The axis around which notes are mirrored. Usually set to your song's key."
318
+ )
319
  neg_key_pc = note_name_to_pc(neg_key)
320
 
321
  st.divider()
322
+
323
+ transpose_semitones = st.slider("🎵 Transpose (semitones)", -12, 12, 0)
324
+
325
+ st.divider()
 
 
 
 
326
 
327
  if st.button("🗑️ Clear All Sections"):
328
  st.session_state.song_sections = []
329
  st.session_state.section_counter = 0
330
  st.rerun()
331
 
332
+
333
+ # ═══════════════════════════════════════════════════════════════
334
+ # PIANO VISUALIZATION
335
+ # ═══════════════════════════════════════════════════════════════
336
+
337
  def draw_piano_svg(notes, label, start_midi=36, num_keys=37):
338
+ white_key_w, black_key_w = 22, 14
339
+ white_key_h, black_key_h = 90, 55
 
 
340
  padding = 20
341
  white_pcs = {0, 2, 4, 5, 7, 9, 11}
342
  black_x_offset = {1: 0.65, 3: 1.65, 6: 3.65, 8: 4.65, 10: 5.65}
 
345
  w_idx = 0
346
  for i in range(num_keys):
347
  midi = start_midi + i
348
+ if midi % 12 in white_pcs:
 
349
  white_positions[midi] = w_idx
350
  w_idx += 1
351
 
352
  total_width = w_idx * (white_key_w + 1) + padding * 2
353
  total_height = white_key_h + 60
354
 
355
+ svg = f'<svg width="{total_width}" height="{total_height}" style="background:#161b22; border-radius:10px; border:1px solid #30363d;">'
356
  svg += f'<text x="{padding}" y="18" fill="#8b949e" font-family="monospace" font-size="12" font-weight="bold">{label}</text>'
357
  y_start = 28
358
 
 
361
  is_active = midi in notes
362
  fill = "#58a6ff" if is_active else "#e6edf3"
363
  stroke = "#1f6feb" if is_active else "#8b949e"
364
+ svg += f'<rect x="{x}" y="{y_start}" width="{white_key_w}" height="{white_key_h}" fill="{fill}" stroke="{stroke}" rx="2"/>'
365
  if is_active:
366
  name = midi_to_name(midi)
367
+ svg += f'<text x="{x + white_key_w//2}" y="{y_start + white_key_h - 6}" fill="#0d1117" font-size="8" text-anchor="middle" font-weight="bold">{name}</text>'
368
 
369
  for i in range(num_keys):
370
  midi = start_midi + i
 
374
  x = padding + (black_x_offset[pc] + (octave_offset * 7)) * (white_key_w + 1)
375
  is_active = midi in notes
376
  fill = "#1f6feb" if is_active else "#0d1117"
377
+ svg += f'<rect x="{x}" y="{y_start}" width="{black_key_w}" height="{black_key_h}" fill="{fill}" rx="1"/>'
 
 
 
 
378
 
379
  svg += '</svg>'
380
  return svg
381
 
382
+
383
+ # ═══════════════════════════════════════════════════════════════
384
+ # MAIN CONTENT
385
+ # ═══════════════════════════════════════════════════════════════
386
+
387
  st.markdown("---")
388
  st.subheader("🎵 Song Structure Builder")
389
 
390
+ # Empty state
 
 
 
 
 
 
 
391
  if len(st.session_state.song_sections) == 0:
392
+ st.info("👆 Click 'Add First Section' to start building your song!")
 
 
 
 
 
 
 
 
 
 
393
  if st.button("➕ Add First Section", type="primary", use_container_width=True):
394
  add_section()
395
  st.rerun()
396
 
397
+
398
+ # ═══════════════════════════════════════════════════════════════
399
+ # SECTION CARDS
400
+ # ═══════════════════════════════════════════════════════════════
401
+
402
  for idx, section in enumerate(st.session_state.song_sections):
403
  with st.container():
404
+ # Header row
405
+ col_num, col_type, col_name, col_controls = st.columns([0.5, 2, 2, 2])
406
 
407
  with col_num:
408
+ st.markdown(f"**#{idx + 1}**")
409
 
410
+ with col_type:
411
+ section_types = get_section_types()
412
+ current_type = section.get('section_type', 'Verse')
413
+ if current_type not in section_types:
414
+ current_type = 'Verse'
415
+
416
+ new_type = st.selectbox(
417
+ "Type",
418
+ section_types,
419
+ index=section_types.index(current_type),
420
+ key=f"type_{section['id']}",
421
  label_visibility="collapsed",
422
+ help="Select section type - auto-applies emotional defaults"
 
423
  )
424
+
425
+ if new_type != section.get('section_type'):
426
+ section['section_type'] = new_type
427
+ if new_type != 'Custom':
428
+ section['name'] = SectionNamer.generate_name(
429
+ new_type,
430
+ [s for s in st.session_state.song_sections if s['id'] != section['id']]
431
+ )
432
 
433
+ with col_name:
434
+ if section.get('section_type') == 'Custom':
435
+ section['name'] = st.text_input(
436
+ "Name",
437
+ value=section.get('name', 'Custom'),
438
+ key=f"name_{section['id']}",
439
+ label_visibility="collapsed",
440
+ help="Enter custom section name"
441
+ )
442
+ else:
443
+ auto_name = SectionNamer.generate_name(
444
+ section.get('section_type', 'Verse'),
445
+ [s for s in st.session_state.song_sections if s['id'] != section['id']]
446
+ )
447
+ section['name'] = st.text_input(
448
+ "Name",
449
+ value=section.get('name', auto_name),
450
+ key=f"name_{section['id']}",
451
+ label_visibility="collapsed",
452
+ help="Auto-generated name, you can edit it"
453
+ )
454
 
455
+ with col_controls:
456
+ c1, c2, c3, c4 = st.columns(4)
457
+ with c1:
458
+ if st.button("⬆️", key=f"up_{section['id']}", disabled=(idx == 0), help="Move section up"):
459
+ move_section_up(section['id'])
460
+ st.rerun()
461
+ with c2:
462
+ if st.button("⬇️", key=f"down_{section['id']}", disabled=(idx == len(st.session_state.song_sections) - 1), help="Move section down"):
463
+ move_section_down(section['id'])
464
+ st.rerun()
465
+ with c3:
466
+ if st.button("📋", key=f"dup_{section['id']}", help="Create a copy of this section"):
467
+ duplicate_section(section['id'])
468
+ st.rerun()
469
+ with c4:
470
+ if st.button("🗑️", key=f"del_{section['id']}", help="Remove this section"):
471
+ remove_section(section['id'])
472
+ st.rerun()
473
 
474
+ # Chords and Genre row
475
+ col_chords, col_genre = st.columns([2, 1])
476
 
477
+ with col_chords:
478
+ placeholder = "e.g., Cmaj7 Am7 F G" if "Chord" in input_mode else "e.g., I vi IV V"
 
 
 
 
 
 
479
  section['chords'] = st.text_input(
480
  "Chord Progression",
481
  value=section['chords'],
482
  key=f"chords_{section['id']}",
483
  placeholder=placeholder,
484
+ help="Enter chords separated by spaces (e.g., Cmaj7 Am7 F G). Supports slash chords like C/E"
485
  )
486
 
487
+ with col_genre:
488
+ genre_list = ["Pop", "Jazz", "Gospel", "Blues", "Classical", "RnB", "Waltz",
489
+ "Afrobeats", "Trap", "Bossa Nova", "Hindustani Classical",
490
+ "Neo-Soul", "Reggae", "Latin", "K-Pop", "Lo-fi", "Funk"]
491
+ current_genre = section.get('genre', 'Pop')
492
+ if current_genre not in genre_list:
493
+ current_genre = 'Pop'
494
+
495
  section['genre'] = st.selectbox(
496
+ "Genre/Voicing",
497
+ genre_list,
498
+ index=genre_list.index(current_genre),
499
  key=f"genre_{section['id']}",
500
+ help="How chords will be voiced - affects piano arrangement style"
501
+ )
502
+
503
+ # Chord Presets - Two Dropdowns (Category + Preset)
504
+ st.markdown("**🎵 Chord Presets:**")
505
+ col_cat, col_preset, col_apply = st.columns([2, 3, 1])
506
+
507
+ with col_cat:
508
+ # Category dropdown (short list)
509
+ categories = list(CHORD_PRESETS.keys())
510
+ selected_category = st.selectbox(
511
+ "Category",
512
+ categories,
513
+ key=f"preset_cat_{section['id']}",
514
+ label_visibility="collapsed",
515
+ help="Select progression category (Pop, Jazz, Blues, etc.)"
516
+ )
517
+
518
+ with col_preset:
519
+ # Preset dropdown (only shows presets from selected category)
520
+ if selected_category:
521
+ presets_in_category = list(CHORD_PRESETS[selected_category].keys())
522
+ selected_preset = st.selectbox(
523
+ "Preset",
524
+ ["Select..."] + presets_in_category,
525
+ key=f"preset_name_{section['id']}",
526
+ label_visibility="collapsed",
527
+ help="Select chord progression to auto-fill"
528
+ )
529
+ else:
530
+ selected_preset = "Select..."
531
+
532
+ with col_apply:
533
+ # Apply button
534
+ if selected_preset != "Select...":
535
+ if st.button("✨", key=f"apply_{section['id']}", help="Apply selected preset to chord progression"):
536
+ preset_roman = CHORD_PRESETS[selected_category][selected_preset]
537
+
538
+ if "Roman" in input_mode:
539
+ section['chords'] = preset_roman
540
+ else:
541
+ section['chords'] = get_preset_chord_symbols(preset_roman, key_pc)
542
+
543
+ st.rerun()
544
+
545
+ # Emotional Context
546
+ with st.expander("🎭 Emotional Context", expanded=False):
547
+ # Preset selector
548
+ preset_col1, preset_col2 = st.columns([4, 1])
549
+ with preset_col1:
550
+ emotional_presets = list(EMOTIONAL_PRESETS.keys())
551
+ current_preset = section.get('emotional_preset', 'Supportive Verse')
552
+ if current_preset not in emotional_presets:
553
+ current_preset = 'Supportive Verse'
554
+
555
+ selected_emotional = st.selectbox(
556
+ "Preset",
557
+ emotional_presets,
558
+ index=emotional_presets.index(current_preset),
559
+ key=f"emo_preset_{section['id']}",
560
+ help="Quick emotional presets"
561
+ )
562
+
563
+ with preset_col2:
564
+ if st.button("✨", key=f"apply_emo_{section['id']}", help="Apply preset"):
565
+ preset_values = EMOTIONAL_PRESETS[selected_emotional]
566
+ section['emotional_preset'] = selected_emotional
567
+ section['energy'] = preset_values['energy']
568
+ section['density'] = preset_values['density']
569
+ section['role'] = preset_values['role']
570
+ section['movement'] = preset_values['movement']
571
+ st.rerun()
572
+
573
+ # Show preset description
574
+ if selected_emotional in EMOTIONAL_PRESETS:
575
+ st.caption(f"💡 {EMOTIONAL_PRESETS[selected_emotional].get('description', '')}")
576
+
577
+ st.markdown("---")
578
+
579
+ # Energy slider
580
+ section['energy'] = st.slider(
581
+ "🔥 Energy",
582
+ 0.0, 1.0,
583
+ section.get('energy', 0.5),
584
+ step=0.1,
585
+ key=f"energy_{section['id']}",
586
+ help="0.0 = Quiet/Delicate, 1.0 = Loud/Powerful"
587
+ )
588
+
589
+ # Density radio
590
+ density_options = ['Sparse', 'Medium', 'Thick']
591
+ current_density = section.get('density', 'Medium')
592
+ if current_density not in density_options:
593
+ current_density = 'Medium'
594
+
595
+ section['density'] = st.radio(
596
+ "🎹 Density",
597
+ density_options,
598
+ index=density_options.index(current_density),
599
+ key=f"density_{section['id']}",
600
+ horizontal=True,
601
+ help="Sparse = 2-3 notes, Medium = 4-5 notes, Thick = 6-8 notes"
602
+ )
603
+
604
+ # Role radio
605
+ role_options = ['Lead', 'Support', 'Ambient']
606
+ current_role = section.get('role', 'Support')
607
+ if current_role not in role_options:
608
+ current_role = 'Support'
609
+
610
+ section['role'] = st.radio(
611
+ "🎚️ Role",
612
+ role_options,
613
+ index=role_options.index(current_role),
614
+ key=f"role_{section['id']}",
615
+ horizontal=True,
616
+ help="Lead = Front of mix, Support = Behind vocals, Ambient = Background"
617
+ )
618
+
619
+ # Movement radio
620
+ movement_options = ['Static', 'Flowing', 'Agitated']
621
+ current_movement = section.get('movement', 'Flowing')
622
+ if current_movement not in movement_options:
623
+ current_movement = 'Flowing'
624
+
625
+ section['movement'] = st.radio(
626
+ "🌊 Movement",
627
+ movement_options,
628
+ index=movement_options.index(current_movement),
629
+ key=f"movement_{section['id']}",
630
+ horizontal=True,
631
+ help="Static = Block chords, Flowing = Smooth transitions, Agitated = Dramatic"
632
  )
633
 
634
+ # Advanced settings
635
+ with st.expander("⚙️ Advanced", expanded=False):
636
+ c1, c2 = st.columns(2)
637
+ with c1:
638
  section['lh_octave'] = st.slider(
639
+ "LH Octave", 1, 4, section.get('lh_octave', 2),
 
640
  key=f"lh_{section['id']}",
641
+ help="Left hand octave - lower = deeper bass"
642
  )
643
+ with c2:
644
  section['rh_octave'] = st.slider(
645
+ "RH Octave", 3, 6, section.get('rh_octave', 4),
 
646
  key=f"rh_{section['id']}",
647
+ help="Right hand octave - higher = brighter sound"
648
  )
649
 
650
  st.markdown("---")
651
 
652
+
653
+ # Add Section Button
654
+ if len(st.session_state.song_sections) > 0:
655
+ if st.button("➕ Add Section", use_container_width=True):
656
  add_section()
657
  st.rerun()
658
 
659
  st.markdown("---")
660
 
661
+
662
+ # ═══════════════════════════════════════════════════════════════
663
+ # GENERATE BUTTON
664
+ # ═══════════════════════════════════════════════════════════════
665
+
666
  if st.button("🎹 Generate Full Song", type="primary", use_container_width=True):
667
  if len(st.session_state.song_sections) == 0:
668
+ st.error("❌ Add at least one section first!")
669
  else:
670
  all_results = []
671
  progression_data = []
672
  prev_rh = None
673
 
674
+ for section in st.session_state.song_sections:
675
  if not section['chords'].strip():
676
+ st.warning(f"⚠️ '{section['name']}' has no chords. Skipping.")
677
  continue
678
 
679
  st.markdown(f"### 🎵 {section['name']}")
680
+ st.caption(f"**Genre/Voicing:** {section['genre']} • **Chords:** {section['chords']}")
681
 
682
  tokens = section['chords'].strip().split()
 
683
 
684
  for token in tokens:
685
  try:
686
  if "Roman" in input_mode:
687
  root_pc, quality = roman_to_chord(token, key_pc)
688
+ chord_label = f"{pc_to_note_name(root_pc)}{quality}"
689
  slash_bass = None
690
  else:
691
  root_pc, quality, slash_bass = parse_chord_symbol(token)
692
  chord_label = token
693
 
694
+ # Transpose
695
+ if transpose_semitones != 0:
696
+ root_pc = (root_pc + transpose_semitones) % 12
697
+ if slash_bass:
698
+ slash_bass_pc = note_name_to_pc(slash_bass)
699
+ slash_bass_pc = (slash_bass_pc + transpose_semitones) % 12
700
+ slash_bass = pc_to_note_name(slash_bass_pc)
701
+ chord_label = f"{pc_to_note_name(root_pc)}{quality if quality != 'maj' else ''}"
702
+
703
  lh, rh = GenreVoicer.voice(
704
  root_pc, quality, section['genre'],
705
  octave_lh=section['lh_octave'],
 
707
  slash_bass=slash_bass
708
  )
709
 
710
+ # Apply emotional adaptation
711
+ emotional_ctx = EmotionalContext(
712
+ energy=section.get('energy', 0.5),
713
+ density=section.get('density', 'Medium'),
714
+ role=section.get('role', 'Support'),
715
+ movement=section.get('movement', 'Flowing')
716
+ )
717
+
718
+ if context == "Solo Piano":
719
+ lh, rh = EmotionalAdapter.adapt_solo_piano(lh, rh, emotional_ctx)
720
+ else:
721
+ lh, rh = EmotionalAdapter.adapt_arrangement(lh, rh, emotional_ctx)
722
+
723
  if use_negative:
724
  lh = NegativeHarmony.mirror_in_key(lh, neg_key_pc)
725
  rh = NegativeHarmony.mirror_in_key(rh, neg_key_pc)
726
 
727
+ # Apply voice leading based on Movement setting
728
+ voice_settings = EmotionalAdapter.get_voice_leading_settings(
729
+ section.get('movement', 'Flowing')
730
+ )
731
+
732
+ if use_voice_leading and prev_rh and voice_settings['enabled']:
733
  rh = VoiceLeader.lead(prev_rh, rh)
734
 
735
  has_issues, report, mud = SpectralAuditor.audit(lh, rh, context)
 
753
  'genre': section['genre']
754
  }
755
 
 
756
  all_results.append(result)
757
  progression_data.append({'lh': lh, 'rh': rh})
758
 
759
+ # Display
 
 
 
 
760
  cols = st.columns([1.5, 2.5, 2.5])
 
761
  with cols[0]:
762
+ st.markdown(f"**{result['label']}**")
763
+ st.code(f"LH: {', '.join(result['lh_names'])}")
764
+ st.code(f"RH: {', '.join(result['rh_names'])}")
765
+ if result['has_issues']:
766
+ st.warning(result['report'])
 
767
  else:
768
+ st.success(result['report'])
 
769
  with cols[1]:
770
+ st.markdown(draw_piano_svg(result['lh'], "LEFT HAND", 24, 36), unsafe_allow_html=True)
 
 
771
  with cols[2]:
772
+ st.markdown(draw_piano_svg(result['rh'], "RIGHT HAND", 48, 37), unsafe_allow_html=True)
773
+
774
+ except Exception as e:
775
+ st.error(f"❌ Error parsing '{token}': {e}")
776
+
777
+ st.markdown("---")
778
 
779
+ # Export
780
  if progression_data:
781
+ st.subheader("💾 Export MIDI")
 
782
 
783
  try:
784
+ # Generate all MIDI files
785
  files = MidiExporter.export(progression_data, filename_prefix="full_song")
786
+ combined_file = MidiExporter.export_combined(progression_data, filename_prefix="full_song")
787
+
788
+ # Main button - Combined MIDI
789
+ with open(combined_file, 'rb') as f:
790
+ st.download_button(
791
+ "⬇️ Download Complete MIDI",
792
+ f,
793
+ "song_COMPLETE.mid",
794
+ "audio/midi",
795
+ use_container_width=True,
796
+ type="primary"
797
+ )
798
+
799
+ # Expander for individual parts
800
+ with st.expander("📂 Download Individual Parts"):
801
+ c1, c2 = st.columns(2)
802
+ with c1:
803
+ with open(files['lh'], 'rb') as f:
804
+ st.download_button(
805
+ "⬇️ LH / Bass",
806
+ f,
807
+ "song_LH_Bass.mid",
808
+ "audio/midi",
809
+ use_container_width=True
810
+ )
811
+ with c2:
812
+ with open(files['rh'], 'rb') as f:
813
+ st.download_button(
814
+ "⬇️ RH / Chords",
815
+ f,
816
+ "song_RH_Chords.mid",
817
+ "audio/midi",
818
+ use_container_width=True
819
+ )
820
  except Exception as e:
821
+ st.error(f"❌ Export error: {e}")
822
 
823
+ st.subheader("📊 Summary")
824
+ summary = [{
825
+ 'Section': r['section'],
826
+ 'Chord': r['label'],
827
+ 'LH': ", ".join(r['lh_names']),
828
+ 'RH': ", ".join(r['rh_names'])
829
+ } for r in all_results]
830
+ st.dataframe(summary, use_container_width=True)
831
+
832
+
 
 
 
 
833
  st.markdown("---")
834
+ st.caption("🎹 Harmonic Catalyst PRO")