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)
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
NEON DREAMS — 90s-Style Dance Track
|
||||
Key: A minor, Tempo: 128 BPM, 48 bars (~1:50 duration)
|
||||
|
||||
Structure:
|
||||
0–3 : Intro (pads only)
|
||||
4–11 : Verse (drums, bass, pads, arpeggio)
|
||||
12–19 : Chorus (all instruments, piano melody added)
|
||||
20–31 : 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()
|
||||
Reference in New Issue
Block a user