bac1bd5686
- 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)
311 lines
11 KiB
Python
311 lines
11 KiB
Python
#!/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()
|