Files
midi-playlist/tunes/05_moonlit_mangrove_calypso.py
Hermes Agent 1528c44edd 05_moonlit_mangrove_calypso: minor-key calypso, steel pan + skank + walking bass
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.
2026-10-17 17:42:08 +00:00

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"]))