f19e3f0633
- 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)
415 lines
14 KiB
Python
415 lines
14 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 : Bridge
|
||
32–47 : 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()
|