#!/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")