diff --git a/midi/05_moonlit_mangrove_calypso.mid b/midi/05_moonlit_mangrove_calypso.mid new file mode 100644 index 0000000..88aecc5 Binary files /dev/null and b/midi/05_moonlit_mangrove_calypso.mid differ diff --git a/tunes/05_moonlit_mangrove_calypso.py b/tunes/05_moonlit_mangrove_calypso.py new file mode 100644 index 0000000..f7eebf2 --- /dev/null +++ b/tunes/05_moonlit_mangrove_calypso.py @@ -0,0 +1,203 @@ +#!/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"]))