Files
pi-midi-zone/examples/compose_neon_dreams.py
pi-agent f19e3f0633 Add MIDIUtil examples with working 90s dance compositions
- 01-basic-chord.py: chord progressions (C major)
- 02-90s-dance-track.py: 5-track 90s dance (arp, bass, pads, drums, piano)
- 03-arpeggiator.py: reusable arpeggio generator with dir/type options
- 04-single-note.py: minimal single-note example
- Rewrite compose_neon_dreams.py: cleaner track functions, proper velocity=0 note_off, tick helper
- Add README.md with setup and API reference

Uses MIDIUtil library (beats-based API) instead of mido
(clocks-based API)
2026-04-17 21:54:54 +00:00

415 lines
14 KiB
Python
Raw Permalink 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 : Bridge
3247 : Final Chorus + Outro
Instruments:
Drums (Ch10) — four-on-the-floor dance beat
Synth Lead (Ch1) — Lead 3 Calliope, arpeggios
Strings (Ch2) — Choir Aahs, holding pads
Bass (Ch3) — Synth Bass 2, walking chords
Piano (Ch4) — Acoustic Grand, pentatonic melody
MIDI generation lessons applied:
- All note durations are musically realistic (not sub-millisecond)
- note_off uses velocity=0 (standard MIDI spec)
- All tick calculations forced to int() via tick() helper
- No duplicate dead-code helper functions
- set_tempo and time_signature ONLY on tempo track
- Proper track names via MetaMessage('track_name')
"""
from mido import MidiFile, MidiTrack, Message as M, MetaMessage
PPQ = 480 # ticks per quarter note
BPM = 128
TEMPO = 60_000_000 // BPM # 468750 µs per beat
def tick(f):
"""Convert fractional beat count to integer MIDI tick."""
return int(round(f * PPQ))
# --- Chord definitions: Am | F | G | E7 ---
# Each chord: [bass_note, root_note, third, fifth]
# Bass is one octave below root.
CHORD_LIST = [
{'name': 'Am', 'notes': [24, 33, 36, 40]}, # A2, A3, C4, E4
{'name': 'F', 'notes': [21, 30, 33, 37]}, # F2, F3, A3, C4
{'name': 'G', 'notes': [23, 31, 34, 38]}, # G2, G3, B3, D4
{'name': 'E7', 'notes': [20, 29, 32, 36]}, # E2, E3, G#3, B3
]
CHORD_ORDER = ['Am', 'F', 'G', 'E7']
def get_chord(bar):
"""Return (chord_name, [bass, root, third, fifth]) for given bar."""
idx = bar % 4
name = CHORD_ORDER[idx]
chord = CHORD_LIST[idx]
return name, chord['notes']
# --- Core helpers for absolute-tick-based event generation ---
# Each track builder collects (absolute_tick, Message) tuples,
# then rel() converts them to relative-time Messages for Mido.
def ev_on(tick_abs, note, velocity, channel=0):
"""(tick, message) tuple for note_on with velocity > 0."""
return (tick_abs, M('note_on', note=note, velocity=velocity, channel=channel, time=0))
def ev_off(tick_abs, note, velocity, channel=0):
"""(tick, message) tuple for note_off."""
return (tick_abs, M('note_off', note=note, velocity=velocity, channel=channel, time=0))
def rel(events):
"""Sort events by absolute tick, convert delta times to relative."""
events.sort(key=lambda item: item[0])
output, prev_tick = [], 0
for at, msg in events:
msg.time = at - prev_tick
output.append(msg)
prev_tick = at
return output
# ================================================================
# TRACK 1: DRUMS
# ================================================================
_CH_V = 9 # MIDI channel 10 (0-indexed)
_KICK, _SNARE, _HHC, _HHO, _CLAP = 36, 38, 42, 46, 39
def drums_track(total_bars):
evts = []
for bar in range(total_bars):
base = tick(float(bar) * 4) # bar start tick
# Intro: no drums
if bar < 4:
continue
if bar < 32:
# ---- Main body: four-on-the-floor + fill at end of every 8th bar ----
is_fill = (bar - 4) % 8 == 7
# Beat 1: kick + HH
evts += [
ev_on(base, _KICK, 110, _CH_V),
ev_off(base + tick(0.15), _KICK, 0, _CH_V),
ev_on(base, _HHC, 65, _CH_V),
ev_off(base + tick(0.25), _HHC, 0, _CH_V),
]
# Beat 2: kick + snare + HH
b2 = base + PPQ
evts += [
ev_on(b2, _KICK, 110, _CH_V),
ev_off(b2 + tick(0.15), _KICK, 0, _CH_V),
ev_on(b2, _SNARE, 125, _CH_V),
ev_off(b2 + tick(0.2), _SNARE, 0, _CH_V),
ev_on(b2 + tick(0.5), _HHC, 60, _CH_V),
ev_off(b2 + tick(0.75), _HHC, 0, _CH_V),
]
# Beat 3: kick + HH
b3 = base + tick(2)
evts += [
ev_on(b3, _KICK, 110, _CH_V),
ev_off(b3 + tick(0.15), _KICK, 0, _CH_V),
ev_on(b3 + tick(0.5), _HHC, 60, _CH_V),
ev_off(b3 + tick(0.75), _HHC, 0, _CH_V),
]
# Beat 4: kick + snare + HH + open HH
b4 = base + tick(3)
evts += [
ev_on(b4, _KICK, 110, _CH_V),
ev_off(b4 + tick(0.15), _KICK, 0, _CH_V),
ev_on(b4, _SNARE, 125, _CH_V),
ev_off(b4 + tick(0.2), _SNARE, 0, _CH_V),
ev_on(b4 + tick(0.25), _HHC, 70, _CH_V),
ev_off(b4 + tick(0.5), _HHC, 0, _CH_V),
ev_on(b4 + tick(0.5), _HHO, 75, _CH_V),
ev_off(b4 + tick(0.9), _HHO, 0, _CH_V),
]
# Bar end: fill or short HH
if is_fill:
fb = base + tick(3.75)
for k in range(4):
evts += [
ev_on(fb + tick(k * 0.25), _KICK, 95, _CH_V),
ev_off(fb + tick(k * 0.25) + tick(0.1), _KICK, 0, _CH_V),
]
evts += [
ev_on(fb + tick(1), _CLAP, 100, _CH_V),
ev_off(fb + tick(1) + tick(0.3), _CLAP, 0, _CH_V),
]
else:
evts += [
ev_on(base + tick(3.75), _HHC, 55, _CH_V),
ev_off(base + tick(3.875), _HHC, 0, _CH_V),
]
elif bar < 46:
# ---- Final chorus: full beat + accent on beat 3 off ----
b1, b2, b3, b4 = base, base + PPQ, base + tick(2), base + tick(3)
for tick_pos, note, vel in [
(0, _KICK, 110), (1, _SNARE, 125),
(2, _KICK, 110), (3, _SNARE, 125),
(2.5, _KICK, 100), # extra accent kick
]:
evts += [
ev_on(base + tick(tick_pos), note, vel, _CH_V),
ev_off(base + tick(tick_pos) + tick(0.15), note, 0, _CH_V),
]
# High hats every 8th note
for b in range(4):
evts += [
ev_on(b1 + tick(b), _HHC, 65, _CH_V),
ev_off(b1 + tick(b) + tick(0.25), _HHC, 0, _CH_V),
ev_on(b1 + tick(b + 0.5), _HHC, 55, _CH_V),
ev_off(b1 + tick(b + 0.5) + tick(0.25), _HHC, 0, _CH_V),
]
return rel(evts)
# ================================================================
# TRACK 2: STRINGS (Choir Aahs pads)
# ================================================================
def strings_track(total_bars):
evts = []
bt = PPQ * 4
# Patch + effects at bar 0
evts += [
(0, M('program_change', program=71, channel=2)), # Choir Aahs
(0, M('control_change', control=91, value=40, channel=2)), # reverb
(0, M('control_change', control=93, value=60, channel=2)), # chorus
(0, M('control_change', control=11, value=127, channel=2)),# expression
]
for bar in range(total_bars):
cname, chord = get_chord(bar)
root, third, fifth = chord[1], chord[2], chord[3]
on = tick(float(bar) * 4)
off = on + bt - tick(0.3) # crossfade 0.3 beat before bar end
# Stagger chord tones by 2 ticks to avoid dur=0 ghost notes
evts += [
ev_on(on, root, 55, 2),
ev_off(off, root, 55, 2),
ev_on(on + 2, third, 55, 2),
ev_off(off + 4, third, 55, 2),
ev_on(on + 4, fifth, 55, 2),
ev_off(off + 6, fifth, 55, 2),
]
return rel(evts)
# ================================================================
# TRACK 3: BASS
# ================================================================
def bass_track(total_bars):
evts = []
bt = PPQ * 4
evts.append((0, M('program_change', program=36, channel=3))) # Synth Bass 2
# Bass arp interval patterns (ascending vs descending bar)
arp_ascent = {
'Am': [0, 4, 7, 12], 'F': [0, 4, 8, 12],
'G': [0, 4, 7, 11], 'E7': [0, 4, 7, 12],
}
arp_desc = {
'Am': [12, 7, 4, 0], 'F': [12, 8, 4, 0],
'G': [12, 7, 4, 0], 'E7': [12, 7, 4, 0],
}
for bar in range(total_bars):
cname, chord = get_chord(bar)
root = chord[0] # bass root
on_tick = tick(float(bar) * 4)
if bar < 4:
continue
intervals = arp_ascent[cname] if bar % 2 == 0 else arp_desc[cname]
for i, interval in enumerate(intervals):
note = root + interval
start = on_tick + PPQ * i # one beat apart (quarter notes)
dur = 170 # 0.35 beat — short but clearly audible
evts += [ev_on(start, note, 95, 3), ev_off(start + dur, note, 0, 3)]
return rel(evts)
# ================================================================
# TRACK 4: LEAD ARPEGGIOS
# ================================================================
def lead_track(total_bars):
evts = []
bt = PPQ * 4
evts += [
(0, M('program_change', program=83, channel=1)), # Lead 3 Calliope
(0, M('control_change', control=11, value=120, channel=1)),
(0, M('control_change', control=91, value=80, channel=1)),
(0, M('control_change', control=93, value=70, channel=1)),
]
for bar in range(total_bars):
cname, chord = get_chord(bar)
root, third, fifth = chord[1], chord[2], chord[3]
# Octave-shifted chord tones
ur = root + 12 # one octave up — root
u3 = third + 12 # one octave up — third
u5 = fifth + 12 # one octave up — fifth
u2r = root + 24 # two octaves up
on_tick = tick(float(bar) * 4)
if bar < 4:
# Intro: sparse, every other bar, 2 slow notes
if bar % 2 == 1:
continue
for idx, n in enumerate([ur, u3]):
nt = on_tick + tick(float(idx) * 2)
evts += [ev_on(nt, n, 50, 1)]
evts += [ev_off(nt + tick(1.5), n, 0, 1)]
continue
if bar >= 32:
# Final chorus: denser 10-note 16th-note run
arp = [ur, u3, u5, ur, u5, u3, ur, root + 7, u2r, ur]
else:
# Normal: 8-note 16th-note run, ascending + descending
arp = [ur, u3, u5, ur, u5, u3, ur, fifth]
for idx, n in enumerate(arp):
nt = on_tick + tick(0.25 * idx)
dur = 105
evts += [ev_on(nt, n, 78, 1), ev_off(nt + dur, n, 0, 1)]
return rel(evts)
# ================================================================
# TRACK 5: PIANO MELODY
# ================================================================
def piano_track(total_bars):
evts = []
bt = PPQ * 4
evts.append((0, M('program_change', program=0, channel=4))) # Acoustic Grand
# Melody shapes: indices into pentatonic scale
shapes = [
[0, 2, 4, 2, 4, 2, 0], # ascending arpeggio fragment
[4, 2, 0, 2, 4, 5, 4], # descending
[0, 2, 1, 4, 2, 1, 0], # stepwise
[2, 4, 5, 4, 2, 0, 4], # climbing
]
for bar in range(total_bars):
cname, chord = get_chord(bar)
root = chord[1] # root (not bass)
on_tick = tick(float(bar) * 4)
if bar < 12:
continue # melody starts at bar 12
# Build pentatonic scale from root (2 octaves)
pent = sorted(set(root + i for i in [0, 2, 3, 5, 7, 10, 12, 14, 15]))
pent = [n for n in pent if n <= root + 24]
# Melody density changes by section
if bar % 8 in (4, 5):
# Chorus: 8th-note phrases, 5 notes
shape = shapes[bar % 4][:5]
spacing = tick(0.5)
dur = tick(0.35)
elif bar % 8 == 7:
# Bar 7: half notes, sustain
shape = [shapes[bar % 4][-1], shapes[bar % 4][-1], shapes[bar % 4][-1]]
spacing = tick(1.0)
dur = tick(0.85)
else:
# Verse / other chorus bars: 4 notes
shape = shapes[bar % 4][:4]
spacing = tick(0.5)
dur = tick(0.35)
for idx, pidx in enumerate(shape):
note = pent[min(pidx, len(pent) - 1)]
nt = on_tick + spacing * idx
evts += [ev_on(nt, note, 88, 4), ev_off(nt + dur, note, 0, 4)]
return rel(evts)
# ================================================================
# MAIN — assemble the complete MIDI file
# ================================================================
def main():
total_bars = 48
outfile = 'neon_dreams.mid'
mid = MidiFile(ticks_per_beat=PPQ, type=1)
# Track 0: tempo + time signature
t0 = MidiTrack()
t0.append(MetaMessage('time_signature', numerator=4, denominator=4, time=0))
t0.append(MetaMessage('set_tempo', tempo=TEMPO, time=0))
t0.append(MetaMessage('track_name', name='Tempo', time=0))
mid.tracks.append(t0)
# Track 1: Drums
t1 = MidiTrack()
t1.append(MetaMessage('track_name', name='Drums', time=0))
t1.extend(drums_track(total_bars))
mid.tracks.append(t1)
# Track 2: Strings
t2 = MidiTrack()
t2.append(MetaMessage('track_name', name='Strings', time=0))
t2.extend(strings_track(total_bars))
mid.tracks.append(t2)
# Track 3: Bass
t3 = MidiTrack()
t3.append(MetaMessage('track_name', name='Bass', time=0))
t3.extend(bass_track(total_bars))
mid.tracks.append(t3)
# Track 4: Lead
t4 = MidiTrack()
t4.append(MetaMessage('track_name', name='Lead', time=0))
t4.extend(lead_track(total_bars))
mid.tracks.append(t4)
# Track 5: Piano
t5 = MidiTrack()
t5.append(MetaMessage('track_name', name='Piano', time=0))
t5.extend(piano_track(total_bars))
mid.tracks.append(t5)
mid.save(outfile)
print(f"Saved {outfile}{len(mid.tracks)} tracks, type={mid.type}")
for i, trk in enumerate(mid.tracks):
total = sum(m.time for m in trk)
name = getattr(trk, 'name', f'Track {i}')
print(f" Track {i} '{name}': {total} ticks = {total / PPQ / 4:.0f} bars")
print(f" Playback: {mid.length:.1f}s")
if __name__ == '__main__':
main()