#!/usr/bin/env python3
"""
LaBoom Techno v3 — Klassische Satzlehre × moderne Form
=======================================================

KOMPONIST-PRINZIPIEN (Mozart/Beethoven/Schubert):
  - 4-stimmiger Satz: S/A/T/B mit eigenständigen Linien
  - Keine Quint- oder Oktavparallelen
  - Leittonauflösung (7. → 1.)
  - Septimauflösung (7 → 6)
  - Gegenbewegung Bass ↔ Sopran
  - Schritte vor Sprüngen
  - Funktionsharmonik T-S-D-T mit Vorhalten (4-3 Suspension)
  - Schubert-Mediante: G-Dur ↔ B♭-Dur via gem. Ton D/G
  - Mozart-Periode: Vordersatz (4b) → Nachsatz (4b)
  - Beethoven-Sequenz: Motiv abwärtssequenziert

TECHNO-FORM:
  INTRO    8b   Pad allein (Choralsatz)
  BUILD    8b   + Kick + Bass-Schritte
  DROP1    16b  Full Techno, klassischer Bass + Lead-Melodie
  BREAK    8b   Modulation G → B♭ via Mediante
  CLIMAX   16b  B♭-Dur dann E♭-Dur Modulation (Schubert)
  DROP2    16b  E♭-Dur Triumph mit voller Kadenz
  OUTRO    8b   Rückführung E♭ → G via Trugschluss
"""

import mido, os, math, random, sys
from mido import MidiFile, MidiTrack, Message, MetaMessage
from collections import defaultdict

# Import chord-aware melody generator (the FIX)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from melody_fix import build_chord_aware_melody

random.seed(1791)  # Mozart's Todesjahr

OUT_DIR = "/root/.openclaw/workspace/downloads/midi-techno-v3"
os.makedirs(OUT_DIR, exist_ok=True)

TPQ = 480
BPM = 126
BAR = 4 * TPQ

KICK, SNARE, CLAP = 36, 38, 39
CLOSED_HAT, OPEN_HAT, RIDE = 42, 46, 51
TAMBOURINE, COWBELL, CRASH = 54, 56, 49

# ============================================================================
# TONLEITER & AKKORD-DEFINITIONEN
# ============================================================================

# Scale degrees as pitch-class offsets from root
MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11]
MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10]
HARMONIC_MINOR = [0, 2, 3, 5, 7, 8, 11]  # für Leitton

# Roman numerals → (chord root offset, quality)
DIATONIC_MAJOR = {
    'I':   (0, [0, 4, 7]),
    'ii':  (2, [0, 3, 7]),
    'iii': (4, [0, 3, 7]),
    'IV':  (5, [0, 4, 7]),
    'V':   (7, [0, 4, 7]),
    'V7':  (7, [0, 4, 7, 10]),
    'vi':  (9, [0, 3, 7]),
    'vii°':(11, [0, 3, 6]),
    'I64': (0, [7, 12, 16]),  # 6/4 chord (für Kadenz-Vorhalt)
    'Isus4': (0, [0, 5, 7]),
    'IV_M': (5, [0, 4, 7]),
    'bVI': (8, [0, 4, 7]),  # für Schubert-Mediante
    'bIII':(3, [0, 4, 7]),  # für Mediante
}

DIATONIC_MINOR = {
    'i':   (0, [0, 3, 7]),
    'ii°': (2, [0, 3, 6]),
    'III': (3, [0, 4, 7]),
    'iv':  (5, [0, 3, 7]),
    'v':   (7, [0, 3, 7]),
    'V':   (7, [0, 4, 7]),   # harmonisch Moll → Dur-Dominante
    'V7':  (7, [0, 4, 7, 10]),
    'VI':  (8, [0, 4, 7]),
    'VII': (10, [0, 4, 7]),
    'bII': (1, [0, 4, 7]),   # neapolitanischer Sextakkord
    'i64': (0, [7, 12, 15]),
}

# ============================================================================
# CHORD-FUNCTION → ABSOLUTE PITCHES (in current key)
# ============================================================================

def chord_in_key(key_root_pc, key_mode, roman):
    """Returns (chord_root_pc, [interval offsets]) for the roman numeral 
       in the given key."""
    table = DIATONIC_MAJOR if key_mode == 'major' else DIATONIC_MINOR
    root_offset, intervals = table[roman]
    chord_root = (key_root_pc + root_offset) % 12
    return chord_root, intervals


def build_voicing(chord_root_pc, intervals, prev_voicing=None, 
                  range_satb=((36,55),(50,67),(57,74),(62,79))):
    """4-stimmiger Satz: Bass / Tenor / Alt / Sopran.
    Wenn prev_voicing gegeben, voice-lead minimaly without parallels."""
    # Generate all chord-tone pitches in entire range
    chord_pcs = [(chord_root_pc + iv) % 12 for iv in intervals]
    chord_root = chord_pcs[0]
    
    def candidates_in_range(rng):
        lo, hi = rng
        return [p for p in range(lo, hi+1) if p % 12 in chord_pcs]
    
    if prev_voicing is None:
        # Initial voicing: root in bass, then close position 5-3-1
        bass = chord_root + 36  # near C2
        while bass < range_satb[0][0]: bass += 12
        while bass > range_satb[0][1]: bass -= 12
        
        # 5th in tenor
        fifth = (chord_root + 7) % 12
        ten_candidates = [p for p in candidates_in_range(range_satb[1]) if p % 12 == fifth]
        tenor = ten_candidates[0] if ten_candidates else range_satb[1][0]
        
        # 3rd in alto
        third_pc = chord_pcs[1] if len(chord_pcs) > 1 else fifth
        alt_candidates = [p for p in candidates_in_range(range_satb[2]) if p % 12 == third_pc]
        alto = alt_candidates[0] if alt_candidates else range_satb[2][0]
        
        # Root in soprano (or 3rd if root too high)
        sop_candidates = [p for p in candidates_in_range(range_satb[3]) if p % 12 == chord_root]
        if not sop_candidates:
            sop_candidates = [p for p in candidates_in_range(range_satb[3]) if p % 12 == third_pc]
        soprano = sop_candidates[0] if sop_candidates else range_satb[3][0]
        
        return [bass, tenor, alto, soprano]
    
    # Voice-leading: bass moves to chord root (down a 5th or up a 4th preferred);
    # upper voices move to closest chord tone with no parallels
    new_v = [None, None, None, None]
    
    # Bass: root, prefer step within octave of prev
    target_bass_pc = chord_root
    bass_cands = [p for p in candidates_in_range(range_satb[0]) if p % 12 == target_bass_pc]
    if bass_cands:
        new_v[0] = min(bass_cands, key=lambda p: min(abs(p - prev_voicing[0]),
                                                     abs(p - prev_voicing[0] - 12),
                                                     abs(p - prev_voicing[0] + 12)))
    else:
        new_v[0] = prev_voicing[0]
    
    # Soprano / Alto / Tenor: closest chord tone, avoiding parallels with bass
    for voice_idx in (3, 2, 1):  # sop first, then alto, then tenor
        rng = range_satb[voice_idx]
        cands = candidates_in_range(rng)
        prev = prev_voicing[voice_idx]
        
        # Sort by distance
        cands.sort(key=lambda p: abs(p - prev))
        
        # Filter to avoid parallel 5ths/8ves with bass
        bass_prev = prev_voicing[0]
        bass_new = new_v[0]
        bass_motion = bass_new - bass_prev
        
        good = []
        for cand in cands:
            voice_motion = cand - prev
            
            # Check parallels with bass
            prev_interval = (prev - bass_prev) % 12
            new_interval = (cand - bass_new) % 12
            
            is_parallel_5th = (prev_interval == 7 and new_interval == 7 and bass_motion != 0 and voice_motion != 0)
            is_parallel_8va = (prev_interval == 0 and new_interval == 0 and bass_motion != 0 and voice_motion != 0)
            
            # Check parallels with other already-placed voices
            parallel_with_others = False
            for other_idx in range(voice_idx + 1, 4):
                if new_v[other_idx] is None: continue
                other_prev = prev_voicing[other_idx]
                other_new = new_v[other_idx]
                pi = (prev - other_prev) % 12
                ni = (cand - other_new) % 12
                if (pi in (7, 0) and ni == pi and 
                    other_new - other_prev != 0 and voice_motion != 0):
                    parallel_with_others = True
                    break
            
            if not is_parallel_5th and not is_parallel_8va and not parallel_with_others:
                good.append(cand)
        
        # Prefer small motion
        if good:
            new_v[voice_idx] = good[0]
        else:
            new_v[voice_idx] = cands[0] if cands else prev
    
    # Make sure voices don't cross
    for i in range(3):
        if new_v[i] >= new_v[i+1]:
            new_v[i+1] = new_v[i] + 1
    
    return new_v


# ============================================================================
# MELODIE-KONSTRUKTION
# (Mozart-Phrasen, Schritte vor Sprünge, Antezedens/Konsequens)
# ============================================================================

def build_melody_phrase(key_root_pc, key_mode, chord_progression, num_bars=8, 
                        register=72, motif_seed=None):
    """Baut Sopran-Melodie über num_bars mit klassischer Phrasenstruktur:
       Vordersatz (Bars 1-4) → Nachsatz (Bars 5-8)
       Schritte bevorzugt, Sprünge nur zu Akkord-Tönen, Leittonauflösung.
    
    chord_progression: list of (chord_root_pc, intervals) — len matches bars in groups
    """
    scale = MAJOR_SCALE if key_mode == 'major' else HARMONIC_MINOR
    scale_pcs = [(key_root_pc + s) % 12 for s in scale]
    
    melody = []  # list of (start_tick, dur_ticks, pitch, velocity)
    
    # Use 1+1+2-bar phrase structure (Mozart): motif, repeat, extend
    eighth = TPQ // 2
    quarter = TPQ
    half = TPQ * 2
    
    # Build motif: 4 notes in 1 bar (Mozart-typical)
    # Pattern: leap up, step down, step down, step down  (gap-fill principle)
    chord = chord_progression[0]
    chord_root, intervals = chord
    chord_tones = [(chord_root + iv) % 12 for iv in intervals]
    
    # Start on 5th of chord (typical Mozart opening)
    start_pc = chord_tones[2] if len(chord_tones) > 2 else chord_tones[-1]
    start_pitch = register + ((start_pc - register) % 12)
    
    def scale_step_from(pitch, direction=1, n=1):
        """Move n scale steps from pitch."""
        for _ in range(abs(n)):
            pc = pitch % 12
            sorted_scale = sorted(scale_pcs)
            # find current scale index
            idx = 0
            for i, sp in enumerate(sorted_scale):
                if pc <= sp:
                    idx = i
                    break
            else:
                idx = len(sorted_scale) - 1
            
            if direction * n > 0:
                next_idx = (idx + 1) % len(sorted_scale)
                next_pc = sorted_scale[next_idx]
                if next_pc <= pc:
                    pitch = (pitch // 12 + 1) * 12 + next_pc
                else:
                    pitch = (pitch // 12) * 12 + next_pc
            else:
                next_idx = (idx - 1) % len(sorted_scale)
                next_pc = sorted_scale[next_idx]
                if next_pc >= pc:
                    pitch = (pitch // 12 - 1) * 12 + next_pc
                else:
                    pitch = (pitch // 12) * 12 + next_pc
        return pitch
    
    # ----- Motiv (1 Bar) -----
    # Take 4 quarter-note positions; gap-fill with chord-tone leaps
    cur = start_pitch
    motif = []
    motif.append((0, quarter, cur, 95))  # downbeat: chord tone, accent
    
    # Step up to next scale tone
    cur = scale_step_from(cur, direction=1, n=2)
    motif.append((quarter, quarter, cur, 80))
    
    # Step down (gap-fill from leap)
    cur = scale_step_from(cur, direction=-1, n=1)
    motif.append((2*quarter, quarter, cur, 80))
    
    # Cadential: step down to chord tone
    cur = scale_step_from(cur, direction=-1, n=1)
    motif.append((3*quarter, quarter, cur, 85))
    
    melody.extend(motif)
    
    # ----- Bar 2: motif sequenced (Mozart's "repetition" idea) -----
    # Shift down by one scale step (Beethoven-style sequence)
    motif2_start_pitch = scale_step_from(motif[-1][2], direction=-1, n=1)
    cur = motif2_start_pitch
    melody.append((BAR, quarter, cur, 90))
    cur = scale_step_from(cur, direction=1, n=2)
    melody.append((BAR + quarter, quarter, cur, 80))
    cur = scale_step_from(cur, direction=-1, n=1)
    melody.append((BAR + 2*quarter, quarter, cur, 80))
    cur = scale_step_from(cur, direction=-1, n=1)
    melody.append((BAR + 3*quarter, quarter, cur, 80))
    
    # ----- Bars 3-4: erweiterte Phrase (2 bars), endet auf Halbschluss (V) -----
    # Eighth-note Schritte aufwärts dann abwärts
    cur = melody[-1][2]
    eighth_positions = []
    t = 2 * BAR
    # bar 3: 8 8ths gehen schrittweise hoch
    for i in range(8):
        eighth_positions.append((t + i * eighth, eighth, cur, 75))
        if i % 2 == 0:
            cur = scale_step_from(cur, direction=1, n=1)
    
    # bar 4: 8ths schrittweise hinunter zum Halbschluss (5. Stufe)
    target_5th = (key_root_pc + 7) % 12
    for i in range(8):
        if i < 6:
            cur = scale_step_from(cur, direction=-1, n=1)
            eighth_positions.append((3 * BAR + i * eighth, eighth, cur, 70))
        elif i == 6:
            # Land on 5th (V)
            # Find closest pitch of pc target_5th
            cands = [register + 12 - 12 + (target_5th - (register % 12)) % 12 - 12,
                     register + (target_5th - (register % 12)) % 12,
                     register + 12 + (target_5th - (register % 12)) % 12 - 12]
            cands = [c for c in cands if 60 <= c <= 84]
            if cands:
                cur = min(cands, key=lambda c: abs(c - eighth_positions[-1][2]))
            eighth_positions.append((3 * BAR + 6 * eighth, quarter, cur, 90))
        # bar4 beat 4: rest (silence makes phrase breathe)
    
    melody.extend(eighth_positions)
    
    # ----- Bars 5-6: Wiederholung Motiv (Nachsatz beginnt) -----
    # Same motif as bars 1-2 but ending differently
    for s, d, p, v in motif:
        melody.append((4 * BAR + s, d, p, v))
    
    # Bar 6: variation
    cur = motif[-1][2]
    for i in range(4):
        melody.append((5 * BAR + i * quarter, quarter, cur, 80))
        cur = scale_step_from(cur, direction=1 if i % 2 == 0 else -1, n=1)
    
    # ----- Bars 7-8: vollständige Kadenz (V-I) endet auf Tonika -----
    # Bar 7: V-Akkord skalentönische Abstieg
    cur = melody[-1][2]
    # zur 7. Stufe (Leitton) hoch
    leading_tone_pc = (key_root_pc + 11) % 12 if key_mode == 'major' else (key_root_pc + 11) % 12
    target_leading = 60 + ((leading_tone_pc - 60 % 12) % 12)
    while target_leading < cur - 3: target_leading += 12
    while target_leading > cur + 7: target_leading -= 12
    melody.append((6 * BAR, half, target_leading, 90))
    melody.append((6 * BAR + half, half, target_leading, 85))
    
    # Bar 8: Leittonauflösung zur Tonika
    tonic_pc = key_root_pc
    # nearest tonic above leading tone
    target_tonic = target_leading + 1
    while target_tonic % 12 != tonic_pc: target_tonic += 1
    melody.append((7 * BAR, 2 * BAR // 4 * 3, target_tonic, 95))
    
    return melody


# ============================================================================
# BASS-LINIE (klassisch, mit Gegenbewegung zur Melodie)
# ============================================================================

def build_classical_bass(voicings_per_bar, num_bars):
    """Bass läuft als 4-Viertel-Linie in Schritten und Akkordtönen.
       Klassischer Stil: Quintfall in Kadenz, sonst Stufen oder gebrochene Dreiklänge.
    """
    events = []
    for bar in range(num_bars):
        if bar >= len(voicings_per_bar):
            break
        v = voicings_per_bar[bar]
        bass_root = v[0]
        next_root = voicings_per_bar[bar+1][0] if bar+1 < len(voicings_per_bar) else bass_root
        
        # Direction toward next chord
        direction = 1 if next_root > bass_root else -1
        
        # Pattern: Root - 5th - Root - leading-step
        fifth = bass_root + 7
        if fifth > bass_root + 12: fifth -= 12
        
        # Approach to next root chromatically/stepwise
        if direction > 0:
            approach = next_root - 1 if next_root > bass_root else next_root + 1
        else:
            approach = next_root + 1
        
        # Half-notes for INTRO/OUTRO, quarter notes for active sections
        # Here we do quarter-note walking bass (Beethoven-style)
        pattern = [bass_root, fifth, bass_root + 12, approach]
        
        base = bar * BAR
        for beat, pitch in enumerate(pattern):
            # Stay in bass range
            while pitch < 28: pitch += 12
            while pitch > 55: pitch -= 12
            
            t = base + beat * TPQ
            vel = 95 if beat == 0 else (80 if beat % 2 == 0 else 75)
            dur = int(TPQ * 0.85)
            events.append((t, Message('note_on', channel=0, note=pitch, velocity=vel, time=0)))
            events.append((t + dur, Message('note_off', channel=0, note=pitch, velocity=0, time=0)))
    
    return events


# ============================================================================
# CHORALSATZ (4-stimmig, Choralsatz-Pad)
# ============================================================================

def build_chorale(progression_per_bar, num_bars, start_voicing=None):
    """Erzeuge 4-stimmigen Satz für alle Akkorde."""
    voicings = []
    prev = start_voicing
    for bar in range(num_bars):
        if bar < len(progression_per_bar):
            chord_root_pc, intervals = progression_per_bar[bar]
            v = build_voicing(chord_root_pc, intervals, prev_voicing=prev)
            voicings.append(v)
            prev = v
    return voicings


def chorale_to_events(voicings, num_bars, channel_pad=4, channel_chord=2):
    """Convert SATB voicings to MIDI events.
    Bass on its own track (returned separately).
    Tenor/Alto/Sopran als Pad-Akkord."""
    pad_events = []
    bass_events = []
    
    for bar, v in enumerate(voicings):
        base = bar * BAR
        bass, tenor, alto, soprano = v
        
        # Pad sustains the upper 3 voices
        for note in (tenor, alto, soprano):
            pad_events.append((base, Message('note_on', channel=channel_pad, note=note, velocity=55, time=0)))
            pad_events.append((base + BAR - TPQ//4, Message('note_off', channel=channel_pad, note=note, velocity=0, time=0)))
    
    return pad_events, bass_events  # bass handled separately


# ============================================================================
# SECTION-DEFINITIONS (mit klassischen Kadenzen)
# ============================================================================

# Each section: (name, num_bars, key_root_pc, key_mode, roman_progression_per_bar)
# G major: pc=7,  B♭ major: pc=10,  E♭ major: pc=3,  g minor: pc=7 minor

SECTIONS = [
    # INTRO 8b: G-Dur Choral, einfache Kadenz I-IV-V-I-vi-ii-V-I
    ('INTRO',  8,  7, 'major', ['I', 'IV', 'V', 'I',  'vi', 'ii', 'V', 'I']),
    
    # BUILD 8b: G-Dur, Mozart-Periode mit Halbschluss (V) am Ende des Vordersatzes
    ('BUILD',  8,  7, 'major', ['I', 'V',  'vi', 'IV',  'I', 'V', 'IV', 'V']),  # Halbschluss
    
    # DROP1 16b: G-Dur, klassische Periodenfolge + Trugschluss-Wendung
    ('DROP1',  16, 7, 'major', ['I','vi','IV','V', 'I','vi','ii','V',
                                 'I','iii','IV','I','ii','V7','vi','V']),  # Trugschluss bei bar 14
    
    # BREAK 8b: G-Dur → B♭-Dur via chromatische Mediante (Schubert)
    #   G → Em → Cm → B♭   (Cm ist Schubert's gemeinsame Pivot G-major/Bb-major)
    ('BREAK',  8,  7, 'major', ['I','vi','iii','bIII',   # bIII = B♭ in G-Dur context!
                                 'I','vi','iii','bIII']),
    
    # CLIMAX 16b: B♭-Dur etabliert → E♭-Dur Modulation (Schubert)
    # bIII von B♭-Dur = D♭, aber wir nehmen IV (E♭) als Hauptmodulationsziel
    ('CLIMAX', 16, 10, 'major', ['I','V','vi','IV',  'ii','V','I','I',
                                  'IV','iii','ii','V', 'I','vi','IV','V']),
    
    # DROP2 16b: E♭-Dur Triumph (Modulation via gemeinsamen Akkord aus B♭-Dur: IV = E♭)
    ('DROP2',  16, 3, 'major', ['I','V','vi','IV',  'I','V','IV','I',
                                 'ii','V','I','vi', 'IV','V7','I','I']),
    
    # OUTRO 8b: Rückführung E♭ → G via Trugschluss/Mediante
    # E♭ → C-Dur (V-Vorbereitung) → G-Dur (I)
    ('OUTRO',  8,  7, 'major', ['IV','V','I','I',  'IV','V','I','I']),
]

TOTAL_BARS = sum(b for _, b, _, _, _ in SECTIONS)


def get_bar_info(bar_global):
    """Returns (section_name, bar_in_section, total_section_bars, key_pc, key_mode, roman)"""
    accum = 0
    for name, length, kr, km, prog in SECTIONS:
        if bar_global < accum + length:
            local = bar_global - accum
            return name, local, length, kr, km, prog[local % len(prog)]
        accum += length
    return SECTIONS[-1][0], 0, SECTIONS[-1][1], SECTIONS[-1][2], SECTIONS[-1][3], SECTIONS[-1][4][0]


# ============================================================================
# ASSEMBLE PROGRESSION FOR ENTIRE TRACK
# ============================================================================

def assemble_full_progression():
    """Returns list of (chord_root_pc, intervals) for each bar."""
    progression = []
    for bar in range(TOTAL_BARS):
        name, bib, length, kr, km, roman = get_bar_info(bar)
        chord_root_pc, intervals = chord_in_key(kr, km, roman)
        progression.append((chord_root_pc, intervals))
    return progression


# ============================================================================
# DRUMS (techno modern)
# ============================================================================

def drums_for_bar(bar_global):
    name, bib, length, _, _, _ = get_bar_info(bar_global)
    events = []
    base = bar_global * BAR
    
    intensity = {
        'INTRO': 0.0,
        'BUILD': 0.4,
        'DROP1': 1.0,
        'BREAK': 0.15,
        'CLIMAX': 0.85,
        'DROP2': 1.15,
        'OUTRO': 0.4,
    }[name]
    
    if intensity < 0.1:
        return events
    
    # 4-on-floor kick
    for beat in range(4):
        t = base + beat * TPQ
        vel = min(127, int(110 * min(1.0, intensity)))
        if beat == 0: vel = min(127, vel + 8)
        events.append((t, Message('note_on', channel=9, note=KICK, velocity=vel, time=0)))
        events.append((t + TPQ//4, Message('note_off', channel=9, note=KICK, velocity=0, time=0)))
    
    if intensity < 0.3:
        return events
    
    # Hats
    for eighth in range(8):
        t = base + eighth * (TPQ//2)
        vel = min(127, max(20, int((60 if eighth%2==0 else 80) * intensity)))
        events.append((t, Message('note_on', channel=9, note=CLOSED_HAT, velocity=vel, time=0)))
        events.append((t + TPQ//8, Message('note_off', channel=9, note=CLOSED_HAT, velocity=0, time=0)))
    
    # Open hat offbeats
    if intensity >= 0.5:
        for beat in (1, 3):
            t = base + beat * TPQ + TPQ//2
            events.append((t, Message('note_on', channel=9, note=OPEN_HAT, velocity=85, time=0)))
            events.append((t + TPQ//3, Message('note_off', channel=9, note=OPEN_HAT, velocity=0, time=0)))
    
    # Clap on 2 and 4
    if intensity >= 0.5:
        for beat in (1, 3):
            t = base + beat * TPQ
            events.append((t, Message('note_on', channel=9, note=CLAP, velocity=min(127, int(105*intensity)), time=0)))
            events.append((t + TPQ//4, Message('note_off', channel=9, note=CLAP, velocity=0, time=0)))
    
    # Ride at high intensity
    if intensity >= 1.0:
        for beat in range(4):
            t = base + beat * TPQ + TPQ//2
            events.append((t, Message('note_on', channel=9, note=RIDE, velocity=55, time=0)))
            events.append((t + TPQ//4, Message('note_off', channel=9, note=RIDE, velocity=0, time=0)))
    
    # Crash on section starts
    if bib == 0 and name != 'INTRO':
        events.append((base, Message('note_on', channel=9, note=CRASH, velocity=110, time=0)))
        events.append((base + TPQ, Message('note_off', channel=9, note=CRASH, velocity=0, time=0)))
    
    # Snare roll buildup
    if (name in ('BUILD', 'BREAK', 'CLIMAX') and bib == length - 1):
        intervals = [TPQ//4]*4 + [TPQ//8]*8 + [TPQ//16]*8
        t_local = 0
        for i, iv in enumerate(intervals):
            if t_local >= BAR:
                break
            vel = 45 + int((i/len(intervals)) * 55)
            events.append((base + t_local, Message('note_on', channel=9, note=SNARE, velocity=vel, time=0)))
            events.append((base + t_local + iv//2, Message('note_off', channel=9, note=SNARE, velocity=0, time=0)))
            t_local += iv
    
    return events


# ============================================================================
# RISER
# ============================================================================

def riser_events_for_section(section_name, bib, length, bar_global):
    events = []
    # Last 2 bars of BUILD → riser into DROP1
    if section_name == 'BUILD' and bib >= length - 2:
        progress = (bib - (length - 2)) / 2
        n = 32
        for i in range(n):
            t_local = int((i / n) * BAR)
            t = bar_global * BAR + t_local
            pitch = 36 + int((i / n + progress) * 30)
            vel = 40 + int((i / n) * 50)
            events.append((t, Message('note_on', channel=3, note=pitch, velocity=vel, time=0)))
            events.append((t + TPQ//8, Message('note_off', channel=3, note=pitch, velocity=0, time=0)))
    
    # Last 2 bars of CLIMAX → epic riser into DROP2
    if section_name == 'CLIMAX' and bib >= length - 2:
        progress = (bib - (length - 2)) / 2
        n = 48
        for i in range(n):
            t_local = int((i / n) * BAR)
            t = bar_global * BAR + t_local
            pitch = 30 + int((i / n + progress) * 54)
            vel = 60 + int((i / n) * 60)
            events.append((t, Message('note_on', channel=3, note=pitch, velocity=vel, time=0)))
            events.append((t + TPQ//8, Message('note_off', channel=3, note=pitch, velocity=0, time=0)))
    
    return events


# ============================================================================
# MAIN BUILD
# ============================================================================

def main():
    print(f"🎼 LaBoom Techno v3 — Klassische Satzlehre × Modern Techno\n")
    
    print(f"Sektionen:")
    accum = 0
    for name, length, kr, km, _ in SECTIONS:
        key_name = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'][kr]
        mode = 'Dur' if km == 'major' else 'Moll'
        print(f"  bars {accum:3d}-{accum+length-1:3d}  {name:8s} {key_name}-{mode}  ({length} bars)")
        accum += length
    print(f"  TOTAL: {TOTAL_BARS} bars = {TOTAL_BARS * 4 * 60 / BPM:.1f}s @ {BPM} BPM\n")
    
    print(f"📐 Assembling progression…")
    progression = assemble_full_progression()
    
    # Validate
    for bar, (root, ivs) in enumerate(progression[:8]):
        name, bib, _, kr, km, roman = get_bar_info(bar)
        rn = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'][root]
        q = 'maj' if 4 in ivs else 'min'
        print(f"  bar {bar:2d}  {name}  {roman:5s}  → {rn}{q}")
    print(f"  …")
    
    print(f"\n🎵 Building voicings (4-stimmiger Satz)…")
    voicings = build_chorale(progression, TOTAL_BARS)
    print(f"  → {len(voicings)} bars of SATB voicings")
    
    # Build all layers
    print(f"\n🛠  Building layers:")
    
    # Pad (Choralsatz)
    pad_events, _ = chorale_to_events(voicings, TOTAL_BARS, channel_pad=4)
    print(f"  ✓ Pad (Choral): {sum(1 for _,m in pad_events if m.type=='note_on')} notes")
    
    # Bass (klassische Stimmführung — Stufen, Quintfall)
    # In INTRO/OUTRO: nur Pad-Bass, sonst Walking
    bass_events = []
    for bar in range(TOTAL_BARS):
        name, _, _, _, _, _ = get_bar_info(bar)
        if name in ('INTRO', 'BREAK', 'OUTRO'):
            # Sustained root bass
            base = bar * BAR
            bass_pitch = voicings[bar][0]
            bass_events.append((base, Message('note_on', channel=0, note=bass_pitch, velocity=85, time=0)))
            bass_events.append((base + BAR - TPQ//4, Message('note_off', channel=0, note=bass_pitch, velocity=0, time=0)))
        else:
            # Quarter-note walking bass (à la Beethoven)
            v = voicings[bar]
            next_v = voicings[bar+1] if bar+1 < TOTAL_BARS else v
            base = bar * BAR
            root = v[0]
            fifth = root + 7
            # bass voicing: 1-5-1+8-approach
            approach = next_v[0] + (1 if next_v[0] < root else -1)
            pattern = [root, fifth - 12 if fifth - 12 >= 28 else fifth, root, approach]
            
            for beat, pitch in enumerate(pattern):
                while pitch < 28: pitch += 12
                while pitch > 50: pitch -= 12
                t = base + beat * TPQ
                vel = 100 if beat == 0 else 78
                dur = int(TPQ * 0.85)
                bass_events.append((t, Message('note_on', channel=0, note=pitch, velocity=vel, time=0)))
                bass_events.append((t + dur, Message('note_off', channel=0, note=pitch, velocity=0, time=0)))
            
            # In DROP2 add 8th-note offbeat
            if name == 'DROP2':
                for beat in range(4):
                    t = base + beat * TPQ + TPQ//2
                    pitch = pattern[beat] + 12
                    while pitch > 50: pitch -= 12
                    bass_events.append((t, Message('note_on', channel=0, note=pitch, velocity=70, time=0)))
                    bass_events.append((t + TPQ//4, Message('note_off', channel=0, note=pitch, velocity=0, time=0)))
    print(f"  ✓ Bass (klassisch): {sum(1 for _,m in bass_events if m.type=='note_on')} notes")
    
    # Lead-Melodie: Mozart-Periodenbau für jede 8-Bar-Phrase
    lead_events = []
    # Build melody section by section, each section gets its own classical melody
    section_starts = []
    accum = 0
    for name, length, kr, km, _ in SECTIONS:
        section_starts.append((accum, length, kr, km, name))
        accum += length
    
    for sstart, slen, kr, km, name in section_starts:
        if name in ('INTRO', 'BREAK', 'OUTRO'):
            continue
        # Melodien sind 8-Bar-Phrasen
        n_phrases = slen // 8
        for ph in range(n_phrases):
            phrase_start = sstart + ph * 8
            phrase_prog = progression[phrase_start:phrase_start+8]
            
            register = {'BUILD': 72, 'DROP1': 74, 'CLIMAX': 76, 'DROP2': 79}[name]
            
            # FIX: use chord-aware melody that respects each bar's harmony
            melody = build_chord_aware_melody(kr, km, phrase_prog, register=register)
            # Add to events at phrase_start offset
            for s, d, p, v in melody:
                t = phrase_start * BAR + s
                lead_events.append((t, Message('note_on', channel=1, note=p, velocity=v, time=0)))
                lead_events.append((t + d, Message('note_off', channel=1, note=p, velocity=0, time=0)))
                # Octave double on accent notes in DROP2
                if name == 'DROP2' and v >= 90 and p < 84:
                    lead_events.append((t, Message('note_on', channel=1, note=p-12, velocity=v-15, time=0)))
                    lead_events.append((t + d, Message('note_off', channel=1, note=p-12, velocity=0, time=0)))
    print(f"  ✓ Lead (Mozart-Periode): {sum(1 for _,m in lead_events if m.type=='note_on')} notes")
    
    # Counter-Melodie in CLIMAX (parallele Terz unter Lead, klassische Manier)
    counter_events = []
    for ev_t, msg in lead_events:
        if msg.type == 'note_on' and msg.velocity > 0:
            bar = ev_t // BAR
            name, _, _, kr, km, _ = get_bar_info(bar)
            if name == 'CLIMAX':
                # Third below in current scale
                scale = MAJOR_SCALE if km == 'major' else HARMONIC_MINOR
                p = msg.note
                # Step 2 scale degrees down
                pc = p % 12
                scale_pcs = sorted([(kr + s) % 12 for s in scale])
                if pc in scale_pcs:
                    idx = scale_pcs.index(pc)
                    new_pc = scale_pcs[(idx - 2) % len(scale_pcs)]
                    new_p = p - 12
                    while new_p % 12 != new_pc: new_p += 1
                    counter_events.append((ev_t, Message('note_on', channel=5, note=new_p, velocity=msg.velocity-20, time=0)))
        elif msg.type == 'note_off':
            bar = ev_t // BAR
            name, _, _, kr, km, _ = get_bar_info(bar)
            if name == 'CLIMAX':
                # Find matching counter pitch — simplification: send all-off
                counter_events.append((ev_t, Message('note_off', channel=5, note=msg.note - 4, velocity=0, time=0)))
    print(f"  ✓ Counter-melody (Terz-Parallele): {sum(1 for _,m in counter_events if m.type=='note_on')} notes")
    
    # Drums
    drum_events = []
    for bar in range(TOTAL_BARS):
        drum_events.extend(drums_for_bar(bar))
    print(f"  ✓ Drums (4-on-floor + Variation): {sum(1 for _,m in drum_events if m.type=='note_on')} hits")
    
    # Riser
    riser_events = []
    for bar in range(TOTAL_BARS):
        name, bib, length, _, _, _ = get_bar_info(bar)
        riser_events.extend(riser_events_for_section(name, bib, length, bar))
    print(f"  ✓ Riser: {sum(1 for _,m in riser_events if m.type=='note_on')} notes")
    
    # ----- Assemble MIDI -----
    mid_out = MidiFile(ticks_per_beat=TPQ)
    meta = MidiTrack()
    meta.append(MetaMessage('set_tempo', tempo=int(60_000_000 / BPM), time=0))
    meta.append(MetaMessage('time_signature', numerator=4, denominator=4, time=0))
    meta.append(MetaMessage('track_name', name='LaBoom Techno v3 - Klassisch', time=0))
    mid_out.tracks.append(meta)
    
    # Bass
    bass_track = events_to_track(bass_events, name="Bass (klassisch)")
    bass_track.insert(0, Message('program_change', channel=0, program=38, time=0))  # Synth Bass
    mid_out.tracks.append(bass_track)
    
    # Lead
    lead_track = events_to_track(lead_events, name="Lead (Mozart)")
    lead_track.insert(0, Message('program_change', channel=1, program=81, time=0))  # Saw Lead
    mid_out.tracks.append(lead_track)
    
    # Pad (Choralsatz)
    pad_track = events_to_track(pad_events, name="Pad (Choralsatz)")
    pad_track.insert(0, Message('program_change', channel=4, program=89, time=0))  # Warm Pad
    mid_out.tracks.append(pad_track)
    
    # Counter
    counter_track = events_to_track(counter_events, name="Counter (Terzparallele)")
    counter_track.insert(0, Message('program_change', channel=5, program=48, time=0))  # Strings
    mid_out.tracks.append(counter_track)
    
    # Riser
    riser_track = events_to_track(riser_events, name="Riser")
    riser_track.insert(0, Message('program_change', channel=3, program=84, time=0))
    mid_out.tracks.append(riser_track)
    
    # Drums
    mid_out.tracks.append(events_to_track(drum_events, name="Drums"))
    
    out_path = os.path.join(OUT_DIR, "laboom_TECHNO_v3.mid")
    mid_out.save(out_path)
    print(f"\n  🎶 SAVED → {out_path}")
    return out_path


def events_to_track(events, name="Track"):
    events = sorted(events, key=lambda x: (x[0], 0 if x[1].type == 'note_off' else 1))
    track = MidiTrack()
    track.append(MetaMessage('track_name', name=name, time=0))
    last_t = 0
    for t, msg in events:
        msg = msg.copy()
        msg.time = max(0, t - last_t)
        track.append(msg)
        last_t = t
    return track


if __name__ == '__main__':
    main()
