Files
pi-midi-zone/examples/compose_neon_dreams.py
T
pi-agent bac1bd5686 feat: add 90s-midi-composer skill and Neon Dreams demo
- SKILL.md: complete reference for creating 90s-style MIDI with Python/mido
- drum_reference.md: full GM drum kit note mapping
- compose_neon_dreams.py: 1.5 min A minor dance track at 128 BPM
- neon_dreams.mid: compiled 6-track MIDI (drums, pads, bass, synth lead, piano)
2026-04-17 17:42:20 +00:00

311 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
NEON DREAMS — 90s-Style Dance Track
Key: A minor, Tempo: 128 BPM, 48 bars (~1:50 duration)
Structure:
03 : Intro (pads only)
411 : Verse (drums, bass, pads, arpeggio)
1219 : Chorus (all instruments, piano melody added)
2031 : Outro (drums simplify, piano + arpeggio only)
Instruments:
Drums (Ch10) — four-on-the-floor dance beat
Synth Lead (Ch1) — sawtooth arpeggios
Pads (Ch2) — warm New Age pad
Bass (Ch3) — synth bass walking roots
Piano (Ch4) — pentatonic melody
"""
from mido import MidiFile, MidiTrack, Message as M, MetaMessage
PPQ = 480 # ticks per quarter note
BPM = 128
TEMPO = 60_000_000 // BPM
def t(x):
"""Beat-time (fractional) to ticks."""
return int(x * PPQ)
# Am | F | G | E7
CHORD = [
[45, 60, 64], # Am
[41, 45, 60], # F
[43, 46, 50], # G
[40, 47, 52], # E7
]
def rel(items):
"""Absolute timestamps → relative Mido messages."""
items.sort(key=lambda x: x[0])
out, prev = [], 0
for at, msg in items:
msg.time = at - prev
out.append(msg)
prev = at
return out
# === DRUM PATTERNS ===
_K, _S, _HH, _HHO = 36, 38, 42, 46 # GM drum codes
def drum_events(total_bars=48):
events = []
def kick(tick):
events.append((tick, M('note_on', note=_K, velocity=110, channel=9)))
events.append((tick + t(0.5), M('note_off', note=_K, velocity=50, channel=9)))
def kick_soft(tick):
events.append((tick, M('note_on', note=_K, velocity=90, channel=9)))
events.append((tick + t(0.35), M('note_off', note=_K, velocity=40, channel=9)))
def snare(tick):
events.append((tick, M('note_on', note=_S, velocity=125, channel=9)))
events.append((tick + t(0.25), M('note_off', note=_S, velocity=60, channel=9)))
def hh(v=70):
events.append((v // 10 * 10, M('note_on', note=_HH, velocity=v, channel=9))) # hack, let me redo
events.append((v // 10 * 10, M('note_off', note=_HH, velocity=30, channel=9)))
# Actually, let me do this differently:
# I'll add events directly with tick offsets
for bar in range(total_bars):
base = bar * t(4.0)
# Determine bar type based on section
if bar < 4:
# Intro: no drums
continue
elif bar < 40:
# Normal dance beat (standard)
# Beat 1: Kick
kick(base)
events.append((base + t(0.5), M('note_on', note=_HH, velocity=65, channel=9)))
events.append((base + t(0.5), M('note_off', note=_HH, velocity=35, channel=9)))
# Beat 2: Kick + Snare + HH
kick(base + t(1.0))
snare(base + t(1.0))
events.append((base + t(1.5), M('note_on', note=_HH, velocity=60, channel=9)))
events.append((base + t(1.5), M('note_off', note=_HH, velocity=35, channel=9)))
# Beat 3: Kick + HH
kick(base + t(2.0))
events.append((base + t(2.5), M('note_on', note=_HH, velocity=60, channel=9)))
events.append((base + t(2.5), M('note_off', note=_HH, velocity=35, channel=9)))
# Beat 4: Kick + Snare + HH + Open HH
kick(base + t(3.0))
snare(base + t(3.0))
events.append((base + t(3.3), M('note_on', note=_HH, velocity=80, channel=9)))
events.append((base + t(3.3), M('note_off', note=_HH, velocity=35, channel=9)))
events.append((base + t(3.3), M('note_on', note=_HHO, velocity=75, channel=9)))
events.append((base + t(3.9), M('note_off', note=_HHO, velocity=35, channel=9)))
# HH fill at end of bar
events.append((base + t(3.8), M('note_on', note=_HH, velocity=55, channel=9)))
events.append((base + t(3.8), M('note_off', note=_HH, velocity=35, channel=9)))
# Extra kick at bar 6 (dance build-up)
if bar % 8 == 6 and bar < 40:
kick(base + t(2.5))
else:
# Outro: simplified, softer drums
events.append((base + t(0), M('note_on', note=_K, velocity=85, channel=9)))
events.append((base + t(0.5), M('note_off', note=_K, velocity=40, channel=9)))
events.append((base + t(1.0), M('note_on', note=_K, velocity=85, channel=9)))
events.append((base + t(1.5), M('note_off', note=_K, velocity=40, channel=9)))
events.append((base + t(2.0), M('note_on', note=_K, velocity=85, channel=9)))
events.append((base + t(2.5), M('note_off', note=_K, velocity=40, channel=9)))
events.append((base + t(3.0), M('note_on', note=_K, velocity=85, channel=9)))
events.append((base + t(3.5), M('note_off', note=_K, velocity=40, channel=9)))
events.append((base + t(3.8), M('note_on', note=_HH, velocity=50, channel=9)))
events.append((base + t(3.8), M('note_off', note=_HH, velocity=30, channel=9)))
return rel(events)
# === PADS ===
def pad_events(total_bars=48):
events = [
(0, M('program_change', program=88, channel=2)),
(0, M('control_change', control=11, value=127, channel=2)),
(0, M('control_change', control=91, value=45, channel=2)),
(0, M('control_change', control=93, value=55, channel=2)),
]
for bar in range(total_bars):
chord = CHORD[bar % 4]
on_tick = bar * t(4.0)
off_tick = on_tick + t(3.7)
# Crossfade: next chord starts 0.3 beats before this one ends
for note in chord:
events.append((on_tick, M('note_on', note=note, velocity=55, channel=2)))
events.append((off_tick, M('note_off', note=note, velocity=25, channel=2)))
return rel(events)
# === BASS ===
def bass_events(total_bars=48):
events = [
(0, M('program_change', program=37, channel=3)),
]
for bar in range(total_bars):
root, third, fifth = CHORD[bar % 4]
base = bar * t(4.0)
# Section-aware patterns
if bar < 4:
# Intro: no bass
continue
elif bar < 16 or bar % 4 == 0:
notes = [root, root + 9, root + 16, (root + 9)] # pentatonic walk
elif bar % 4 == 2:
notes = [root, root + 2, root + 5, root + 7] # chromatic climb
else:
notes = [root, root + 4, root + 7, root + 12] # triad arpeggio
for i, note in enumerate(notes):
on = base + t(1.0 * i)
off = base + t(1.0 + 0.01 * i)
events.append((on, M('note_on', note=note, velocity=92, channel=3)))
events.append((off, M('note_off', note=note, velocity=30, channel=3)))
return rel(events)
# === SYNTH LEAD ARPEGGIOS ===
def lead_events(total_bars=48):
events = [
(0, M('program_change', program=80, channel=1)),
(0, M('control_change', control=11, value=100, channel=1)),
(0, M('control_change', control=91, value=80, channel=1)),
(0, M('control_change', control=93, value=70, channel=1)),
(0, M('control_change', control=5, value=60, channel=1)), # filter cutoff
]
for bar in range(total_bars):
root, third, fifth = CHORD[bar % 4]
base = bar * t(4.0)
if bar < 4:
# Intro: sparse arpeggio (every 2nd bar)
if bar % 2 == 1:
continue
arp = [root + 12, root + 19, root + 24]
for i, note in enumerate(arp):
on = base + t(1.0 * i)
off = base + t(3.0 * i) + t(2.9)
events.append((on, M('note_on', note=note, velocity=55, channel=1)))
events.append((off, M('note_off', note=note, velocity=30, channel=1)))
continue
# Octave-shifted arpeggio
u3 = third + 12
u5 = fifth + 12
ur = root + 12
if bar < 40:
# Normal: 8 notes per bar (ascending-descending octaves)
arp = [ur, u3, u5, ur, u5, u3, ur, fifth]
else:
# Outro: sparse
arp = [ur, fifth, ur, u3]
for i, note in enumerate(arp):
on = base + t(0.25 * i)
off = base + t(0.25 * i) + t(0.2)
events.append((on, M('note_on', note=note, velocity=75, channel=1)))
events.append((off, M('note_off', note=note, velocity=40, channel=1)))
return rel(events)
# === PIANO MELODY ===
def piano_events(total_bars=48):
events = [
(0, M('program_change', program=1, channel=4)),
]
shapes = [
lambda p: [p[2], p[4], p[0], p[2]],
lambda p: [p[3], p[0], p[1], p[3]],
lambda p: [p[4], p[2], p[1], p[4]],
lambda p: [p[0], p[2], p[3], p[0]],
]
for bar in range(total_bars):
root = CHORD[bar % 4][0]
base = bar * t(4.0)
if bar < 12:
# Chorus starts at bar 12
continue
elif bar >= 47:
break
pent = [root + i for i in [0, 3, 5, 7, 10]]
mel = shapes[bar % 4](pent)
for i, note in enumerate(mel):
on = base + t(1.0 * i)
off = base + t(1.0 * i) + t(0.8)
events.append((on, M('note_on', note=note, velocity=85, channel=4)))
events.append((off, M('note_off', note=note, velocity=30, channel=4)))
return rel(events)
# === MAIN ===
def main():
total_bars = 48
mid = MidiFile(ticks_per_beat=PPQ)
# Track 0: Tempo
tempo_trk = MidiTrack()
tempo_trk.append(MetaMessage('set_tempo', tempo=TEMPO, time=0))
tempo_trk.append(MetaMessage('track_name', 'Tempo', time=0))
mid.tracks.append(tempo_trk)
# Track 1: Drums
drums_trk = MidiTrack()
drums_trk.append(MetaMessage('set_tempo', tempo=TEMPO, time=0))
drums_trk.extend(drum_events(total_bars))
mid.tracks.append(drums_trk)
# Track 2: Pads
pads_trk = MidiTrack()
pads_trk.extend(pad_events(total_bars))
mid.tracks.append(pads_trk)
# Track 3: Bass
bass_trk = MidiTrack()
bass_trk.extend(bass_events(total_bars))
mid.tracks.append(bass_trk)
# Track 4: Synth Lead
lead_trk = MidiTrack()
lead_trk.extend(lead_events(total_bars))
mid.tracks.append(lead_trk)
# Track 5: Piano
piano_trk = MidiTrack()
piano_trk.extend(piano_events(total_bars))
mid.tracks.append(piano_trk)
outfile = 'neon_dreams.mid'
mid.save(outfile)
total_ticks = sum(m.time for trk in mid.tracks for m in trk)
print(f"Saved {outfile}{len(mid.tracks)} tracks")
for i, trk in enumerate(mid.tracks):
tt = sum(m.time for m in trk)
name = trk.name if hasattr(trk, 'name') and trk.name else f'Track {i}'
print(f" Track {i} ({name}): {tt} ticks = {tt/480/4:.0f} bars = {tt/480/128*60/60:.1f}s")
if __name__ == '__main__':
main()