40 bars, D minor, 104 BPM. Intro / A(16) / B(8) / A'(8) / outro, loop bars 4-36. Hand-rolled calypso groove (kick 1&3 with the push, rimshot 2&4, eighths on hat+shaker), offbeat guitar skank, walking upright bass with chromatic approaches, GM 114 steel-pan lead.
204 lines
8.2 KiB
Python
204 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
"Moonlit Mangrove Calypso" — minor-key Caribbean adventure music
|
|
in the spirit of a certain scrappy young pirate's theme. D minor, 104 BPM.
|
|
Structure: 4-bar intro, A (16), B (8), A' (8), outro (4). Loop: bars 4-36.
|
|
|
|
The calypso fingerprints:
|
|
- steel-pan lead (GM 114, labelled "calypso drum" in the MT-32 table —
|
|
the patch name alone is a time machine)
|
|
- guitar skank: short chord stabs on the OFFBEATS (the & of every beat)
|
|
- walking upright bass: root, fifth, octave, chromatic approach
|
|
- straight-eighth shaker under kick-on-1-and-3, rimshot on 2 and 4
|
|
- syncopated melody: sixteenth turns, rests on the strong beats,
|
|
and the harmonic-minor C# leading tone doing its 1990 thing
|
|
No relation to any existing pirate, sword, or grog empire is claimed.
|
|
"""
|
|
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, DRUMS
|
|
from songkit import (bar, melody, vibrato, crash, humanize, parse_score,
|
|
QUALITIES, TPQ)
|
|
|
|
BPM = 104
|
|
OUT = os.path.join(os.path.dirname(__file__), "..", "midi",
|
|
"05_moonlit_mangrove_calypso.mid")
|
|
|
|
# Chord chart, one (root, quality) per bar, 40 bars
|
|
CHORDS = (
|
|
[("D", "m"), ("G", "m"), ("Bb", ""), ("A", "")] + # intro
|
|
[("D", "m"), ("D", "m"), ("G", "m"), ("G", "m"), # A (16)
|
|
("Bb", ""), ("Bb", ""), ("A", ""), ("A", ""),
|
|
("D", "m"), ("D", "m"), ("G", "m"), ("A", ""),
|
|
("Bb", ""), ("A", ""), ("D", "m"), ("D", "m")] +
|
|
[("G", "m"), ("G", "m"), ("A", ""), ("A", ""), # B (8)
|
|
("Bb", ""), ("Bb", ""), ("G", "m"), ("A", "")] +
|
|
[("D", "m"), ("G", "m"), ("Bb", ""), ("A", ""), # A' (8)
|
|
("D", "m"), ("G", "m"), ("A", ""), ("D", "m")] +
|
|
[("Bb", ""), ("A", ""), ("D", "m"), ("D", "m")] # outro
|
|
)
|
|
|
|
# --- melodies (bar grids, every bar lints to 4/4) --------------------------
|
|
MELODY_A = """
|
|
r8 D4-16 F4-16 A4-8 C5-8 D5-4 r4 | C5-8 A4-16 C5-16 A4-8 F4-8 A4-4 r8 D5-16 C#5-16 |
|
|
D5-4 r4 A4-8 C5-8 D5-8 F5-8 | E5-8 D5-16 E5-16 D5-8 C#5-8 A4-4 A4-4 |
|
|
F5-4 E5-8 F5-16 E5-16 D5-8 C5-8 A4-4 | Bb4-8 C5-8 D5-4 r8 D5-8 C5-8 A4-8 |
|
|
Bb4-4 A4-8 Bb4-16 A4-16 G4-8 A4-8 C#5-4 | D5-2 C#5-4 A4-4 |
|
|
r8 D4-16 F4-16 A4-8 C5-8 D5-4 r4 | C5-8 A4-16 C5-16 A4-8 F4-8 A4-4 r8 D5-16 C#5-16 |
|
|
F5-8 E5-8 D5-4 C5-8 Bb4-8 A4-4 | G4-8 A4-8 C#5-4 C#5-8 D5-8 E5-4 |
|
|
F5-4 E5-4 D5-8 E5-8 F5-8 A5-8 | G5-8 F5-16 G5-16 F5-8 E5-8 C5-4 C5-4 |
|
|
D5-4 C5-8 D5-8 A4-4 F4-8 A4-8 | D5-2 r4 r4
|
|
"""
|
|
MELODY_B = """
|
|
r4 A5-8 G5-8 F5-4 E5-8 F5-8 | G5-4 F5-8 G5-8 F5-4 D5-4 |
|
|
r8 Bb5-16 A5-16 G5-8 F5-8 E5-4 D5-4 | C#5-8 D5-8 E5-4 r8 F5-8 E5-8 D5-8 |
|
|
F5-8 G5-8 A5-4 r8 A5-8 G5-8 F5-8 | E5-4 F5-8 E5-8 D5-4 C5-8 Bb4-8 |
|
|
A4-8 Bb4-8 C5-4 C5-8 D5-8 E5-4 | D5-2 C#5-4 D5-4
|
|
"""
|
|
MELODY_A2 = """
|
|
r8 D4-16 F4-16 A4-8 C5-8 D5-4 r4 | C5-8 A4-16 C5-16 A4-8 F4-8 A4-4 r8 D5-16 C#5-16 |
|
|
D5-4 r4 A4-8 C5-8 D5-8 F5-8 | E5-8 D5-16 E5-16 D5-8 C#5-8 A4-4 A4-4 |
|
|
F5-4 E5-8 F5-16 E5-16 D5-8 C5-8 A4-4 | Bb4-8 C5-8 D5-4 r8 D5-8 C5-8 A4-8 |
|
|
Bb4-4 A4-8 Bb4-16 A4-16 G4-8 A4-8 C#5-4 | D5-2 C#5-4 A4-4
|
|
"""
|
|
PREVIEW = """
|
|
r8 D4-16-72 F4-16-72 A4-8-72 C5-8-72 D5-4-72 r4 | C5-8-72 A4-16-72 C5-16-72 A4-8-72 F4-8-72 A4-4-72 r8 D5-16-72 C#5-16-72
|
|
"""
|
|
TAG = """
|
|
r4 A4-8 C5-8 D5-2 | r2 E5-4 D5-4 | r4 A4-4 D5-2 | r1
|
|
"""
|
|
|
|
|
|
def skank(track, start_bar, chords, channel=2):
|
|
"""Calypso offbeat chord stabs: short, tight, on the & of every beat."""
|
|
for i, (root, qual) in enumerate(chords):
|
|
for k, beat in enumerate((0.5, 1.5, 2.5, 3.5)):
|
|
vel = humanize(76 if k in (0, 2) else 68, seed=start_bar * 4 + k + i * 17)
|
|
for iv in QUALITIES[qual][:3]:
|
|
track.note(bar(start_bar + i, beat), N(root + "4") + iv,
|
|
TPQ // 2 - 60, vel, channel)
|
|
return track
|
|
|
|
|
|
def walking_bass(track, start_bar, chords, channel=3):
|
|
"""Root, fifth, octave, chromatic approach to the next root."""
|
|
for i, (root, qual) in enumerate(chords):
|
|
nxt = chords[(i + 1) % len(chords)][0] if i + 1 < len(chords) else root
|
|
r = N(root + "2")
|
|
approach = N(nxt + "2") - 1 if i + 1 < len(chords) else r + 7
|
|
for k, p in enumerate((r, r + 7, r + 12, approach)):
|
|
track.note(bar(start_bar + i, k), p, TPQ - 30,
|
|
humanize(88, seed=start_bar * 4 + k), channel)
|
|
return track
|
|
|
|
|
|
def calypso_groove(track, start_bar, bars):
|
|
"""Kick 1 and 3 (+ the & of 3), rimshot 2 and 4, eighths on hat and
|
|
shaker, open hat now and then. This is the merengue-ish calypso bed."""
|
|
for b in range(bars):
|
|
bb = start_bar + b
|
|
track.note(bar(bb, 0), DRUMS["bass1"], TPQ // 4, 104)
|
|
track.note(bar(bb, 2), DRUMS["bass1"], TPQ // 4, 96)
|
|
track.note(bar(bb, 2.5), DRUMS["bass1"], TPQ // 4, 72) # the push
|
|
track.note(bar(bb, 1), DRUMS["stick"], TPQ // 4, 98)
|
|
track.note(bar(bb, 3), DRUMS["stick"], TPQ // 4, 98)
|
|
for k in range(8):
|
|
track.note(bar(bb, k * 0.5), DRUMS["hat"], TPQ // 4,
|
|
60 if k % 2 == 0 else 46)
|
|
track.note(bar(bb, k * 0.5), DRUMS["shaker"], TPQ // 4,
|
|
44 if k % 2 == 0 else 34)
|
|
if b % 4 == 3:
|
|
track.note(bar(bb, 3.5), DRUMS["openhat"], TPQ // 4, 70)
|
|
return track
|
|
|
|
|
|
def calypso_fill(track, bar_no):
|
|
"""Sixteenth roll with a tom tumble down into the next section."""
|
|
for k in range(8):
|
|
track.note(bar(bar_no, 2 + k * 0.25), DRUMS["snare"], TPQ // 8,
|
|
60 + k * 7)
|
|
for k, d in enumerate(("hightom", "midtom", "lotom")):
|
|
track.note(bar(bar_no, 3 + k * 0.333), DRUMS[d], TPQ // 4, 92 - k * 8)
|
|
return track
|
|
|
|
|
|
def build():
|
|
mf = MidiFile(tpq=TPQ)
|
|
mf.time_sig = (4, 2)
|
|
mf.key_sig = (-1, True) # D minor
|
|
mf.tempos = [MPQN.from_bpm(0, BPM)]
|
|
|
|
lead = Track("Steel pan", channel=0)
|
|
lead.program(0, "calypso drum") # GM 114 = steel drums
|
|
lead.cc(0, 7, 100); lead.cc(0, 10, 54)
|
|
sk = Track("Skank guitar", channel=2)
|
|
sk.program(0, "clean electric")
|
|
sk.cc(0, 7, 70); sk.cc(0, 10, 84)
|
|
bass = Track("Walking bass", channel=3)
|
|
bass.program(0, "acoustic bass")
|
|
bass.cc(0, 7, 92); bass.cc(0, 10, 30)
|
|
d = Track("Calypso kit", channel=9)
|
|
d.cc(0, 7, 96)
|
|
|
|
# intro (0-3): groove + skank only, quiet pan preview of the theme in 2-3
|
|
skank(sk, 0, CHORDS[0:4])
|
|
walking_bass(bass, 0, CHORDS[0:4])
|
|
calypso_groove(d, 1, 3)
|
|
melody(lead, 2, PREVIEW)
|
|
crash(d, bar(4), vel=96)
|
|
|
|
# A (4-19)
|
|
skank(sk, 4, CHORDS[4:20])
|
|
walking_bass(bass, 4, CHORDS[4:20])
|
|
calypso_groove(d, 4, 16)
|
|
melody(lead, 4, MELODY_A)
|
|
vibrato(lead, bar(4), bar(20), tempo_bpm=BPM, depth=0.35, rate=6.0)
|
|
calypso_fill(d, 19)
|
|
crash(d, bar(20))
|
|
|
|
# B (20-27): higher, more wistful — the moon over the mangrove
|
|
skank(sk, 20, CHORDS[20:28])
|
|
walking_bass(bass, 20, CHORDS[20:28])
|
|
calypso_groove(d, 20, 8)
|
|
melody(lead, 20, MELODY_B)
|
|
vibrato(lead, bar(20), bar(28), tempo_bpm=BPM, depth=0.4, rate=6.0)
|
|
calypso_fill(d, 27)
|
|
crash(d, bar(28))
|
|
|
|
# A' (28-35): theme returns, busier hats implied by the open hats
|
|
skank(sk, 28, CHORDS[28:36])
|
|
walking_bass(bass, 28, CHORDS[28:36])
|
|
calypso_groove(d, 28, 8)
|
|
melody(lead, 28, MELODY_A2)
|
|
vibrato(lead, bar(28), bar(36), tempo_bpm=BPM, depth=0.45, rate=6.5)
|
|
calypso_fill(d, 35)
|
|
crash(d, bar(36))
|
|
|
|
# outro (36-39): tag, then the whole band holds one last Dm
|
|
skank(sk, 36, CHORDS[36:40])
|
|
walking_bass(bass, 36, CHORDS[36:40])
|
|
calypso_groove(d, 36, 2)
|
|
melody(lead, 36, TAG)
|
|
crash(d, bar(36))
|
|
for p in ("D3", "A3", "D4", "F4", "A4"):
|
|
sk.note(bar(38), N(p), TPQ * 8, 72)
|
|
bass.note(bar(38), N("D2"), TPQ * 8, 84)
|
|
lead.note(bar(38), N("D6"), TPQ * 8, 78)
|
|
crash(d, bar(38), vel=100)
|
|
|
|
mf.add_track(lead); mf.add_track(sk); mf.add_track(bass); mf.add_track(d)
|
|
# game loop: intro plays once, then A-B-A' (bars 4-35) repeats
|
|
mf.set_loop(bar(4), bar(36))
|
|
return mf
|
|
|
|
|
|
if __name__ == "__main__":
|
|
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
|
mf = build()
|
|
mf.write(OUT)
|
|
d = verify(OUT, min_sec=60, max_sec=180)
|
|
print("OK:", OUT)
|
|
print(" duration:", d["duration_sec"], "s | notes:",
|
|
sum(len(t["notes"]) for t in d["tracks"]),
|
|
"| tracks:", len(d["tracks"]))
|