bc8df7f33b
- 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.
201 lines
6.7 KiB
Python
201 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Starlit Conversation — 90s Slow Jam / Love Song
|
|
============================================
|
|
Romantic, emotional ballad with Rhodes electric piano,
|
|
warm pads, gentle strings, and soft drum groove. 88 BPM.
|
|
|
|
Structure: 26 bars (~1.8 min at 88 BPM)
|
|
Intro (2 bars) — Rhodes soft chords only
|
|
Verse (8 bars) — Rhodes melody + pads
|
|
Chorus (8 bars) — Rhodes + strings + soft drums
|
|
Verse 2 (8 bars) — Rhodes + pads + drums
|
|
Chorus 2 (4 bars) — Rhodes + strings + drums
|
|
Outro (6 bars) — Rhodes fade only
|
|
|
|
Instruments:
|
|
T0: Rhodes (GM 16) — electric piano chords + melody
|
|
T1: Pads (GM 88) — warm choral backdrop
|
|
T2: Strings (GM 49) — sustained chord accents
|
|
T3: Drums (Ch 10) — soft 90s slow-jam groove
|
|
"""
|
|
|
|
from midiutil import MIDIFile
|
|
|
|
# ========== Configuration =============
|
|
BPM = 88
|
|
PPQ = 960
|
|
|
|
# Chord progression: Bb | G | Am | F
|
|
CHORDS = [
|
|
{"root": 58, "notes": [58, 62, 66]}, # Bb3: Bb3, D4, F4
|
|
{"root": 55, "notes": [55, 59, 63]}, # G3: G3, Bb3, D4
|
|
{"root": 57, "notes": [57, 61, 65]}, # Am3: A3, C4, Em4
|
|
{"root": 53, "notes": [53, 57, 61]}, # F3: F3, Am3, C4
|
|
]
|
|
|
|
STRUCTURE = [
|
|
("intro", 2),
|
|
("verse", 8),
|
|
("chorus", 8),
|
|
("verse2", 8),
|
|
("chorus2", 4),
|
|
("outro", 6),
|
|
]
|
|
total_bars = sum(c for _, c in STRUCTURE)
|
|
|
|
# ========== Helper functions =============
|
|
def pad_sustain(midi, track, channel, num_bars, chord_root_fn, vol=55):
|
|
"""Add sustained warm pad chords. Each note gets ONE note_on/note_off."""
|
|
midi.addControllerEvent(track, channel, 0, 91, 70) # reverb
|
|
midi.addControllerEvent(track, channel, 0, 93, 80) # chorus
|
|
midi.addControllerEvent(track, channel, 0, 1, 40) # modulation
|
|
midi.addControllerEvent(track, channel, 0, 11, 125) # expression
|
|
|
|
for bar in range(num_bars):
|
|
root = chord_root_fn(bar)
|
|
beat = bar * 4.0
|
|
# Three-note voicing, one note_on each
|
|
for note in [root + 7, root + 12, root + 19]:
|
|
midi.addNote(track, channel, note, beat, 3.6, vol)
|
|
|
|
|
|
def slow_jam_drums(midi, track, num_bars):
|
|
"""
|
|
Soft drum pattern — no four-on-the-floor:
|
|
Kick on 1. Ghost kick on 3.
|
|
Snare/CLAP on 2, 3, 4.
|
|
HH on every 8th note.
|
|
"""
|
|
for bar in range(num_bars):
|
|
b = bar * 4.0
|
|
|
|
# Kick
|
|
midi.addNote(track, 9, 36, b + 0.0, 0.45, 85) # beat 1
|
|
midi.addNote(track, 9, 36, b + 2.5, 0.25, 60) # ghost on 3
|
|
|
|
# Snare on 2, 3, 4
|
|
for bp in [2.0, 3.0, 3.5]:
|
|
midi.addNote(track, 9, 38, b + bp, 0.35, 65)
|
|
|
|
# Closed HH on every half-beat
|
|
for bp in range(8):
|
|
midi.addNote(track, 9, 42, b + bp * 0.5, 0.25, 45)
|
|
|
|
# Open HH shimmer at end of chorus bars
|
|
if bar % 8 in (8, 20):
|
|
midi.addNote(track, 9, 46, b + 3.8, 0.3, 40)
|
|
|
|
|
|
def rhodes_melody(midi, track, channel, bars_offset, num_bars,
|
|
chords, pattern):
|
|
"""
|
|
Rhodes electric piano melody.
|
|
pattern: "intro", "verse", "chorus", "outro"
|
|
bars_offset is the starting bar index.
|
|
"""
|
|
for i in range(num_bars):
|
|
bar = bars_offset + i
|
|
chord = chords[bar % len(chords)]
|
|
root = chord["notes"][0]
|
|
base = bar * 4.0
|
|
|
|
if pattern == "intro":
|
|
# Soft sustained chords — one note on per bar
|
|
notes = [chord["notes"][(j * 3) % len(chord["notes"])] for j in range(2)]
|
|
for j, n in enumerate(notes):
|
|
midi.addNote(track, channel, n + 12, base + j * 1.5, 1.3, 70)
|
|
|
|
elif pattern == "verse":
|
|
# 3-note phrases leaving space
|
|
phrases = [
|
|
(0.0, root + 7, 1.5), # 5th
|
|
(2.0, root + 12, 1.0), # root + octave
|
|
(3.5, root + 16 + (i % 2), 1.0), # variation
|
|
]
|
|
for bp, pitch, dur in phrases:
|
|
midi.addNote(track, channel, pitch, base + bp, dur, 78)
|
|
|
|
elif pattern == "chorus":
|
|
# Flowing 1/8-note arpeggio phrases
|
|
offsets = [0, 3, 7, 12, 7, 3, 0, 4] if i % 2 == 0 else [0, 4, 7, 11, 7, 4, 0, -3]
|
|
for j, off in enumerate(offsets):
|
|
pitch = root + off + 12
|
|
midi.addNote(track, channel, pitch, base + j * 0.5, 0.35, 82)
|
|
|
|
elif pattern == "outro":
|
|
# Descending resolution, get quieter
|
|
notes_seq = [root + 24, root + 19, root + 16, root + 12, root + 7, root + 4]
|
|
vol = max(40, 80 - i * 8)
|
|
for j, note in enumerate(notes_seq):
|
|
midi.addNote(track, channel, note, base + j * 0.85, 0.6, vol)
|
|
|
|
|
|
def strings_accent(midi, track, channel, bars_offset, num_bars,
|
|
chords, pattern="soft"):
|
|
"""
|
|
Add string chord accents during chorus sections.
|
|
Only played on first half of each chorus bar.
|
|
"""
|
|
for i in range(num_bars):
|
|
bar = bars_offset + i
|
|
chord = chords[bar % len(chords)]
|
|
root = chord["notes"][0]
|
|
base = bar * 4.0
|
|
vol = 55 if pattern == "soft" else 40
|
|
|
|
# Add single string chord (one note) to avoid overlap
|
|
midi.addNote(track, channel, root + 20, base, 2.0, vol)
|
|
|
|
|
|
# ========== Build the composition =============
|
|
midi = MIDIFile(4, file_format=1, ticks_per_quarternote=PPQ)
|
|
midi.addTempo(0, 0, BPM)
|
|
midi.addTrackName(0, 0, "Rhodes Electric")
|
|
midi.addTrackName(1, 0, "Choir Pads")
|
|
midi.addTrackName(2, 0, "Strings")
|
|
midi.addTrackName(3, 0, "Soft Drums")
|
|
|
|
midi.addProgramChange(0, 0, 0, 16) # Rhodes
|
|
midi.addProgramChange(1, 0, 0, 88) # Choir Pad
|
|
midi.addProgramChange(2, 0, 0, 49) # Strings
|
|
|
|
# Track 3: Drums (enters at bar 2, verse 1) — but plays all to keep it simple
|
|
slow_jam_drums(midi, 3, total_bars)
|
|
|
|
# Track 1: Pads (full song)
|
|
pad_sustain(midi, 1, 0, total_bars,
|
|
lambda bar: CHORDS[bar % 4]["root"])
|
|
|
|
# --- Track 0: Rhodes piano sections ---
|
|
# Intro (bar 0-1): soft sustained
|
|
rhodes_melody(midi, 0, 0, 0, 2, CHORDS, "intro")
|
|
|
|
# Verse (bar 2-9): verse pattern
|
|
rhodes_melody(midi, 0, 0, 2, 8, CHORDS, "verse")
|
|
|
|
# Chorus (bar 10-17): flowing pattern
|
|
rhodes_melody(midi, 0, 0, 10, 8, CHORDS, "chorus")
|
|
|
|
# Verse 2 (bar 18-25): verse pattern
|
|
rhodes_melody(midi, 0, 0, 18, 8, CHORDS, "verse")
|
|
|
|
# Chorus 2 (bar 26-29): flowing pattern
|
|
rhodes_melody(midi, 0, 0, 26, 4, CHORDS, "chorus")
|
|
|
|
# Outro (bar 30-35): fading descent
|
|
rhodes_melody(midi, 0, 0, 30, 6, CHORDS, "outro")
|
|
|
|
# --- Track 2: Strings (only during chorus bars) ---
|
|
strings_accent(midi, 2, 0, 10, 8, CHORDS, "soft")
|
|
strings_accent(midi, 2, 0, 26, 4, CHORDS, "soft")
|
|
|
|
# ========== Write to disk =============
|
|
output_file = "/home/paperclip/projects/gitea/pi-midi-zone/midi_output/starlit_conversation.mid"
|
|
with open(output_file, "wb") as f:
|
|
midi.writeFile(f)
|
|
|
|
print(f"Written: {output_file}")
|
|
print(f" 4-track, {total_bars} bars at {BPM} BPM")
|
|
print(f" Style: 90s Slow Jam / Love Song")
|