#!/usr/bin/env python3
"""
LaBoom Trance v4 — Classic Trance Edition (1994-2000)
======================================================

Synthese aus drei Welten:

A) ORIGINAL LA-BOOM-MATERIAL (G-Moll, 84 BPM) — Melodie-Essenz
B) KLASSISCHE SATZLEHRE (Mozart/Beethoven/Schubert) — Voice-Leading, Funktionsharmonik
C) CLASSIC-TRANCE-ARCHITEKTUR (Energy 52, Robert Miles, Paul van Dyk, Binary Finary, ATB,
   Veracocha, Gouryella, System F, Rank 1, Chicane, BT, William Orbit, Solar Stone)

TRANCE-FORMEL (extrahiert aus den Top-Anthems):
    BPM:         138  (Trance-Standard, Café Del Mar / For An Angel)
    Tonart:      F-Moll  (Children, Café Del Mar)
                 Modulation in 2. Hälfte: F-Moll → A♭-Dur (parallel) → C-Moll
    Form:        32-bar Intro · 16b Build1 · 32b Drop1 · 16b Break · 16b BuildUp ·
                 32b Climax-Drop · 16b Outro
    Bass:        Sidechain-gepumpter Off-Beat / Rolling 16th (Café Del Mar Stil)
    Drums:       4-on-floor Kick · Open-Hat Off-Beat · Snare/Clap 2&4 · Snare-Roll-Builds
    Leads:       Supersaw (4 detuned Saws) · Pluck-Arpeggio · Counter-Melodie
    Breakdown:   Drums raus · Piano-Melodie pur (Robert Miles "Children" Trick)
    Build:       Snare-Roll 1/4 → 1/8 → 1/16 + Riser-Pad + Filter-Sweep
    Drop:        Volle Kick zurück, Supersaw spielt Hauptmelodie, Pluck-Arp läuft Counter

KLASSISCHE SATZLEHRE-PRINZIPIEN (bleiben aus v3):
  ✓ 4-stimmiger Pad-Satz (S/A/T/B)
  ✓ Voice-Leading (max 2 Halbtöne Bewegung)
  ✓ Keine Quint- oder Oktavparallelen
  ✓ Leittonauflösung (e → f in F-Moll)
  ✓ Septimauflösung
  ✓ Schubert-Mediante für Modulation
  ✓ Mozart-Periodenbau (Vordersatz/Nachsatz)

AKKORDFOLGEN (extrahiert aus Top-Trance):
  Café Del Mar:   i  – VI  – VII – iv      (Fm – Db – Eb – Bbm)
  Children:       i  – VI  – III – VII     (Fm – Db – Ab – Eb)
  For An Angel:   i  – III – iv  – VII     (Fm – Ab – Bbm – Eb)
  Climax Lift:    Ab – Eb  – Fm  – Db      (relative Dur als Befreiung)
"""

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

random.seed(1996)  # Goldener Trance-Jahrgang (Children, Café Del Mar Re-Release)

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

TPQ = 480
BPM = 138
BAR = 4 * TPQ
EIGHTH = TPQ // 2
SIXTEENTH = TPQ // 4
QUARTER = TPQ
HALF = TPQ * 2

# GM Drum-Map
KICK, SNARE, CLAP = 36, 38, 39
CLOSED_HAT, OPEN_HAT, RIDE = 42, 46, 51
CRASH, CRASH2, SPLASH = 49, 57, 55
TAMBOURINE, COWBELL, TRIANGLE = 54, 56, 81
LO_TOM, MID_TOM, HI_TOM = 41, 47, 50

# ============================================================================
# TONLEITER & AKKORD-DEFINITIONEN (F-Moll als Trance-Standard-Tonart)
# ============================================================================

# F = pitch class 5
KEY_ROOT_PC = 5  # F
KEY_MODE = 'minor'

# Skalen-Pitch-Classes
MINOR_PCS = [(5 + s) % 12 for s in [0, 2, 3, 5, 7, 8, 10]]    # F natural minor
HARM_PCS  = [(5 + s) % 12 for s in [0, 2, 3, 5, 7, 8, 11]]    # F harmonic minor (Leitton E)

# Akkord-Definitionen (chord_root_pc, [intervals from root])
# Roman in Fm:
#   i  = Fm (F-Ab-C)        = (5,  [0,3,7])
#   ii°= Gdim                = (7,  [0,3,6])
#   III= Ab (Ab-C-Eb)        = (8,  [0,4,7])
#   iv = Bbm (Bb-Db-F)       = (10, [0,3,7])
#   v  = Cm (C-Eb-G)         = (0,  [0,3,7])
#   V  = C (C-E-G) harm.     = (0,  [0,4,7])
#   V7 = C7                  = (0,  [0,4,7,10])
#   VI = Db (Db-F-Ab)        = (1,  [0,4,7])
#   VII= Eb (Eb-G-Bb)        = (3,  [0,4,7])

CHORD_LIB = {
    'i':   (5,  [0, 3, 7]),
    'i7':  (5,  [0, 3, 7, 10]),
    'iv':  (10, [0, 3, 7]),
    'iv7': (10, [0, 3, 7, 10]),
    'v':   (0,  [0, 3, 7]),
    'V':   (0,  [0, 4, 7]),
    'V7':  (0,  [0, 4, 7, 10]),
    'III': (8,  [0, 4, 7]),
    'VI':  (1,  [0, 4, 7]),
    'VII': (3,  [0, 4, 7]),
    'ii°': (7,  [0, 3, 6]),
    'bII': (6,  [0, 4, 7]),  # Neapolitanisch (Gb)
    # Modulation in parallele Dur-Tonart Ab (für Climax)
    'I_Ab':  (8, [0, 4, 7]),    # Ab
    'V_Ab':  (3, [0, 4, 7]),    # Eb
    'vi_Ab': (5, [0, 3, 7]),    # Fm (=tonika der ausgangstonart!) — Pivot
    'IV_Ab': (1, [0, 4, 7]),    # Db
    'ii_Ab': (10,[0, 3, 7]),    # Bbm
}

# Top-Trance Chord-Progressions (alle in/um Fm)
PROG_CAFE_DEL_MAR = ['i',  'VI', 'VII', 'iv']    # Fm-Db-Eb-Bbm
PROG_CHILDREN     = ['i',  'VI', 'III', 'VII']   # Fm-Db-Ab-Eb
PROG_FOR_AN_ANGEL = ['i',  'III','iv',  'VII']   # Fm-Ab-Bbm-Eb
PROG_CLIMAX_LIFT  = ['I_Ab','V_Ab','vi_Ab','IV_Ab']  # Ab-Eb-Fm-Db (Befreiung)
PROG_OUTRO        = ['iv', 'V',  'i',   'i']     # Bbm-C-Fm-Fm (echte Kadenz, Leitton)


# ============================================================================
# 4-STIMMIGER SATZ MIT VOICE-LEADING (aus v3 übernommen + verfeinert)
# ============================================================================

def chord_pcs(roman):
    root_pc, intervals = CHORD_LIB[roman]
    return [(root_pc + iv) % 12 for iv in intervals]

def build_voicing(roman, prev_voicing=None,
                  ranges=((36, 55), (50, 67), (57, 74), (62, 81))):
    """4-stimmiger Satz (Bass, Tenor, Alt, Sopran) mit Voice-Leading.
    Vermeidet Quint-/Oktavparallelen, bevorzugt Schritte."""
    root_pc, intervals = CHORD_LIB[roman]
    pcs = [(root_pc + iv) % 12 for iv in intervals]
    chord_root = pcs[0]

    def cands_in(rng, pc_set):
        lo, hi = rng
        return [p for p in range(lo, hi + 1) if p % 12 in pc_set]

    if prev_voicing is None:
        # Initial: Root-5-3-1 close voicing
        bass = chord_root + 36
        while bass < ranges[0][0]: bass += 12
        while bass > ranges[0][1]: bass -= 12
        ten = next((p for p in cands_in(ranges[1], {pcs[2] if len(pcs) > 2 else pcs[0]})), ranges[1][0])
        alt = next((p for p in cands_in(ranges[2], {pcs[1] if len(pcs) > 1 else pcs[0]})), ranges[2][0])
        sop = next((p for p in cands_in(ranges[3], {pcs[0]})), ranges[3][0])
        v = [bass, ten, alt, sop]
        # ensure ordering
        v.sort()
        return v

    pc_set = set(pcs)
    new = [None] * 4
    # Bass first: root, smallest motion
    bass_cands = [p for p in cands_in(ranges[0], {chord_root})]
    if bass_cands:
        new[0] = min(bass_cands, key=lambda p: abs(p - prev_voicing[0]))
    else:
        new[0] = prev_voicing[0]

    # Other voices: nearest chord tone, avoid parallels with bass
    for vi in (3, 2, 1):  # sop, alt, ten
        cands = cands_in(ranges[vi], pc_set)
        prev = prev_voicing[vi]
        cands.sort(key=lambda p: abs(p - prev))
        bass_prev, bass_new = prev_voicing[0], new[0]
        bass_motion = bass_new - bass_prev
        chosen = None
        for c in cands:
            vm = c - prev
            prev_int = (prev - bass_prev) % 12
            new_int = (c - bass_new) % 12
            par5 = prev_int == 7 and new_int == 7 and bass_motion and vm
            par8 = prev_int == 0 and new_int == 0 and bass_motion and vm
            if par5 or par8:
                continue
            # parallels with already-placed upper voices?
            par_other = False
            for oi in range(vi + 1, 4):
                if new[oi] is None: continue
                op, on = prev_voicing[oi], new[oi]
                pi = (prev - op) % 12
                ni = (c - on) % 12
                if pi == ni and pi in (0, 7) and (on - op) and vm:
                    par_other = True
                    break
            if par_other:
                continue
            chosen = c
            break
        new[vi] = chosen if chosen is not None else (cands[0] if cands else prev)

    # ensure ordering (no voice crossing)
    for i in range(3):
        if new[i] >= new[i + 1]:
            new[i + 1] = new[i] + 1
    return new


# ============================================================================
# LA-BOOM-MELODIE-ESSENZ (G-Moll Original → nach F-Moll transponiert: −2 Halbtöne)
# ============================================================================
# Charakteristische Melodie-Kontur aus dem Original (transponiert):
# F-G-Ab-Bb-Ab-G-F (typische Moll-Phrase) — wir nehmen als Grund-Motiv

# Motif in F harmonic minor (using degrees 1-7)
# F=5, G=7, Ab=8, Bb=10, C=0, Db=1, Eb=3 (nat) oder E=4 (harm)
LABOOM_MOTIF_PC = [5, 7, 8, 10, 8, 7, 5]  # F-G-Ab-Bb-Ab-G-F

def closest_octave(pc, near):
    """Return MIDI pitch with given pitch-class closest to `near`."""
    base = (near // 12) * 12 + pc
    cands = [base - 12, base, base + 12]
    return min(cands, key=lambda p: abs(p - near))


def laboom_motif_at(start_register=72):
    """Bring LA-Boom-Motif als MIDI-Pitches (Sopran-Register)."""
    pitches = []
    cur = start_register
    for pc in LABOOM_MOTIF_PC:
        cur = closest_octave(pc, cur)
        pitches.append(cur)
    return pitches


# ============================================================================
# AKKORD-AWARE MELODY (chord-tone on strong beats, Mozart-style)
# ============================================================================

def is_chord_tone(pitch, roman):
    pcs = chord_pcs(roman)
    return pitch % 12 in pcs

def nearest_chord_tone(pitch, roman, direction=0):
    """Nearest chord tone to `pitch`. direction: 0=any, +1=up, −1=down."""
    pcs = chord_pcs(roman)
    candidates = []
    for octave in range(-2, 3):
        base = (pitch // 12 + octave) * 12
        for pc in pcs:
            candidates.append(base + pc)
    if direction == 0:
        return min(candidates, key=lambda p: abs(p - pitch))
    elif direction > 0:
        ups = [p for p in candidates if p > pitch]
        return min(ups, key=lambda p: p - pitch) if ups else pitch
    else:
        downs = [p for p in candidates if p < pitch]
        return max(downs, key=lambda p: pitch - p) if downs else pitch

def build_trance_lead_phrase(chord_progression, start_pitch=72, num_bars=8):
    """MINIMALISTISCHES TRANCE-HOOK-PATTERN:
    Pro Bar: 'ta-ta-ta-ta...........' — 4 schnelle Repetitionen,
    dann ein langer ausgehaltener Ton. ALLES auf der DOMINANTE (5. Stufe)
    des aktuellen Akkords. Klassisches Trance-Riff-Schema:
    
        BAR LAYOUT (2 BARS PRO PHRASE):
        Bar A:  | x  x  x  x  _  _  _  _ | x _ _ _ _ _ _ _ |
                  16  16 16 16  (rest)     half-note hold
        
        Aber kompakter pro Bar:
        | ta ta ta ta ---------- |  ('ta' = Achtel, '----' = halbe Note)
        
    Tonhöhe: 5. Stufe des Akkords (dominante Spannung), pro Phrase
    wechselt sie mit dem Akkord. Letzter Takt landet auf Tonika (F).
    
    Inspiration: Energy 52 'Café Del Mar', Binary Finary '1998',
    System F 'Out of the Blue' — alle nutzen genau dieses Pattern.
    """
    notes = []
    bars_per_chord = max(1, num_bars // len(chord_progression))

    for bar_i in range(num_bars):
        bar_start = bar_i * BAR
        chord_idx = (bar_i // bars_per_chord) % len(chord_progression)
        roman = chord_progression[chord_idx]
        root_pc, intervals = CHORD_LIB[roman]
        
        # DOMINANTE = 5. Stufe des Akkords (Quinte)
        # Für Moll-i (Fm) → C ist die Quinte
        # Für VI (Db) → Ab
        # Für VII (Eb) → Bb
        # Für iv (Bbm) → F (was zufällig die Tonika ist!)
        fifth_pc = (root_pc + 7) % 12
        dominant_pitch = closest_octave(fifth_pc, start_pitch)
        
        # 'ta-ta-ta-ta' = 4 Achtel auf den ersten Beat (= Beat 1)
        # gefolgt von langer Note (halbe + viertel = 3 Beats Pause/Ton)
        # Pattern in Ticks (BAR = 4*QUARTER = 16*SIXTEENTH = 8*EIGHTH):
        
        # Variante: 4 schnelle Repetitionen + langer Halt
        # ta-ta-ta-ta . . . . LAAAAAANG . . . . . . . .
        # |--|--|--|--|        |---------------------|
        # 16 16 16 16         half + quarter dotted
        
        # Schnelle 4 "ta"s (16tel) auf Beat 1
        tata_dur = SIXTEENTH
        for i in range(4):
            t = bar_start + i * SIXTEENTH
            vel = 105 if i == 0 else 90  # erster akzentuiert
            notes.append((t, tata_dur - 5, dominant_pitch, vel))
        
        # Lange Halt-Note: ab Beat 2 bis Bar-Ende = 3 Beats
        long_start = bar_start + QUARTER  # ab Beat 2
        long_dur = 3 * QUARTER - 10  # bis kurz vor Bar-Ende
        # Eine Oktave höher für Hook-Effekt? — nein, gleicher Ton, sonst zerfällt das Pattern
        notes.append((long_start, long_dur, dominant_pitch, 100))
        
        # Im letzten Bar der Progression: lande auf TONIKA (F)
        if bar_i == num_bars - 1:
            # Überschreibe die letzte Note mit Tonika
            tonic_pitch = closest_octave(KEY_ROOT_PC, dominant_pitch)
            # Ersetze die long-note
            notes[-1] = (long_start, long_dur, tonic_pitch, 108)

    return notes


def build_trance_lead_minimal(chord_progression, start_pitch=72, num_bars=8,
                              variant='basic'):
    """Erweiterte Varianten für Drop2/Climax — gleiche Idee, mehr Drama."""
    notes = []
    bars_per_chord = max(1, num_bars // len(chord_progression))

    for bar_i in range(num_bars):
        bar_start = bar_i * BAR
        chord_idx = (bar_i // bars_per_chord) % len(chord_progression)
        roman = chord_progression[chord_idx]
        root_pc, intervals = CHORD_LIB[roman]
        fifth_pc = (root_pc + 7) % 12
        dominant_pitch = closest_octave(fifth_pc, start_pitch)
        # Oktav-Hoch für Climax
        if variant == 'octave':
            dominant_pitch += 12
        
        # 4 ta-ta-ta-ta + lange Note (gleiche Logik)
        for i in range(4):
            t = bar_start + i * SIXTEENTH
            vel = 110 if i == 0 else 95
            notes.append((t, SIXTEENTH - 5, dominant_pitch, vel))
        
        long_start = bar_start + QUARTER
        long_dur = 3 * QUARTER - 10
        notes.append((long_start, long_dur, dominant_pitch, 105))
        
        if bar_i == num_bars - 1:
            tonic_pitch = closest_octave(KEY_ROOT_PC, dominant_pitch)
            notes[-1] = (long_start, long_dur, tonic_pitch, 112)

    return notes


# ============================================================================
# PLUCK-ARPEGGIO (16th notes, in chord)
# ============================================================================

def build_arpeggio(chord_progression, num_bars=8, register=72, pattern='updown'):
    """16th-note Pluck-Arpeggio über Akkordfolge."""
    notes = []
    bars_per_chord = max(1, num_bars // len(chord_progression))
    for bar_i in range(num_bars):
        bar_start = bar_i * BAR
        chord_idx = (bar_i // bars_per_chord) % len(chord_progression)
        roman = chord_progression[chord_idx]
        root_pc, intervals = CHORD_LIB[roman]
        # 4-note voicing: root, 3rd, 5th, octave
        seq = []
        for iv in intervals[:3]:
            seq.append(closest_octave((root_pc + iv) % 12, register))
        seq.append(closest_octave(root_pc, register + 12))  # octave
        if pattern == 'updown':
            full = seq + list(reversed(seq[1:-1]))  # up-down
        elif pattern == 'up':
            full = seq + seq
        else:
            full = seq + seq[::-1]
        # 16 sixteenths per bar
        for s16 in range(16):
            p = full[s16 % len(full)]
            t = bar_start + s16 * SIXTEENTH
            notes.append((t, SIXTEENTH, p, 72 if s16 % 4 == 0 else 64))
    return notes


# ============================================================================
# BASS-LINE (Trance-Style: sidechain-pumped 8th notes + occasional rolling 16th)
# ============================================================================

def build_trance_bass(chord_progression, num_bars=16, style='offbeat'):
    """Sidechain-Off-Beat-Bass (typisch Café Del Mar / Energy 52)."""
    notes = []
    bars_per_chord = max(1, num_bars // len(chord_progression))
    for bar_i in range(num_bars):
        bar_start = bar_i * BAR
        chord_idx = (bar_i // bars_per_chord) % len(chord_progression)
        roman = chord_progression[chord_idx]
        root_pc, intervals = CHORD_LIB[roman]
        bass_pitch = root_pc + 36  # ~C2 register
        while bass_pitch < 36: bass_pitch += 12
        while bass_pitch > 48: bass_pitch -= 12

        if style == 'offbeat':
            # 8th notes on the OFF-beat (typical sidechain bass)
            for beat in range(4):
                t = bar_start + beat * QUARTER + EIGHTH  # off-beat
                notes.append((t, EIGHTH - 20, bass_pitch, 95))
        elif style == 'rolling':
            # Rolling 16ths (Veracocha "Carte Blanche" style)
            fifth = (root_pc + 7) % 12
            fifth_pitch = closest_octave(fifth, bass_pitch)
            seq = [bass_pitch, bass_pitch, fifth_pitch, bass_pitch] * 4
            for s in range(16):
                t = bar_start + s * SIXTEENTH
                vel = 100 if s % 4 == 0 else 75
                notes.append((t, SIXTEENTH - 10, seq[s], vel))
        elif style == 'walking':
            # Walking quarter-bass on every beat
            for beat in range(4):
                t = bar_start + beat * QUARTER
                notes.append((t, QUARTER - 30, bass_pitch, 90))
    return notes


# ============================================================================
# DRUM-PROGRAMMING (Trance-Standard)
# ============================================================================

def build_drums(num_bars, intensity='full', has_kick=True):
    """4-on-Floor Trance Drums.
    intensity: 'minimal', 'build', 'full', 'climax'
    """
    notes = []
    for bar_i in range(num_bars):
        bar_start = bar_i * BAR

        if has_kick:
            # 4-on-floor kick
            for beat in range(4):
                t = bar_start + beat * QUARTER
                vel = 115 if intensity == 'climax' else 108
                notes.append((t, EIGHTH, KICK, vel))

        if intensity in ('full', 'climax'):
            # Clap on beats 2 and 4
            for beat in (1, 3):
                t = bar_start + beat * QUARTER
                notes.append((t, EIGHTH, CLAP, 100))
            # Closed hi-hat on 8ths
            for ei in range(8):
                t = bar_start + ei * EIGHTH
                # Skip kick beats slightly to let kick breathe
                if ei % 2 == 0:
                    notes.append((t, SIXTEENTH, CLOSED_HAT, 60))
                else:
                    notes.append((t, SIXTEENTH, OPEN_HAT, 85))  # off-beat open hat (signature)
            # Ride accent every other bar
            if bar_i % 2 == 0 and intensity == 'climax':
                notes.append((bar_start, QUARTER, RIDE, 70))
        elif intensity == 'build':
            # Closed hat only, gradually denser
            density = min(16, 4 + bar_i * 2)
            for d in range(density):
                t = bar_start + d * (BAR // density)
                notes.append((t, SIXTEENTH, CLOSED_HAT, 60 + d * 2))
            # Snare roll in last 2 bars
            if bar_i >= num_bars - 2:
                # Build snare roll: bar n-2 = 1/8, bar n-1 = 1/16, last beat = 1/32
                subdiv = 8 if bar_i == num_bars - 2 else 16
                for s in range(subdiv):
                    t = bar_start + s * (BAR // subdiv)
                    notes.append((t, SIXTEENTH, SNARE, 70 + s * 3))

        # Crash on bar 1 of new sections
        if bar_i == 0:
            notes.append((bar_start, BAR, CRASH, 110))

    return notes


# ============================================================================
# PAD (Choralsatz, 4-stimmig, Voice-Leading)
# ============================================================================

def build_pad_progression(chord_progression, num_bars=16):
    """Halt-Pad: 4-stimmiger Satz, ein Akkord pro Bar (oder pro 2 Bars)."""
    notes_per_voice = [[], [], [], []]  # bass, ten, alt, sop
    bars_per_chord = max(1, num_bars // len(chord_progression))
    prev_voicing = None
    for bar_i in range(num_bars):
        bar_start = bar_i * BAR
        chord_idx = (bar_i // bars_per_chord) % len(chord_progression)
        roman = chord_progression[chord_idx]
        voicing = build_voicing(roman, prev_voicing)
        for vi, p in enumerate(voicing):
            notes_per_voice[vi].append((bar_start, BAR, p, 70))
        prev_voicing = voicing
    return notes_per_voice


# ============================================================================
# RISER (white-noise-like sweep via sustained pitch with crescendo)
# ============================================================================

def build_riser(num_bars=2, start_pitch=48, end_pitch=84):
    """Pitch-Sweep + Volume-Crescendo. Simuliert White-Noise-Riser."""
    notes = []
    total_ticks = num_bars * BAR
    # 32 steps for smooth glissando
    steps = 32
    step_ticks = total_ticks // steps
    for s in range(steps):
        pitch = start_pitch + int((end_pitch - start_pitch) * s / steps)
        vel = 50 + int(60 * s / steps)
        notes.append((s * step_ticks, step_ticks + 30, pitch, vel))
    return notes


# ============================================================================
# MIDI ASSEMBLY
# ============================================================================

class TrackBuilder:
    def __init__(self, name, channel, program=0, volume=100):
        self.name = name
        self.channel = channel
        self.program = program
        self.volume = volume
        self.events = []  # (abs_tick, msg_factory)

    def add_note(self, start_tick, dur_ticks, pitch, velocity):
        pitch = max(0, min(127, int(pitch)))
        velocity = max(1, min(127, int(velocity)))
        self.events.append((start_tick, 'on', pitch, velocity))
        self.events.append((start_tick + dur_ticks, 'off', pitch, 64))

    def add_notes(self, notes, offset=0):
        for t, d, p, v in notes:
            self.add_note(t + offset, d, p, v)

    def to_midi_track(self):
        trk = MidiTrack()
        trk.append(MetaMessage('track_name', name=self.name, time=0))
        if self.channel != 9:  # 9 = drums (program-independent)
            trk.append(Message('program_change', program=self.program, channel=self.channel, time=0))
        trk.append(Message('control_change', control=7, value=self.volume, channel=self.channel, time=0))
        # Sort events
        self.events.sort(key=lambda e: (e[0], 0 if e[1] == 'off' else 1))
        prev_t = 0
        for abs_t, kind, pitch, vel in self.events:
            delta = abs_t - prev_t
            if kind == 'on':
                trk.append(Message('note_on', note=pitch, velocity=vel, channel=self.channel, time=delta))
            else:
                trk.append(Message('note_off', note=pitch, velocity=vel, channel=self.channel, time=delta))
            prev_t = abs_t
        return trk


def build_trance_track():
    """Assembliert das komplette Trance-Arrangement."""
    mid = MidiFile(ticks_per_beat=TPQ)

    # Tempo + meta track
    meta = MidiTrack()
    meta.append(MetaMessage('track_name', name='Tempo & Meta', time=0))
    meta.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(BPM), time=0))
    meta.append(MetaMessage('time_signature', numerator=4, denominator=4, time=0))
    meta.append(MetaMessage('key_signature', key='Fm', time=0))
    mid.tracks.append(meta)

    # GM-Programme (FluidR3 SoundFont)
    # 0 = Acoustic Grand
    # 50 = Synth Strings 1 (pad)
    # 51 = Synth Strings 2
    # 81 = Lead 1 (Square) — pluck
    # 82 = Lead 2 (Sawtooth) — supersaw lead
    # 88 = Pad 1 (New Age)
    # 89 = Pad 2 (Warm)
    # 91 = Pad 4 (Choir)
    # 99 = FX 4 (Atmosphere) — riser
    # 38 = Synth Bass 1
    # 39 = Synth Bass 2

    pad_b   = TrackBuilder('Pad_Bass',   channel=0,  program=89, volume=85)
    pad_t   = TrackBuilder('Pad_Tenor',  channel=1,  program=89, volume=80)
    pad_a   = TrackBuilder('Pad_Alto',   channel=2,  program=89, volume=80)
    pad_s   = TrackBuilder('Pad_Sopran', channel=3,  program=91, volume=85)  # Choir
    lead    = TrackBuilder('Supersaw_Lead', channel=4, program=82, volume=110)
    lead2   = TrackBuilder('Supersaw_Layer', channel=5, program=81, volume=85)  # detune layer
    pluck   = TrackBuilder('Pluck_Arp',  channel=6,  program=84, volume=88)  # Lead 5 (charang)
    bass    = TrackBuilder('Synth_Bass', channel=7,  program=38, volume=110)
    piano   = TrackBuilder('Piano',      channel=8,  program=0,  volume=95)
    drums   = TrackBuilder('Drums',      channel=9,  program=0,  volume=115)
    riser   = TrackBuilder('Riser',      channel=10, program=99, volume=85)
    counter = TrackBuilder('Counter',    channel=11, program=50, volume=80)  # Synth Strings

    # ====================================================================
    # ARRANGEMENT (alle Zeiten in Bars; total ≈ 160 Bars @ 138 BPM ≈ 2:47)
    # ====================================================================
    cur_bar = 0

    def bars_to_ticks(b):
        return b * BAR

    # ---- INTRO (16b): nur Pad + sparsame Kick ab b8 ----
    intro_bars = 16
    intro_prog = PROG_CHILDREN
    pad_voices = build_pad_progression(intro_prog, intro_bars)
    offset = bars_to_ticks(cur_bar)
    for vi, builder in enumerate([pad_b, pad_t, pad_a, pad_s]):
        builder.add_notes(pad_voices[vi], offset=offset)
    # Sparse kick last 8 bars
    sparse_kick = []
    for b in range(8, 16):
        for beat in range(4):
            sparse_kick.append((b * BAR + beat * QUARTER, EIGHTH, KICK, 90))
    drums.add_notes(sparse_kick, offset=offset)
    cur_bar += intro_bars

    # ---- BUILD1 (16b): Drums build, lead vorbereitet ----
    build1_bars = 16
    build1_prog = PROG_CAFE_DEL_MAR
    pad_voices = build_pad_progression(build1_prog, build1_bars)
    offset = bars_to_ticks(cur_bar)
    for vi, builder in enumerate([pad_b, pad_t, pad_a, pad_s]):
        builder.add_notes(pad_voices[vi], offset=offset)
    drums.add_notes(build_drums(build1_bars, intensity='build'), offset=offset)
    # Bass starts (off-beat) bar 8
    bass.add_notes(build_trance_bass(build1_prog, num_bars=8, style='offbeat'),
                   offset=offset + bars_to_ticks(8))
    # Pluck-Arpeggio joins last 8 bars
    pluck.add_notes(build_arpeggio(build1_prog, num_bars=8, register=72),
                    offset=offset + bars_to_ticks(8))
    # Riser bars 14-15
    riser_notes = build_riser(num_bars=2, start_pitch=48, end_pitch=84)
    riser.add_notes(riser_notes, offset=offset + bars_to_ticks(14))
    cur_bar += build1_bars

    # ---- DROP1 (32b): Full trance, Supersaw + Pluck + Bass + Drums ----
    drop1_bars = 32
    drop1_prog = PROG_CAFE_DEL_MAR  # i-VI-VII-iv
    pad_voices = build_pad_progression(drop1_prog, drop1_bars)
    offset = bars_to_ticks(cur_bar)
    for vi, builder in enumerate([pad_b, pad_t, pad_a, pad_s]):
        builder.add_notes(pad_voices[vi], offset=offset)
    drums.add_notes(build_drums(drop1_bars, intensity='full'), offset=offset)
    bass.add_notes(build_trance_bass(drop1_prog, num_bars=drop1_bars, style='offbeat'),
                   offset=offset)
    pluck.add_notes(build_arpeggio(drop1_prog, num_bars=drop1_bars, register=72),
                    offset=offset)
    # Lead: spielt LaBoom-Motif transformiert, in 8-Takt-Phrasen
    lead_phrase = build_trance_lead_phrase(drop1_prog, start_pitch=77, num_bars=drop1_bars)
    lead.add_notes(lead_phrase, offset=offset)
    # Layer 2: gleiches Material, eine Oktave tiefer (Supersaw-Fett)
    lead2_phrase = [(t, d, p - 12, max(60, v - 15)) for t, d, p, v in lead_phrase]
    lead2.add_notes(lead2_phrase, offset=offset)
    # Counter-Melodie spielt LaBoom-Motif im Hintergrund (vermisst sonst)
    # — NEIN, das würde das minimalistische Pattern verwaschen.
    # Stattdessen: nichts, Pluck-Arp trägt die Harmonie.
    cur_bar += drop1_bars

    # ---- BREAK (16b): Drums OUT, Piano-Melodie pur (Robert Miles trick) ----
    break_bars = 16
    break_prog = PROG_CHILDREN  # Children-style: Fm-Db-Ab-Eb
    pad_voices = build_pad_progression(break_prog, break_bars)
    offset = bars_to_ticks(cur_bar)
    for vi, builder in enumerate([pad_b, pad_t, pad_a, pad_s]):
        builder.add_notes(pad_voices[vi], offset=offset)
    # Piano spielt LaBoom-Motif solo (Children-Style)
    piano_phrase = build_trance_lead_phrase(break_prog, start_pitch=72, num_bars=break_bars)
    piano.add_notes(piano_phrase, offset=offset)
    # No kick, no bass — pure breakdown atmosphere
    # Hat tail nur in den letzten 4 Bars
    hat_tail = []
    for b in range(break_bars - 4, break_bars):
        for s in range(16):
            t = b * BAR + s * SIXTEENTH
            hat_tail.append((t, SIXTEENTH, CLOSED_HAT, 50 + s * 2))
    drums.add_notes(hat_tail, offset=offset)
    cur_bar += break_bars

    # ---- BUILD2 (16b): Climax-Vorbereitung, Modulation Fm → Ab ----
    build2_bars = 16
    # Pivot: Bbm fungiert als ii in Ab → wir nutzen progression mit pivot
    build2_prog = ['iv', 'V', 'i', 'VI'] + ['VI', 'VII', 'I_Ab', 'V_Ab']  # 8 Akkorde
    # Wir bauen das auf 16 Bars hoch (2 Bars pro Akkord)
    pad_voices = build_pad_progression(build2_prog, build2_bars)
    offset = bars_to_ticks(cur_bar)
    for vi, builder in enumerate([pad_b, pad_t, pad_a, pad_s]):
        builder.add_notes(pad_voices[vi], offset=offset)
    drums.add_notes(build_drums(build2_bars, intensity='build'), offset=offset)
    # Bass kommt zurück
    bass.add_notes(build_trance_bass(build2_prog, num_bars=build2_bars, style='offbeat'),
                   offset=offset)
    # Pluck weiterhin
    pluck.add_notes(build_arpeggio(build2_prog, num_bars=build2_bars, register=72),
                    offset=offset)
    # Counter-Melodie (Strings)
    counter_phrase = build_trance_lead_phrase(build2_prog, start_pitch=84, num_bars=build2_bars)
    # Halbiere velocity — soll noch zurückhaltend sein
    counter_phrase = [(t, d, p, max(50, v - 20)) for t, d, p, v in counter_phrase]
    counter.add_notes(counter_phrase, offset=offset)
    # Großer Riser in den letzten 4 Bars
    big_riser = build_riser(num_bars=4, start_pitch=36, end_pitch=96)
    riser.add_notes(big_riser, offset=offset + bars_to_ticks(12))
    cur_bar += build2_bars

    # ---- CLIMAX-DROP (32b): in Ab-Dur ("Befreiung"), volle Power ----
    climax_bars = 32
    climax_prog = PROG_CLIMAX_LIFT  # Ab-Eb-Fm-Db
    pad_voices = build_pad_progression(climax_prog, climax_bars)
    offset = bars_to_ticks(cur_bar)
    for vi, builder in enumerate([pad_b, pad_t, pad_a, pad_s]):
        builder.add_notes(pad_voices[vi], offset=offset)
    drums.add_notes(build_drums(climax_bars, intensity='climax'), offset=offset)
    bass.add_notes(build_trance_bass(climax_prog, num_bars=climax_bars, style='offbeat'),
                   offset=offset)
    pluck.add_notes(build_arpeggio(climax_prog, num_bars=climax_bars, register=72, pattern='updown'),
                    offset=offset)
    # Lead spielt jetzt in Ab-Dur, OKTAVIERT (Climax-Variante)
    climax_lead = build_trance_lead_minimal(climax_prog, start_pitch=80,
                                            num_bars=climax_bars, variant='octave')
    lead.add_notes(climax_lead, offset=offset)
    # Octave layer (eine Oktave tiefer)
    lead2_climax = [(t, d, p - 12, max(60, v - 12)) for t, d, p, v in climax_lead]
    lead2.add_notes(lead2_climax, offset=offset)
    # Counter-Melodie spielt parallele Quinten zur Dominante (Climax-Fett)
    counter_climax = [(t, d, p + 7, max(55, v - 22)) for t, d, p, v in climax_lead]
    counter.add_notes(counter_climax, offset=offset)
    cur_bar += climax_bars

    # ---- OUTRO (16b): Rück nach Fm, Filter-Down, Pad-Tail ----
    outro_bars = 16
    outro_prog = PROG_OUTRO  # iv-V-i-i (echte Kadenz)
    pad_voices = build_pad_progression(outro_prog, outro_bars)
    offset = bars_to_ticks(cur_bar)
    for vi, builder in enumerate([pad_b, pad_t, pad_a, pad_s]):
        builder.add_notes(pad_voices[vi], offset=offset)
    # Drums abklingend (erst 8b voll, dann 8b ausklingen)
    drums.add_notes(build_drums(8, intensity='full'), offset=offset)
    # Final kick on last downbeat
    drums.add_notes([(0, EIGHTH, KICK, 110)], offset=offset + bars_to_ticks(outro_bars - 1))
    bass.add_notes(build_trance_bass(outro_prog, num_bars=8, style='offbeat'),
                   offset=offset)
    # Piano spielt Motif zum Schluss (Echo)
    final_piano = build_trance_lead_phrase(outro_prog, start_pitch=72, num_bars=outro_bars)
    piano.add_notes(final_piano, offset=offset)
    cur_bar += outro_bars

    # Final crash
    drums.add_notes([(0, BAR, CRASH, 115)], offset=bars_to_ticks(cur_bar - 1))

    print(f"Total bars: {cur_bar} ≈ {cur_bar * 60 / BPM / 4:.1f}s")

    for tb in (pad_b, pad_t, pad_a, pad_s, lead, lead2, pluck, bass,
               piano, drums, riser, counter):
        mid.tracks.append(tb.to_midi_track())

    return mid


if __name__ == '__main__':
    mid = build_trance_track()
    out_mid = os.path.join(OUT_DIR, 'laboom_TRANCE_v4.mid')
    mid.save(out_mid)
    print(f"Saved: {out_mid}")
    # Note count
    total_notes = 0
    for trk in mid.tracks:
        for msg in trk:
            if msg.type == 'note_on' and msg.velocity > 0:
                total_notes += 1
    print(f"Notes: {total_notes}")
