Files

143 lines
5.2 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
"The U's Late Equaliser" — fast chiptune sports/fighting theme
Style: Game Boy racing / fighting-game stage select. E minor, 152 BPM.
Structure: 2-bar intro, A (8), B (8), A' with harmony (8), tag (2).
The chiptune fingerprints:
- square lead hook in 8th-note runs, nothing sustains
- 16th-note synth bass, octave pops
- off-beat chord stabs (the "skank" that makes 152 BPM feel faster)
- drum fills every 4 bars, crash on the downbeat of every section
"""
import os, sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
from midikit import MidiFile, Track, MPQN, verify, note as N
from songkit import (bar, melody, arp, bass_drive, drums, vibrato, crash,
chord, parse_score, QUALITIES as QUAL, TPQ)
BPM = 152
OUT = os.path.join(os.path.dirname(__file__), "..", "midi",
"03_late_equaliser.mid")
CHORDS = (
[("E", "m"), ("E", "m")] + # intro
[("E", "m"), ("E", "m"), ("C", ""), ("C", ""),
("G", ""), ("G", ""), ("D", ""), ("D", "")] + # A
[("C", ""), ("D", ""), ("E", "m"), ("E", "m"),
("C", ""), ("D", ""), ("E", "m"), ("E", "m")] + # B
[("E", "m"), ("E", "m"), ("C", ""), ("C", ""),
("G", ""), ("G", ""), ("D", ""), ("D", "")] + # A'
[("E", "m"), ("E", "m")] # tag
)
HOOK_A = """
E5-8 G5-8 B5-8 A5-8 G5-8 E5-8 G5-4 | B5-8 A5-8 G5-8 E5-8 F#5-4 G5-4 |
G5-8 A5-8 G5-8 E5-8 C5-2 | E5-8 G5-8 C6-8 B5-8 A5-4 G5-4 |
B5-8 A5-8 B5-8 D6-8 B5-4 G5-4 | D6-8 B5-8 A5-8 G5-8 A5-4 B5-4 |
A5-8 B5-8 A5-8 F#5-8 D5-4 F#5-4 | G5-8 A5-8 B5-8 C6-8 B5-2
"""
HOOK_B = """
C6-8 B5-8 A5-8 G5-8 A5-4 E5-4 | B5-8 A5-8 G5-8 F#5-8 A5-4 D5-4 |
E5-8 G5-8 B5-8 E6-8 D6-4 B5-4 | E6-2 B5-4 G5-4 |
C6-8 D6-8 C6-8 A5-8 G5-4 E5-4 | F#5-8 A5-8 D6-8 C6-8 B5-4 A5-4 |
G5-8 B5-8 E6-8 G6-8 F#6-4 E6-4 | E6-1
"""
INTRO_LICK = "B4-8 E5-8 G5-8 B5-8 A5-8 G5-8 F#5-8 G5-8"
def build():
mf = MidiFile(tpq=TPQ)
mf.time_sig = (4, 2)
mf.key_sig = (-1, True) # E minor
mf.tempos = [MPQN.from_bpm(0, BPM)]
lead = Track("Square lead", channel=0)
lead.program(0, "square lead")
lead.cc(0, 7, 90); lead.cc(0, 10, 52)
harm = Track("Harmony 3rd", channel=1)
harm.program(0, "square lead")
harm.cc(0, 7, 62); harm.cc(0, 10, 78)
stabs = Track("Stabs", channel=2)
stabs.program(0, "electric piano 1")
stabs.cc(0, 7, 62); stabs.cc(0, 10, 24)
bass = Track("Bass", channel=3)
bass.program(0, "synth bass 2")
bass.cc(0, 7, 84); bass.cc(0, 10, 36)
d = Track("Drums", channel=9)
d.cc(0, 7, 88)
def stab_chart(start, count):
"""Off-beat chord stabs: hit on the 'and' of every beat."""
for i in range(count):
root, qual = CHORDS[start + i]
for b in (0.5, 1.5, 2.5, 3.5):
for iv in QUAL[qual]:
stabs.note(bar(start + i, b), N(root + "4") + iv,
TPQ // 2 - 40, 78)
for start, count in ((0, 2), (2, 8), (10, 8), (18, 8), (26, 2)):
stab_chart(start, count)
# intro
bass_drive(bass, 0, CHORDS[0:2], subdivision=2, velocity=90)
melody(lead, 0, INTRO_LICK)
melody(lead, 1, INTRO_LICK)
drums(d, 1, 1, "dos_roll", velocity_scale=0.8)
# A
bass_drive(bass, 2, CHORDS[2:10], subdivision=2)
melody(lead, 2, HOOK_A)
vibrato(lead, bar(2), bar(10), tempo_bpm=BPM, depth=0.4, rate=6.5)
drums(d, 2, 8, "drive")
drums(d, 5, 1, "dos_roll", velocity_scale=0.7)
drums(d, 9, 1, "break")
crash(d, bar(10))
# B
bass_drive(bass, 10, CHORDS[10:18], subdivision=2)
melody(lead, 10, HOOK_B)
vibrato(lead, bar(10), bar(18), tempo_bpm=BPM, depth=0.45, rate=6.5)
drums(d, 10, 8, "drive")
drums(d, 13, 1, "dos_roll", velocity_scale=0.7)
drums(d, 17, 1, "dos_roll")
crash(d, bar(18))
# A' with third-harmony
bass_drive(bass, 18, CHORDS[18:26], subdivision=2)
melody(lead, 18, HOOK_A)
vibrato(lead, bar(18), bar(26), tempo_bpm=BPM, depth=0.5, rate=6.5)
# harmony a diatonic third below, velocity down
for t, p, dur, v in parse_score(HOOK_A):
harm.note(bar(18) + t, p - 4 if p % 12 in (0, 1) else p - 3,
dur, max(40, v - 25))
drums(d, 18, 8, "drive")
drums(d, 21, 1, "dos_roll", velocity_scale=0.7)
drums(d, 25, 1, "dos_roll")
crash(d, bar(26))
# tag: cadence hit
bass_drive(bass, 26, CHORDS[26:28], subdivision=2, velocity=100)
for p in ("E2", "E3", "B3"):
bass.note(bar(27), N(p), TPQ * 4, 100)
for p in ("E4", "G4", "B4", "E5"):
stabs.note(bar(27), N(p), TPQ * 4, 85)
lead.note(bar(27), N("E6"), TPQ * 4, 95)
crash(d, bar(26))
drums(d, 26, 1, "break")
mf.add_track(lead); mf.add_track(harm); mf.add_track(stabs)
mf.add_track(bass); mf.add_track(d)
mf.set_loop(bar(2), bar(26))
return mf
if __name__ == "__main__":
os.makedirs(os.path.dirname(OUT), exist_ok=True)
mf = build()
mf.write(OUT)
dd = verify(OUT, min_sec=30, max_sec=120)
print("OK:", OUT)
print(" duration:", dd["duration_sec"], "s | notes:",
sum(len(t["notes"]) for t in dd["tracks"]),
"| tracks:", len(dd["tracks"]))