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)
This commit is contained in:
@@ -1,20 +1,46 @@
|
|||||||
# pi-midi-zone
|
# MIDI Util Examples
|
||||||
|
|
||||||
A place for Pi to create music!
|
Working examples created from the [MIDIUtil](https://github.com/MarkCWirt/MIDIUtil) source repo patterns.
|
||||||
|
All examples use MIDIUtil with beats-based (not ticks-based) time, the cleanest API for MIDI composition.
|
||||||
|
|
||||||
## Contents
|
## Setup
|
||||||
|
|
||||||
### Skills
|
```bash
|
||||||
|
pip3 install --break-system-packages MIDIUtil
|
||||||
|
sudo apt-get install -y fluidsynth fluid-soundfont-gm
|
||||||
|
```
|
||||||
|
|
||||||
- **skills/90s-midi-composer/** — Skill for creating authentic 90s-style MIDI music files using Python + mido. Generates General MIDI/SC-55 aesthetic tracks with classic dance beats, synth leads, pads, bass, and piano.
|
## Examples
|
||||||
|
|
||||||
### Examples
|
| # | File | Description |
|
||||||
|
|---|------|------|
|
||||||
- **examples/compose_neon_dreams.py** — Python script that composes a 90s dance track ("Neon Dreams"), 48 bars / 1.5 min / 128 BPM in A minor.
|
| 01 | basic-chord.py | Chord progressions in C major |
|
||||||
- **examples/neon_dreams.mid** — Completed 6-track MIDI file. Uses fluidsynth to listen: `fluidsynth /path/to/FluidR3_GM.sf2 examples/neon_dreams.mid`
|
| 02 | 90s-dance-track.py | Full multi-track 90s dance composition (arpeggio, bass, pads, drums, piano) |
|
||||||
|
| 03 | arpeggiator.py | Automated arpeggiator with ascending/descending patterns |
|
||||||
|
| 04 | single-note.py | Minimal single-note example |
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
1. Install: `pip3 install mido`, `sudo apt install fluidsynth fluid-soundfont-gm`
|
```bash
|
||||||
2. Run: `python3 examples/compose_neon_dreams.py`
|
cd examples/
|
||||||
3. Listen: `fluidsynth /usr/share/sounds/sf2/FluidR3_GM.sf2 neon_dreams.mid`
|
python3 04-single-note.py
|
||||||
|
python3 01-basic-chord.py
|
||||||
|
python3 03-arpeggiator.py
|
||||||
|
python3 02-90s-dance-track.py
|
||||||
|
|
||||||
|
# Play back with fluidsynth
|
||||||
|
fluidsynth -a alsa /usr/share/sounds/sf2/FluidR3_GM.sf2 90s-dance-track.mid
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
All time values are **in beats** (quarter notes):
|
||||||
|
|
||||||
|
| Method | Signature |
|
||||||
|
|--------|-----------|
|
||||||
|
| `addNote` | `(track, channel, pitch, time, duration, volume)` |
|
||||||
|
| `addTempo` | `(track, time, bpm)` |
|
||||||
|
| `addTrackName` | `(track, time, name)` |
|
||||||
|
| `addProgramChange` | `(track, channel, time, program)` |
|
||||||
|
| `addControllerEvent` | `(track, channel, time, controller, value)` |
|
||||||
|
| `writeFile` | `(fileHandle)` |
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
01-basic-chord.py - MIDIUtil basic example
|
||||||
|
Creates a simple chord progression in C major using MIDIUtil.
|
||||||
|
"""
|
||||||
|
from midiutil import MIDIFile
|
||||||
|
|
||||||
|
# === Setup ===
|
||||||
|
degrees = [
|
||||||
|
[60, 64, 67], # C3 (C4, E4, G4)
|
||||||
|
[62, 65, 69], # D3 (D4, F4, A4)
|
||||||
|
[64, 67, 71], # E3 (E4, G4, B4)
|
||||||
|
[65, 69, 72], # F3 (F4, A4, C5)
|
||||||
|
] # Chords: C, Dm, Em, F
|
||||||
|
|
||||||
|
track = 0 # Track index
|
||||||
|
channel = 0 # MIDI channel 1
|
||||||
|
time = 0 # Start at beat 0
|
||||||
|
duration = 1 # Each chord lasts 1 beat (quarter note)
|
||||||
|
bpm = 120
|
||||||
|
volume = 100 # MIDI velocity 0-127
|
||||||
|
|
||||||
|
# === Create MIDI File ===
|
||||||
|
my_midi = MIDIFile(1) # 1 user track + auto tempo track = format 1
|
||||||
|
my_midi.addTempo(track, 0, bpm)
|
||||||
|
my_midi.addTrackName(track, 0, "Basic Chord Progression")
|
||||||
|
|
||||||
|
# === Add Notes ===
|
||||||
|
for chord in degrees:
|
||||||
|
for pitch in chord:
|
||||||
|
my_midi.addNote(track, channel, pitch, time, duration, volume)
|
||||||
|
time += duration # Advance one beat per chord
|
||||||
|
|
||||||
|
# === Write to Disk ===
|
||||||
|
with open("basic-chord.mid", "wb") as output_file:
|
||||||
|
my_midi.writeFile(output_file)
|
||||||
|
|
||||||
|
print("Written: basic-chord.mid")
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
02-90s-dance-track.py - Full 90s dance track style composition
|
||||||
|
Creates a multi-track 90s dance track with:
|
||||||
|
- Track 1: Synthesizer arpeggio lead (GM patch 80)
|
||||||
|
- Track 2: Synth bass (GM patch 37)
|
||||||
|
- Track 3: Pad/choir (GM patch 88)
|
||||||
|
- Track 4: Drums on channel 10
|
||||||
|
- Track 5: Piano melody (GM patch 1)
|
||||||
|
|
||||||
|
Uses MIDIUtil with time in beats (quarter notes).
|
||||||
|
"""
|
||||||
|
from midiutil import MIDIFile
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# Constants / Configuration
|
||||||
|
# ==============================
|
||||||
|
BPM = 128
|
||||||
|
TEMPO_US = int(60000000 / BPM)
|
||||||
|
PPQ = 960 # Ticks per quarter note (MIDIUtil default)
|
||||||
|
CHANNEL_DRUMS = 9 # MIDI channel 10 = channel index 9
|
||||||
|
VOLUME = 100
|
||||||
|
|
||||||
|
# === Chord data: (root, duration_beats, notes) ===
|
||||||
|
# vi-IV-V-I in A minor: Am | F | G | E
|
||||||
|
CHORD_PROGRESSION = [
|
||||||
|
(57, 4), # Am: A3, C4, E4 - 4 beats
|
||||||
|
(53, 4), # F: F3, A3, C4 - 4 beats
|
||||||
|
(55, 4), # G: G3, B3, D4 - 4 beats
|
||||||
|
(52, 4), # E: E3, G#3, B3 - 4 beats
|
||||||
|
]
|
||||||
|
|
||||||
|
# Arpeggio patterns: list of note offsets from chord root
|
||||||
|
ARP_PATTERNS = [
|
||||||
|
[0, 3, 7, 12], # Ascending: root-3rd-5th-octave
|
||||||
|
[12, 7, 3, 0], # Descending: octave-5th-3rd-root
|
||||||
|
]
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# Create MIDI File
|
||||||
|
# ==============================
|
||||||
|
# Format 1: 5 user tracks + 1 tempo track = 6 total tracks
|
||||||
|
midi = MIDIFile(5, file_format=1, ticks_per_quarternote=PPQ)
|
||||||
|
midi.addTempo(0, 0, BPM) # Tempo goes to the implicit tempo track
|
||||||
|
midi.addTrackName(0, 0, "Arpeggio Lead")
|
||||||
|
midi.addTrackName(1, 0, "Synth Bass")
|
||||||
|
midi.addTrackName(2, 0, "Warm Pad")
|
||||||
|
midi.addTrackName(3, 0, "Drums (Ch10)")
|
||||||
|
midi.addTrackName(4, 0, "Piano Melody")
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# Track 0: Arpeggio Lead (GM Patch 80 - Lead 1 Synth)
|
||||||
|
# ==============================
|
||||||
|
track_arp = 0
|
||||||
|
midi.addProgramChange(track_arp, 0, 0, 80)
|
||||||
|
|
||||||
|
current_beat = 0
|
||||||
|
for chord_root, chord_dur in CHORD_PROGRESSION:
|
||||||
|
arp_idx = 0
|
||||||
|
arp_pattern = ARP_PATTERNS[arp_idx % len(ARP_PATTERNS)]
|
||||||
|
arp_idx += 1
|
||||||
|
|
||||||
|
# Each chord tone gets a 1/4 note (1 beat / 4 arp notes = 1/4 beat per note)
|
||||||
|
beat_duration = chord_dur / len(arp_pattern) # 1 beat / 4 = 0.25
|
||||||
|
for i, (note_offset, next_offset) in enumerate(zip(arp_pattern, arp_pattern[1:] + arp_pattern[:0])):
|
||||||
|
note = chord_root + note_offset
|
||||||
|
if arp_idx % 2 == 0: # Descending on even patterns
|
||||||
|
note = chord_root + arp_pattern[-1 - i]
|
||||||
|
midi.addNote(track_arp, 0, note, current_beat + i * beat_duration, beat_duration, 90)
|
||||||
|
current_beat += chord_dur
|
||||||
|
arp_idx += 1
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# Track 1: Synth Bass (GM Patch 37)
|
||||||
|
# ==============================
|
||||||
|
track_bass = 1
|
||||||
|
midi.addProgramChange(track_bass, 0, 0, 37)
|
||||||
|
|
||||||
|
current_beat = 0
|
||||||
|
for chord_root, chord_dur in CHORD_PROGRESSION:
|
||||||
|
# Root notes on beats 1 and 3 (quarter notes)
|
||||||
|
midi.addNote(track_bass, 0, chord_root, current_beat, 0.5, 100)
|
||||||
|
midi.addNote(track_bass, 0, chord_root + 7, current_beat + 2, 0.5, 95)
|
||||||
|
current_beat += chord_dur
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# Track 2: Warm Pad (GM Patch 88)
|
||||||
|
# ==============================
|
||||||
|
track_pad = 2
|
||||||
|
midi.addProgramChange(track_pad, 0, 0, 88)
|
||||||
|
|
||||||
|
# Add reverb and expression to pad
|
||||||
|
midi.addControllerEvent(track_pad, 0, 0, 91, 60) # Reverb CC91 = 60
|
||||||
|
midi.addControllerEvent(track_pad, 0, 0, 93, 70) # Chorus CC93 = 70
|
||||||
|
|
||||||
|
current_beat = 0
|
||||||
|
for chord_root, chord_dur in CHORD_PROGRESSION:
|
||||||
|
# Pad plays the full chord for 3.5 of 4 beats with fade
|
||||||
|
root = chord_root
|
||||||
|
third = chord_root + 4
|
||||||
|
fifth = chord_root + 7
|
||||||
|
# Note On (pad instruments need note_on to sound)
|
||||||
|
for pitch in [root, third, fifth]:
|
||||||
|
midi.addNote(track_pad, 0, pitch, current_beat, chord_dur - 0.5, 70)
|
||||||
|
current_beat += chord_dur
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# Track 3: Drums (Channel 10 = MIDI channel 9)
|
||||||
|
# Standard 90s dance pattern:
|
||||||
|
# Kick on 1, 2, 3, 4 (kick = note 36)
|
||||||
|
# Snare on 2, 4 (snare = note 38)
|
||||||
|
# Closed HH on every eighth (hh closed = note 42)
|
||||||
|
# Open HH on off-beats (hh open = note 46)
|
||||||
|
# ==============================
|
||||||
|
track_drums = 3
|
||||||
|
|
||||||
|
current_beat = 0
|
||||||
|
total_bars = len(CHORD_PROGRESSION) # One bar = 4 chords
|
||||||
|
for bar in range(total_bars):
|
||||||
|
for beat in range(4):
|
||||||
|
# Kick on every quarter note
|
||||||
|
midi.addNote(track_drums, CHANNEL_DRUMS, 36, current_beat + beat, 0.2, 110)
|
||||||
|
# Snare on beats 2 and 4
|
||||||
|
if beat in [1, 3]:
|
||||||
|
midi.addNote(track_drums, CHANNEL_DRUMS, 38, current_beat + beat, 0.2, 100)
|
||||||
|
# Closed hi-hat on every eighth note
|
||||||
|
midi.addNote(track_drums, CHANNEL_DRUMS, 42, current_beat + beat + 0.5, 0.15, 80)
|
||||||
|
# Open hi-hat on off-beats (between quarters)
|
||||||
|
midi.addNote(track_drums, CHANNEL_DRUMS, 46, current_beat + beat + 0.25, 0.1, 70)
|
||||||
|
midi.addNote(track_drums, CHANNEL_DRUMS, 46, current_beat + beat + 0.75, 0.1, 70)
|
||||||
|
|
||||||
|
current_beat += 4 # One bar = 4 beats
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# Track 4: Piano Melody (GM Patch 1)
|
||||||
|
# Simple pentatonic melody in A minor
|
||||||
|
# ==============================
|
||||||
|
track_piano = 4
|
||||||
|
midi.addProgramChange(track_piano, 0, 0, 1)
|
||||||
|
|
||||||
|
# Melody notes: (beat, pitch, duration, velocity)
|
||||||
|
melody_notes = [
|
||||||
|
# Verse melody - A minor pentatonic
|
||||||
|
(0, 69, 0.5, 95), # E4, half beat
|
||||||
|
(0.5, 72, 0.5, 90), # F4
|
||||||
|
(1.0, 69, 0.5, 85), # E4
|
||||||
|
(1.5, 67, 0.5, 90), # D4
|
||||||
|
|
||||||
|
(2.0, 65, 0.5, 95), # C4
|
||||||
|
(2.5, 67, 0.5, 90), # D4
|
||||||
|
(3.0, 69, 1.0, 85), # E4, whole beat
|
||||||
|
|
||||||
|
# Repeat with variation
|
||||||
|
(4.0, 69, 0.5, 95), # E4
|
||||||
|
(4.5, 72, 0.5, 90), # F4
|
||||||
|
(5.0, 74, 0.5, 95), # G4
|
||||||
|
(5.5, 72, 0.5, 90), # F4
|
||||||
|
|
||||||
|
(6.0, 69, 1.0, 85), # E4
|
||||||
|
(7.0, 67, 0.5, 80), # D4
|
||||||
|
(7.5, 65, 1.0, 75), # C4, whole beat
|
||||||
|
]
|
||||||
|
|
||||||
|
for beat, pitch, duration, vol in melody_notes:
|
||||||
|
midi.addNote(track_piano, 0, pitch, beat, duration, vol)
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# Write to Disk
|
||||||
|
# ==============================
|
||||||
|
with open("90s-dance-track.mid", "wb") as output_file:
|
||||||
|
midi.writeFile(output_file)
|
||||||
|
|
||||||
|
print("Written: 90s-dance-track.mid")
|
||||||
|
print(f" Tracks: {len(CHORD_PROGRESSION)} chord bars at {BPM} BPM")
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#!/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")
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
04-single-note.py - Single note example (from MIDIUtil docs, adapted)
|
||||||
|
Creates a minimal single-track MIDI file with one note.
|
||||||
|
Reference: examples/single-note-example.py from MIDIUtil repo
|
||||||
|
"""
|
||||||
|
from midiutil import MIDIFile
|
||||||
|
|
||||||
|
# Create the MIDIFile Object
|
||||||
|
my_midi = MIDIFile(1)
|
||||||
|
|
||||||
|
# Add track name and tempo
|
||||||
|
track = 0
|
||||||
|
time = 0
|
||||||
|
my_midi.addTrackName(track, time, "Sample Track")
|
||||||
|
my_midi.addTempo(track, time, 120)
|
||||||
|
|
||||||
|
# Add a single note
|
||||||
|
channel = 0 # MIDI channel 1
|
||||||
|
pitch = 60 # MIDI note number (60 = middle C / C4)
|
||||||
|
duration = 1 # Duration in beats (1 = quarter note)
|
||||||
|
volume = 100 # MIDI velocity 0-127
|
||||||
|
|
||||||
|
my_midi.addNote(track, channel, pitch, time, duration, volume)
|
||||||
|
|
||||||
|
# Write to disk
|
||||||
|
with open("single-note.mid", 'wb') as binfile:
|
||||||
|
my_midi.writeFile(binfile)
|
||||||
|
|
||||||
|
print("Written: single-note.mid")
|
||||||
|
print(f" Note: C4(60) on channel {channel}, {duration} beat(s) at 120 BPM")
|
||||||
+317
-213
@@ -7,303 +7,407 @@ Structure:
|
|||||||
0–3 : Intro (pads only)
|
0–3 : Intro (pads only)
|
||||||
4–11 : Verse (drums, bass, pads, arpeggio)
|
4–11 : Verse (drums, bass, pads, arpeggio)
|
||||||
12–19 : Chorus (all instruments, piano melody added)
|
12–19 : Chorus (all instruments, piano melody added)
|
||||||
20–31 : Outro (drums simplify, piano + arpeggio only)
|
20–31 : Bridge
|
||||||
|
32–47 : Final Chorus + Outro
|
||||||
|
|
||||||
Instruments:
|
Instruments:
|
||||||
Drums (Ch10) — four-on-the-floor dance beat
|
Drums (Ch10) — four-on-the-floor dance beat
|
||||||
Synth Lead (Ch1) — sawtooth arpeggios
|
Synth Lead (Ch1) — Lead 3 Calliope, arpeggios
|
||||||
Pads (Ch2) — warm New Age pad
|
Strings (Ch2) — Choir Aahs, holding pads
|
||||||
Bass (Ch3) — synth bass walking roots
|
Bass (Ch3) — Synth Bass 2, walking chords
|
||||||
Piano (Ch4) — pentatonic melody
|
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
|
from mido import MidiFile, MidiTrack, Message as M, MetaMessage
|
||||||
|
|
||||||
PPQ = 480 # ticks per quarter note
|
PPQ = 480 # ticks per quarter note
|
||||||
BPM = 128
|
BPM = 128
|
||||||
TEMPO = 60_000_000 // BPM
|
TEMPO = 60_000_000 // BPM # 468750 µs per beat
|
||||||
|
|
||||||
def t(x):
|
|
||||||
"""Beat-time (fractional) to ticks."""
|
|
||||||
return int(x * PPQ)
|
|
||||||
|
|
||||||
# Am | F | G | E7
|
def tick(f):
|
||||||
CHORD = [
|
"""Convert fractional beat count to integer MIDI tick."""
|
||||||
[45, 60, 64], # Am
|
return int(round(f * PPQ))
|
||||||
[41, 45, 60], # F
|
|
||||||
[43, 46, 50], # G
|
|
||||||
[40, 47, 52], # E7
|
# --- 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 rel(items):
|
|
||||||
"""Absolute timestamps → relative Mido messages."""
|
|
||||||
items.sort(key=lambda x: x[0])
|
|
||||||
out, prev = [], 0
|
|
||||||
for at, msg in items:
|
|
||||||
msg.time = at - prev
|
|
||||||
out.append(msg)
|
|
||||||
prev = at
|
|
||||||
return out
|
|
||||||
|
|
||||||
# === DRUM PATTERNS ===
|
def get_chord(bar):
|
||||||
_K, _S, _HH, _HHO = 36, 38, 42, 46 # GM drum codes
|
"""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']
|
||||||
|
|
||||||
def drum_events(total_bars=48):
|
|
||||||
events = []
|
|
||||||
|
|
||||||
def kick(tick):
|
# --- Core helpers for absolute-tick-based event generation ---
|
||||||
events.append((tick, M('note_on', note=_K, velocity=110, channel=9)))
|
# Each track builder collects (absolute_tick, Message) tuples,
|
||||||
events.append((tick + t(0.5), M('note_off', note=_K, velocity=50, channel=9)))
|
# then rel() converts them to relative-time Messages for Mido.
|
||||||
|
|
||||||
def kick_soft(tick):
|
def ev_on(tick_abs, note, velocity, channel=0):
|
||||||
events.append((tick, M('note_on', note=_K, velocity=90, channel=9)))
|
"""(tick, message) tuple for note_on with velocity > 0."""
|
||||||
events.append((tick + t(0.35), M('note_off', note=_K, velocity=40, channel=9)))
|
return (tick_abs, M('note_on', note=note, velocity=velocity, channel=channel, time=0))
|
||||||
|
|
||||||
def snare(tick):
|
def ev_off(tick_abs, note, velocity, channel=0):
|
||||||
events.append((tick, M('note_on', note=_S, velocity=125, channel=9)))
|
"""(tick, message) tuple for note_off."""
|
||||||
events.append((tick + t(0.25), M('note_off', note=_S, velocity=60, channel=9)))
|
return (tick_abs, M('note_off', note=note, velocity=velocity, channel=channel, time=0))
|
||||||
|
|
||||||
def hh(v=70):
|
def rel(events):
|
||||||
events.append((v // 10 * 10, M('note_on', note=_HH, velocity=v, channel=9))) # hack, let me redo
|
"""Sort events by absolute tick, convert delta times to relative."""
|
||||||
events.append((v // 10 * 10, M('note_off', note=_HH, velocity=30, channel=9)))
|
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
|
||||||
|
|
||||||
# Actually, let me do this differently:
|
|
||||||
# I'll add events directly with tick offsets
|
# ================================================================
|
||||||
|
# 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):
|
for bar in range(total_bars):
|
||||||
base = bar * t(4.0)
|
base = tick(float(bar) * 4) # bar start tick
|
||||||
# Determine bar type based on section
|
|
||||||
|
# Intro: no drums
|
||||||
if bar < 4:
|
if bar < 4:
|
||||||
# Intro: no drums
|
|
||||||
continue
|
continue
|
||||||
elif bar < 40:
|
|
||||||
# Normal dance beat (standard)
|
|
||||||
# Beat 1: Kick
|
|
||||||
kick(base)
|
|
||||||
events.append((base + t(0.5), M('note_on', note=_HH, velocity=65, channel=9)))
|
|
||||||
events.append((base + t(0.5), M('note_off', note=_HH, velocity=35, channel=9)))
|
|
||||||
|
|
||||||
# Beat 2: Kick + Snare + HH
|
if bar < 32:
|
||||||
kick(base + t(1.0))
|
# ---- Main body: four-on-the-floor + fill at end of every 8th bar ----
|
||||||
snare(base + t(1.0))
|
is_fill = (bar - 4) % 8 == 7
|
||||||
events.append((base + t(1.5), M('note_on', note=_HH, velocity=60, channel=9)))
|
# Beat 1: kick + HH
|
||||||
events.append((base + t(1.5), M('note_off', note=_HH, velocity=35, channel=9)))
|
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),
|
||||||
|
]
|
||||||
|
|
||||||
# Beat 3: Kick + HH
|
elif bar < 46:
|
||||||
kick(base + t(2.0))
|
# ---- Final chorus: full beat + accent on beat 3 off ----
|
||||||
events.append((base + t(2.5), M('note_on', note=_HH, velocity=60, channel=9)))
|
b1, b2, b3, b4 = base, base + PPQ, base + tick(2), base + tick(3)
|
||||||
events.append((base + t(2.5), M('note_off', note=_HH, velocity=35, channel=9)))
|
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),
|
||||||
|
]
|
||||||
|
|
||||||
# Beat 4: Kick + Snare + HH + Open HH
|
return rel(evts)
|
||||||
kick(base + t(3.0))
|
|
||||||
snare(base + t(3.0))
|
|
||||||
events.append((base + t(3.3), M('note_on', note=_HH, velocity=80, channel=9)))
|
|
||||||
events.append((base + t(3.3), M('note_off', note=_HH, velocity=35, channel=9)))
|
|
||||||
events.append((base + t(3.3), M('note_on', note=_HHO, velocity=75, channel=9)))
|
|
||||||
events.append((base + t(3.9), M('note_off', note=_HHO, velocity=35, channel=9)))
|
|
||||||
|
|
||||||
# HH fill at end of bar
|
|
||||||
events.append((base + t(3.8), M('note_on', note=_HH, velocity=55, channel=9)))
|
|
||||||
events.append((base + t(3.8), M('note_off', note=_HH, velocity=35, channel=9)))
|
|
||||||
|
|
||||||
# Extra kick at bar 6 (dance build-up)
|
|
||||||
if bar % 8 == 6 and bar < 40:
|
|
||||||
kick(base + t(2.5))
|
|
||||||
else:
|
|
||||||
# Outro: simplified, softer drums
|
|
||||||
events.append((base + t(0), M('note_on', note=_K, velocity=85, channel=9)))
|
|
||||||
events.append((base + t(0.5), M('note_off', note=_K, velocity=40, channel=9)))
|
|
||||||
events.append((base + t(1.0), M('note_on', note=_K, velocity=85, channel=9)))
|
|
||||||
events.append((base + t(1.5), M('note_off', note=_K, velocity=40, channel=9)))
|
|
||||||
events.append((base + t(2.0), M('note_on', note=_K, velocity=85, channel=9)))
|
|
||||||
events.append((base + t(2.5), M('note_off', note=_K, velocity=40, channel=9)))
|
|
||||||
events.append((base + t(3.0), M('note_on', note=_K, velocity=85, channel=9)))
|
|
||||||
events.append((base + t(3.5), M('note_off', note=_K, velocity=40, channel=9)))
|
|
||||||
events.append((base + t(3.8), M('note_on', note=_HH, velocity=50, channel=9)))
|
|
||||||
events.append((base + t(3.8), M('note_off', note=_HH, velocity=30, channel=9)))
|
|
||||||
|
|
||||||
return rel(events)
|
|
||||||
|
|
||||||
|
|
||||||
# === PADS ===
|
# ================================================================
|
||||||
def pad_events(total_bars=48):
|
# TRACK 2: STRINGS (Choir Aahs pads)
|
||||||
events = [
|
# ================================================================
|
||||||
(0, M('program_change', program=88, channel=2)),
|
def strings_track(total_bars):
|
||||||
(0, M('control_change', control=11, value=127, channel=2)),
|
evts = []
|
||||||
(0, M('control_change', control=91, value=45, channel=2)),
|
bt = PPQ * 4
|
||||||
(0, M('control_change', control=93, value=55, channel=2)),
|
|
||||||
|
# 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):
|
for bar in range(total_bars):
|
||||||
chord = CHORD[bar % 4]
|
cname, chord = get_chord(bar)
|
||||||
on_tick = bar * t(4.0)
|
root, third, fifth = chord[1], chord[2], chord[3]
|
||||||
off_tick = on_tick + t(3.7)
|
on = tick(float(bar) * 4)
|
||||||
# Crossfade: next chord starts 0.3 beats before this one ends
|
off = on + bt - tick(0.3) # crossfade 0.3 beat before bar end
|
||||||
for note in chord:
|
# Stagger chord tones by 2 ticks to avoid dur=0 ghost notes
|
||||||
events.append((on_tick, M('note_on', note=note, velocity=55, channel=2)))
|
evts += [
|
||||||
events.append((off_tick, M('note_off', note=note, velocity=25, channel=2)))
|
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(events)
|
return rel(evts)
|
||||||
|
|
||||||
|
|
||||||
# === BASS ===
|
# ================================================================
|
||||||
def bass_events(total_bars=48):
|
# TRACK 3: BASS
|
||||||
events = [
|
# ================================================================
|
||||||
(0, M('program_change', program=37, channel=3)),
|
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):
|
for bar in range(total_bars):
|
||||||
root, third, fifth = CHORD[bar % 4]
|
cname, chord = get_chord(bar)
|
||||||
base = bar * t(4.0)
|
root = chord[0] # bass root
|
||||||
|
on_tick = tick(float(bar) * 4)
|
||||||
|
|
||||||
# Section-aware patterns
|
|
||||||
if bar < 4:
|
if bar < 4:
|
||||||
# Intro: no bass
|
|
||||||
continue
|
continue
|
||||||
elif bar < 16 or bar % 4 == 0:
|
|
||||||
notes = [root, root + 9, root + 16, (root + 9)] # pentatonic walk
|
|
||||||
elif bar % 4 == 2:
|
|
||||||
notes = [root, root + 2, root + 5, root + 7] # chromatic climb
|
|
||||||
else:
|
|
||||||
notes = [root, root + 4, root + 7, root + 12] # triad arpeggio
|
|
||||||
|
|
||||||
for i, note in enumerate(notes):
|
intervals = arp_ascent[cname] if bar % 2 == 0 else arp_desc[cname]
|
||||||
on = base + t(1.0 * i)
|
|
||||||
off = base + t(1.0 + 0.01 * i)
|
|
||||||
events.append((on, M('note_on', note=note, velocity=92, channel=3)))
|
|
||||||
events.append((off, M('note_off', note=note, velocity=30, channel=3)))
|
|
||||||
|
|
||||||
return rel(events)
|
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)
|
||||||
|
|
||||||
|
|
||||||
# === SYNTH LEAD ARPEGGIOS ===
|
# ================================================================
|
||||||
def lead_events(total_bars=48):
|
# TRACK 4: LEAD ARPEGGIOS
|
||||||
events = [
|
# ================================================================
|
||||||
(0, M('program_change', program=80, channel=1)),
|
def lead_track(total_bars):
|
||||||
(0, M('control_change', control=11, value=100, channel=1)),
|
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=91, value=80, channel=1)),
|
||||||
(0, M('control_change', control=93, value=70, channel=1)),
|
(0, M('control_change', control=93, value=70, channel=1)),
|
||||||
(0, M('control_change', control=5, value=60, channel=1)), # filter cutoff
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for bar in range(total_bars):
|
for bar in range(total_bars):
|
||||||
root, third, fifth = CHORD[bar % 4]
|
cname, chord = get_chord(bar)
|
||||||
base = bar * t(4.0)
|
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:
|
if bar < 4:
|
||||||
# Intro: sparse arpeggio (every 2nd bar)
|
# Intro: sparse, every other bar, 2 slow notes
|
||||||
if bar % 2 == 1:
|
if bar % 2 == 1:
|
||||||
continue
|
continue
|
||||||
arp = [root + 12, root + 19, root + 24]
|
for idx, n in enumerate([ur, u3]):
|
||||||
for i, note in enumerate(arp):
|
nt = on_tick + tick(float(idx) * 2)
|
||||||
on = base + t(1.0 * i)
|
evts += [ev_on(nt, n, 50, 1)]
|
||||||
off = base + t(3.0 * i) + t(2.9)
|
evts += [ev_off(nt + tick(1.5), n, 0, 1)]
|
||||||
events.append((on, M('note_on', note=note, velocity=55, channel=1)))
|
|
||||||
events.append((off, M('note_off', note=note, velocity=30, channel=1)))
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Octave-shifted arpeggio
|
if bar >= 32:
|
||||||
u3 = third + 12
|
# Final chorus: denser 10-note 16th-note run
|
||||||
u5 = fifth + 12
|
arp = [ur, u3, u5, ur, u5, u3, ur, root + 7, u2r, ur]
|
||||||
ur = root + 12
|
|
||||||
|
|
||||||
if bar < 40:
|
|
||||||
# Normal: 8 notes per bar (ascending-descending octaves)
|
|
||||||
arp = [ur, u3, u5, ur, u5, u3, ur, fifth]
|
|
||||||
else:
|
else:
|
||||||
# Outro: sparse
|
# Normal: 8-note 16th-note run, ascending + descending
|
||||||
arp = [ur, fifth, ur, u3]
|
arp = [ur, u3, u5, ur, u5, u3, ur, fifth]
|
||||||
|
|
||||||
for i, note in enumerate(arp):
|
for idx, n in enumerate(arp):
|
||||||
on = base + t(0.25 * i)
|
nt = on_tick + tick(0.25 * idx)
|
||||||
off = base + t(0.25 * i) + t(0.2)
|
dur = 105
|
||||||
events.append((on, M('note_on', note=note, velocity=75, channel=1)))
|
evts += [ev_on(nt, n, 78, 1), ev_off(nt + dur, n, 0, 1)]
|
||||||
events.append((off, M('note_off', note=note, velocity=40, channel=1)))
|
|
||||||
|
|
||||||
return rel(events)
|
return rel(evts)
|
||||||
|
|
||||||
|
|
||||||
# === PIANO MELODY ===
|
# ================================================================
|
||||||
def piano_events(total_bars=48):
|
# TRACK 5: PIANO MELODY
|
||||||
events = [
|
# ================================================================
|
||||||
(0, M('program_change', program=1, channel=4)),
|
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 = [
|
shapes = [
|
||||||
lambda p: [p[2], p[4], p[0], p[2]],
|
[0, 2, 4, 2, 4, 2, 0], # ascending arpeggio fragment
|
||||||
lambda p: [p[3], p[0], p[1], p[3]],
|
[4, 2, 0, 2, 4, 5, 4], # descending
|
||||||
lambda p: [p[4], p[2], p[1], p[4]],
|
[0, 2, 1, 4, 2, 1, 0], # stepwise
|
||||||
lambda p: [p[0], p[2], p[3], p[0]],
|
[2, 4, 5, 4, 2, 0, 4], # climbing
|
||||||
]
|
]
|
||||||
|
|
||||||
for bar in range(total_bars):
|
for bar in range(total_bars):
|
||||||
root = CHORD[bar % 4][0]
|
cname, chord = get_chord(bar)
|
||||||
base = bar * t(4.0)
|
root = chord[1] # root (not bass)
|
||||||
|
on_tick = tick(float(bar) * 4)
|
||||||
|
|
||||||
if bar < 12:
|
if bar < 12:
|
||||||
# Chorus starts at bar 12
|
continue # melody starts at bar 12
|
||||||
continue
|
|
||||||
elif bar >= 47:
|
|
||||||
break
|
|
||||||
|
|
||||||
pent = [root + i for i in [0, 3, 5, 7, 10]]
|
# Build pentatonic scale from root (2 octaves)
|
||||||
mel = shapes[bar % 4](pent)
|
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]
|
||||||
|
|
||||||
for i, note in enumerate(mel):
|
# Melody density changes by section
|
||||||
on = base + t(1.0 * i)
|
if bar % 8 in (4, 5):
|
||||||
off = base + t(1.0 * i) + t(0.8)
|
# Chorus: 8th-note phrases, 5 notes
|
||||||
events.append((on, M('note_on', note=note, velocity=85, channel=4)))
|
shape = shapes[bar % 4][:5]
|
||||||
events.append((off, M('note_off', note=note, velocity=30, channel=4)))
|
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)
|
||||||
|
|
||||||
return rel(events)
|
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 ===
|
# ================================================================
|
||||||
|
# MAIN — assemble the complete MIDI file
|
||||||
|
# ================================================================
|
||||||
def main():
|
def main():
|
||||||
total_bars = 48
|
total_bars = 48
|
||||||
mid = MidiFile(ticks_per_beat=PPQ)
|
outfile = 'neon_dreams.mid'
|
||||||
|
|
||||||
# Track 0: Tempo
|
mid = MidiFile(ticks_per_beat=PPQ, type=1)
|
||||||
tempo_trk = MidiTrack()
|
|
||||||
tempo_trk.append(MetaMessage('set_tempo', tempo=TEMPO, time=0))
|
# Track 0: tempo + time signature
|
||||||
tempo_trk.append(MetaMessage('track_name', 'Tempo', time=0))
|
t0 = MidiTrack()
|
||||||
mid.tracks.append(tempo_trk)
|
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
|
# Track 1: Drums
|
||||||
drums_trk = MidiTrack()
|
t1 = MidiTrack()
|
||||||
drums_trk.append(MetaMessage('set_tempo', tempo=TEMPO, time=0))
|
t1.append(MetaMessage('track_name', name='Drums', time=0))
|
||||||
drums_trk.extend(drum_events(total_bars))
|
t1.extend(drums_track(total_bars))
|
||||||
mid.tracks.append(drums_trk)
|
mid.tracks.append(t1)
|
||||||
|
|
||||||
# Track 2: Pads
|
# Track 2: Strings
|
||||||
pads_trk = MidiTrack()
|
t2 = MidiTrack()
|
||||||
pads_trk.extend(pad_events(total_bars))
|
t2.append(MetaMessage('track_name', name='Strings', time=0))
|
||||||
mid.tracks.append(pads_trk)
|
t2.extend(strings_track(total_bars))
|
||||||
|
mid.tracks.append(t2)
|
||||||
|
|
||||||
# Track 3: Bass
|
# Track 3: Bass
|
||||||
bass_trk = MidiTrack()
|
t3 = MidiTrack()
|
||||||
bass_trk.extend(bass_events(total_bars))
|
t3.append(MetaMessage('track_name', name='Bass', time=0))
|
||||||
mid.tracks.append(bass_trk)
|
t3.extend(bass_track(total_bars))
|
||||||
|
mid.tracks.append(t3)
|
||||||
|
|
||||||
# Track 4: Synth Lead
|
# Track 4: Lead
|
||||||
lead_trk = MidiTrack()
|
t4 = MidiTrack()
|
||||||
lead_trk.extend(lead_events(total_bars))
|
t4.append(MetaMessage('track_name', name='Lead', time=0))
|
||||||
mid.tracks.append(lead_trk)
|
t4.extend(lead_track(total_bars))
|
||||||
|
mid.tracks.append(t4)
|
||||||
|
|
||||||
# Track 5: Piano
|
# Track 5: Piano
|
||||||
piano_trk = MidiTrack()
|
t5 = MidiTrack()
|
||||||
piano_trk.extend(piano_events(total_bars))
|
t5.append(MetaMessage('track_name', name='Piano', time=0))
|
||||||
mid.tracks.append(piano_trk)
|
t5.extend(piano_track(total_bars))
|
||||||
|
mid.tracks.append(t5)
|
||||||
|
|
||||||
outfile = 'neon_dreams.mid'
|
|
||||||
mid.save(outfile)
|
mid.save(outfile)
|
||||||
|
print(f"Saved {outfile} — {len(mid.tracks)} tracks, type={mid.type}")
|
||||||
total_ticks = sum(m.time for trk in mid.tracks for m in trk)
|
|
||||||
print(f"Saved {outfile} — {len(mid.tracks)} tracks")
|
|
||||||
for i, trk in enumerate(mid.tracks):
|
for i, trk in enumerate(mid.tracks):
|
||||||
tt = sum(m.time for m in trk)
|
total = sum(m.time for m in trk)
|
||||||
name = trk.name if hasattr(trk, 'name') and trk.name else f'Track {i}'
|
name = getattr(trk, 'name', f'Track {i}')
|
||||||
print(f" Track {i} ({name}): {tt} ticks = {tt/480/4:.0f} bars = {tt/480/128*60/60:.1f}s")
|
print(f" Track {i} '{name}': {total} ticks = {total / PPQ / 4:.0f} bars")
|
||||||
|
print(f" Playback: {mid.length:.1f}s")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user