Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
| 18 |
background: linear-gradient(135deg, #238636, #2ea043);
|
| 19 |
-
color: white; font-weight: bold;
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
st.markdown("""
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 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 |
-
'
|
|
|
|
| 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 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
st.divider()
|
| 245 |
-
|
| 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 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
if use_negative:
|
| 259 |
-
neg_key = st.selectbox(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
neg_key_pc = note_name_to_pc(neg_key)
|
| 261 |
|
| 262 |
st.divider()
|
| 263 |
-
|
| 264 |
-
st.
|
| 265 |
-
|
| 266 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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;
|
| 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}"
|
| 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-
|
| 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 |
-
|
| 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 |
-
#
|
| 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.
|
| 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 |
-
|
|
|
|
| 359 |
|
| 360 |
with col_num:
|
| 361 |
-
st.markdown(f"
|
| 362 |
|
| 363 |
-
with
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
label_visibility="collapsed",
|
| 369 |
-
|
| 370 |
-
help="Give this section a name (Verse, Chorus, etc.)"
|
| 371 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
|
| 373 |
-
with
|
| 374 |
-
if
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
|
| 388 |
-
with
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
-
|
|
|
|
| 394 |
|
| 395 |
-
with
|
| 396 |
-
if "
|
| 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=
|
| 409 |
)
|
| 410 |
|
| 411 |
-
with
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
section['genre'] = st.selectbox(
|
| 413 |
-
"Genre/
|
| 414 |
-
|
| 415 |
-
index=
|
| 416 |
key=f"genre_{section['id']}",
|
| 417 |
-
help="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
)
|
| 419 |
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
|
|
|
| 423 |
section['lh_octave'] = st.slider(
|
| 424 |
-
"
|
| 425 |
-
1, 4, section['lh_octave'],
|
| 426 |
key=f"lh_{section['id']}",
|
| 427 |
-
help="
|
| 428 |
)
|
| 429 |
-
with
|
| 430 |
section['rh_octave'] = st.slider(
|
| 431 |
-
"
|
| 432 |
-
3, 6, section['rh_octave'],
|
| 433 |
key=f"rh_{section['id']}",
|
| 434 |
-
help="
|
| 435 |
)
|
| 436 |
|
| 437 |
st.markdown("---")
|
| 438 |
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
|
|
|
| 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("❌
|
| 450 |
else:
|
| 451 |
all_results = []
|
| 452 |
progression_data = []
|
| 453 |
prev_rh = None
|
| 454 |
|
| 455 |
-
for
|
| 456 |
if not section['chords'].strip():
|
| 457 |
-
st.warning(f"⚠️
|
| 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}
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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"**{
|
| 524 |
-
st.
|
| 525 |
-
st.code(f"
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
st.warning(r['report'])
|
| 529 |
else:
|
| 530 |
-
st.success(
|
| 531 |
-
|
| 532 |
with cols[1]:
|
| 533 |
-
|
| 534 |
-
st.markdown(piano_lh, unsafe_allow_html=True)
|
| 535 |
-
|
| 536 |
with cols[2]:
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
|
|
|
|
|
|
| 541 |
|
|
|
|
| 542 |
if progression_data:
|
| 543 |
-
st.subheader("💾 Export
|
| 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 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
except Exception as e:
|
| 566 |
-
st.error(f"❌
|
| 567 |
|
| 568 |
-
st.subheader("📊
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
})
|
| 579 |
-
st.dataframe(summary_data, use_container_width=True)
|
| 580 |
-
|
| 581 |
-
# Footer
|
| 582 |
st.markdown("---")
|
| 583 |
-
st.
|
| 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")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|