Files
pi-midi-zone/midi_output/01_midnight_drive.py
T
pi-agent bc8df7f33b Add 3 composed MIDI tracks (midi_output)
- midnight_drive.mid: 90s House/Dance at 128 BPM (75s, 5-track)
  Arpeggiated synth lead, driving bass, pads, brass stabs, dance beat

- starlit_conversation.mid: 90s Slow Jam/Love Song at 88 BPM (99s, 4-track)
  Rhodes electric piano, strings, choir pads, soft drum groove

- tronica.mid: 90s Trance/EBM at 138 BPM (69s, 4-track)
  Multi-section arpeggiated lead, wide pads, trance beat, accent lead

Each .mid has a corresponding .py source showing how to compose it with MIDIUtil.
2026-04-17 22:32:46 +00:00

245 lines
8.3 KiB
Python

#!/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")