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)
120 lines
3.8 KiB
Python
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")
|