#!/usr/bin/env python3 """ Midnight Drive — Classic 90s Dance/House ========================================= Driving four-on-the-floor dance track with arpeggiated synth lead, rolling bass, and atmospheric pads. 128 BPM, A minor progression. Structure: 32 bars (2 min at 128 BPM) Intro(4) → Verse(8) → Chorus(8) → Verse(8) → Chorus(4) → Outro(8) Instruments: T0: Synth Lead 1 (GM 80) — ascending arpeggios T1: Synth Bass 1 (GM 37) — driving root pulses T2: Drum set (Ch 10) — four-on-the-floor T3: Pad 4 Choir (GM 88) — warm sustaining chords T4: Brass Section (GM 59) — accent hits and stabs """ from midiutil import MIDIFile # ========== Configuration ========== BPM = 128 PPQ = 960 PITCHBEND_SCALE = 15 # semitones for full pitchwheel # Chord progression: Am | F | G | E (vi-IV-V-I) CHORDS = [ {"root": 57, "name": "Am"}, # A3 {"root": 53, "name": "F"}, # F3 {"root": 55, "name": "G"}, # G3 {"root": 52, "name": "E"}, # E3 ] STRUCTURE = [ ("intro", 4), ("verse", 8), ("chorus", 8), ("verse2", 8), ("chorus2", 4), ("outro", 8), ] # ========== Helper functions ========== def make_arpeggios(midi, track, channel, chords, arp_type, direction, subdivisions, vol): """Generate ascending/descending arpeggios over chord progression.""" if arp_type == "major": intervals = [0, 4, 7, 12] else: intervals = [0, 3, 7, 12] # minor if direction == "down": intervals = intervals[::-1] duration = 1.0 / subdivisions beat = 0.0 for chord in chords: root = chord["root"] chord_beats = 4 total_notes = int(chord_beats / duration) for i in range(total_notes): note_idx = i % len(intervals) octave = i // len(intervals) pitch = max(0, min(127, root + intervals[note_idx] + octave * 12)) midi.addNote(track, channel, pitch, beat, duration * 0.9, vol) beat += duration def make_drum_track(midi, track, num_bars, bars_with_fill=None): """Standard four-on-the-floor 90s house beat.""" if bars_with_fill is None: bars_with_fill = set() for bar in range(num_bars): base = bar * 4 is_fill = bar in bars_with_fill for beat in range(4): t = base + beat # Kick on every quarter midi.addNote(track, 9, 36, t, 0.2, 110) # Snare on 2 & 4 if beat in (1, 3): midi.addNote(track, 9, 38, t, 0.2, 105) # Closed HH every 8th note midi.addNote(track, 9, 42, t + 0.5, 0.12, 75) # verse starts after 4-bar intro if bar >= 4: midi.addNote(track, 9, 46, t + 0.25, 0.1, 55) midi.addNote(track, 9, 42, t + 0.75, 0.12, 70) else: midi.addNote(track, 9, 42, t + 0.25, 0.12, 65) midi.addNote(track, 9, 42, t + 0.75, 0.12, 65) # Bar-end fill (clap roll) if is_fill: cl = base + 3.75 for k in range(3): midi.addNote(track, 9, 39, cl + k * 0.15, 0.1, 95) midi.addNote(track, 9, 49, cl + 0.6, 0.5, 90) midi.addNote(track, 9, 38, cl + 0.6, 0.3, 120) def make_pad_track(midi, track, channel, num_bars, chord_root_fn, vol=65): """Chord pad holding notes for each bar.""" midi.addControllerEvent(track, channel, 0, 91, 65) # reverb midi.addControllerEvent(track, channel, 0, 93, 75) # chorus midi.addControllerEvent(track, channel, 0, 11, 120) # expression for bar in range(num_bars): root = chord_root_fn(bar) third = root + (4 if bar % 8 in (4, 16) else 3) # shift for variation fifth = root + 7 off_time = (bar + 1) * 4 - 0.3 for note in [root, third, fifth]: midi.addNote(track, channel, note, bar * 4, 3.7, vol) def make_brass_track(midi, track, channel, num_bars): """4-bar brass stabs on chorus beats.""" # Stab pattern: [beat_in_bar, note_offset_in_octave_from_root] stab_beats = [(0, 7), (0, 11), (2, 7), (2, 11)] for bar in range(num_bars): if bar % 4 not in (0, 1, 2): # not intro chord_idx = bar % 4 root = CHORDS[chord_idx]["root"] base = bar * 4 for beat_off, note_off in stab_beats: pitch = root + note_off + (24 if note_off == 7 else 19) midi.addNote(track, channel, pitch, base + beat_off, 0.8, 95) # ========== Build the composition ========== midi = MIDIFile(5, file_format=1, ticks_per_quarternote=PPQ) midi.addTempo(0, 0, BPM) midi.addTrackName(0, 0, "Synth Lead") midi.addTrackName(1, 0, "Synth Bass") midi.addTrackName(2, 0, "Drums") midi.addTrackName(3, 0, "Warm Pads") midi.addTrackName(4, 0, "Brass Stabs") # Setup instruments midi.addProgramChange(0, 0, 0, 80) # Lead 1 Synth — arpeggio midi.addProgramChange(1, 0, 0, 37) # Synth Bass 1 midi.addProgramChange(3, 9, 0, 88) # Pad 4 Choir midi.addProgramChange(4, 9, 0, 59) # Brass Section total_bars = sum(count for _, count in STRUCTURE) bars_with_fill = {11, 19, 27} # end of each verse/chorus # Track 2: Drums make_drum_track(midi, 2, total_bars, bars_with_fill) # Track 3: Pads make_pad_track(midi, 3, 9, total_bars, lambda bar: CHORDS[bar % 4]["root"]) # Track 4: Brass make_brass_track(midi, 4, 9, total_bars) # Track 0: Arpeggio lead (sparse intro, then full) intro_bars = STRUCTURE[0][1] bar_offset = intro_bars # Intro: every other bar, sparse notes (8th notes, only 2 per bar) for bar in range(intro_bars): root = CHORDS[bar % 4]["root"] beat = bar * 4 # Only play on even bars in intro if bar % 2 == 0: for note_off in [0, 3, 7]: note = root + note_off + 12 midi.addNote(0, 0, note, beat + note_off * 0.25, 0.5, 70) bar_offset = beat + 4 # Verse: full arpeggio make_arpeggios(midi, 0, 0, CHORDS * 2, arp_type="minor", direction="up", subdivisions=4, vol=85) bar_offset += 4 * 8 # verse # Chorus: faster arpeggio (32nd notes alternating direction) for bar in range(8): root = CHORDS[(bar_offset // 4) % 4]["root"] bar_beat = bar_offset + bar * 4 for i in range(16): # 16 notes over 4 beats = 32nd notes direction = 1 if (bar_offset // 4 + bar) % 2 == 0 else -1 intervals = [0, 3, 7, 12] if direction == 1 else [12, 7, 3, 0] idx = i % len(intervals) octave = i // len(intervals) pitch = root + intervals[idx] + octave * 12 midi.addNote(0, 0, pitch, bar_beat + i * 0.25, 0.2, 90) bar_offset += 32 # Verse 2: same as verse make_arpeggios(midi, 0, 0, CHORDS * 2, arp_type="minor", direction="up", subdivisions=4, vol=85) bar_offset += 32 # Chorus 2: faster, slightly louder for bar in range(4): root = CHORDS[(bar_offset // 4) % 4]["root"] bar_beat = bar_offset + bar * 4 for i in range(16): direction = 1 if (bar_offset // 4 + bar) % 2 == 0 else -1 intervals = [0, 3, 7, 12] if direction == 1 else [12, 7, 3, 0] idx = i % len(intervals) octave = i // len(intervals) pitch = root + intervals[idx] + octave * 12 midi.addNote(0, 0, pitch, bar_beat + i * 0.25, 0.2, 95) bar_offset += 16 # Outro: fade-out (return to sparse intro-style, quieter) for bar in range(8): root = CHORDS[bar % 4]["root"] base = bar_offset + bar * 4 if bar % 2 == 0: fade_vol = max(30, 85 - bar * 8) # gradually get quieter for note_off in [0, 3, 7]: note = root + note_off + 12 midi.addNote(0, 0, note, base + note_off * 0.25, 0.5, fade_vol) # Track 1: Bass (simplified — just roots on quarter notes) bar_offset = 0 # reset for bass which plays the whole song for bar in range(total_bars): root = CHORDS[bar % 4]["root"] base = bar * 4 # Skip bass in intro if bar >= 4: midi.addNote(1, 0, root, base, 0.35, 100) midi.addNote(1, 0, root, base + 2, 0.35, 100) # ========== Write to disk ========== output_file = "/home/paperclip/projects/gitea/pi-midi-zone/midi_output/midnight_drive.mid" with open(output_file, "wb") as f: midi.writeFile(f) print(f"Written: {output_file}") print(f" 5-track, {total_bars} bars at {BPM} BPM") print(f" Style: 90s House / Dance, A minor")