Files
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

120 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""
03-arpeggiator.py - MIDIUtil arpeggiator
Generates automatic ascending/descending arpeggios from chord sets.
Shows clean patterns using MIDIUtil's beat-based time API.
"""
from midiutil import MIDIFile
# ==============================
# Configuration
# ==============================
BPM = 132
PPQ = 960
# Chord definitions: (root MIDI note, number of beats)
# Using A minor progression: Am | F | G | E
CHORDS = [
{"root": 57, "beats": 4}, # Am (A3)
{"root": 53, "beats": 4}, # F (F3)
{"root": 55, "beats": 4}, # G (G3)
{"root": 52, "beats": 4}, # E (E3)
]
# Arpeggio patterns: semitone intervals from chord root
CHORD_TYPES = {
"major": [0, 4, 7, 12], # Major triad + octave
"minor": [0, 3, 7, 12], # Minor triad + octave
"dim": [0, 3, 6, 9], # Diminished
"aug": [0, 4, 8, 12], # Augmented
}
# ==============================
# Helper function
# ==============================
def make_arpeggio(midi, track, channel, chord_list,
arp_type="minor", arp_direction="up",
subdivisions=4, volume=90):
"""
Generate an arpeggio track from a chord list.
Args:
midi: MIDIFile object
track: track index (int)
channel: MIDI channel (int, 0-15)
chord_list: list of {"root": M, "beats": N} with MIDI root notes
arp_type: chord type key for CHORD_TYPES dict ("minor", "major", etc.)
arp_direction: "up" or "down" for arpeggio direction
subdivisions: 16th-note speed (4 = standard 16ths, 8 = 32nds)
volume: note velocity (0-127)
"""
notes = CHORD_TYPES[arp_type].copy()
if arp_direction == "down":
notes = notes[::-1]
note_beats = 1.0 / subdivisions # duration per arpeggio hit
beat = 0.0
for chord in chord_list:
root = chord["root"]
chord_duration = chord["beats"]
total_hits = int(chord_duration / note_beats)
for i in range(total_hits):
note_idx = i % len(notes)
octave = i // len(notes)
pitch = root + notes[note_idx] + (octave * 12)
# Cap MIDI pitch range
pitch = max(0, min(127, pitch))
midi.addNote(track, channel, pitch, beat, note_beats, volume)
beat += note_beats
# ==============================
# Create multi-track file
# ==============================
midi = MIDIFile(3, file_format=1, ticks_per_quarternote=PPQ)
midi.addTempo(0, 0, BPM)
midi.addTrackName(0, 0, "Arpeggio Lead 1 (Minor)")
midi.addTrackName(1, 0, "Arpeggio Lead 2 (Major)")
midi.addTrackName(2, 0, "Bass")
# Track 0: Upward minor arpeggio (GM 80 - Lead 1 Synth)
midi.addProgramChange(0, 0, 0, 80)
make_arpeggio(midi, 0, 0, CHORDS,
arp_type="minor", arp_direction="up",
subdivisions=4, volume=85)
# Track 1: Descending major arpeggio (GM 84 - Lead 3 Synth)
midi.addProgramChange(1, 0, 0, 84)
make_arpeggio(midi, 1, 0, CHORDS,
arp_type="major", arp_direction="down",
subdivisions=8, volume=80)
# Track 2: Bass follows chord roots (quarter notes)
midi.addProgramChange(2, 0, 0, 37) # Synth Bass 1
for chord in CHORDS:
root = chord["root"]
chord_duration = chord["beats"]
# Two quarter-note pulses per beat
for beat_offset in range(0, int(chord_duration * 2)):
time = beat_offset * 0.5
midi.addNote(2, 0, root, time, 0.4, 100)
# Octave pulse for depth
octave = root + 12
if octave <= 127:
midi.addNote(2, 0, octave, time, 0.2, 80)
# ==============================
# Write to Disk
# ==============================
with open("arpeggiator.mid", "wb") as output_file:
midi.writeFile(output_file)
print("Written: arpeggiator.mid")
print(" 3-track arpeggio piece with minor/major contrast")