#!/usr/bin/env python3 """ "Service Station at 2 AM" — DOS adventure main theme Style: early-90s point-and-click / LucasArts-meets-OPL. D minor, 132 BPM. Structure: 4-bar intro, A (8), B (8), A' with counter-melody (8), outro (4). The DOS fingerprints: - OPL-style 16th-note arpeggios on electric piano - driving 8th-note synth bass with chromatic approach notes - square lead with pitch-bend vibrato on held notes - the G# in bar 4 (harmonic-minor leading tone — instant 1993) """ import os, sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) from midikit import MidiFile, Track, MPQN, verify from songkit import (bar, melody, arp, bass_drive, drums, vibrato, crash, parse_score, TPQ) BPM = 132 OUT = os.path.join(os.path.dirname(__file__), "..", "midi", "01_service_station_2am.mid") # Chord chart, one (root, quality) per bar, 32 bars CHORDS = ( [("D", "m"), ("Bb", ""), ("G", "m"), ("A", "")] + # intro [("D", "m"), ("Bb", ""), ("G", "m"), ("A", ""), ("D", "m"), ("Bb", ""), ("G", "m"), ("D", "m")] + # A [("Bb", ""), ("C", ""), ("D", "m"), ("Bb", ""), ("G", "m"), ("D", "m"), ("Bb", ""), ("A", "")] + # B [("D", "m"), ("Bb", ""), ("G", "m"), ("A", ""), ("D", "m"), ("Bb", ""), ("G", "m"), ("D", "m")] + # A' [("D", "m"), ("Bb", ""), ("A", ""), ("D", "m")] # outro ) # --- melodies (bar grids) ------------------------------------------------- MELODY_A = """ D4-8 F4-8 A4-4 A4-8 Bb4-8 A4-4 | F4-8 A4-8 Bb4-4 A4-8 F4-8 D4-4 | G4-8 Bb4-8 D5-8 C5-8 Bb4-4 A4-4 | B4-8 A4-8 G#4-8 A4-8 E4-2 | D5-8 C5-8 D5-8 F5-8 E5-4 D5-4 | C5-8 Bb4-8 C5-8 D5-8 Bb4-2 | A4-8 Bb4-8 C5-4 E5-4 D5-4 | D5-2 r2 """ MELODY_B = """ F5-4 F5-8 Eb5-8 F5-4 D5-4 | E5-4 G5-8 F5-8 E5-4 C5-4 | D5-8 F5-8 A5-4 G5-8 F5-8 E5-4 | F5-2 D5-4 Bb4-8 C5-8 | Bb5-4 A5-8 G5-8 F5-4 D5-4 | E5-4 F5-8 E5-8 D5-4 A4-4 | Bb4-8 C5-8 D5-4 E5-8 F5-8 G#5-4 | A5-2 G#5-4 E5-4 """ MELODY_OUTRO = """ A4-8 D5-8 F5-8 A5-8 D6-2 | C6-4 Bb5-4 F5-4 D5-4 | E5-4 D5-4 C#5-4 A4-4 | D5-1 """ PICKUP = "r2 A4-8 Bb4-8 A4-8 G4-8" # intro bar 4 pickup # A' bars: octave-ish answer underneath the main melody COUNTER_A = """ r2 D5-8 F5-8 E5-4 | r2 F5-8 D5-8 Bb4-4 | r2 G5-8 F5-8 D5-4 | r2 E5-8 G5-8 F5-8 E5-8 | r4 A5-8 G5-8 F5-4 A5-4 | r4 F5-8 A5-8 D5-4 F5-4 | r2 C6-4 Bb5-4 | r2 F5-4 D5-4 r4 """ 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("Lead (square)", channel=0) lead.program(0, "square lead") lead.cc(0, 7, 104); lead.cc(0, 10, 54) # vol, pan centre counter = Track("Counter (EP2)", channel=1) counter.program(0, "electric piano 2") counter.cc(0, 7, 80); counter.cc(0, 10, 90) bass = Track("Bass", channel=2) bass.program(0, "synth bass 1") bass.cc(0, 7, 92); bass.cc(0, 10, 30) acomp = Track("Arp (OPL)", channel=3) acomp.program(0, "electric piano 1") acomp.cc(0, 7, 66); acomp.cc(0, 10, 20) d = Track("Drums", channel=9) d.cc(0, 7, 100) # intro: arp + bass only, pickup lead in bar 4 arp(acomp, 0, CHORDS[0:4], pattern="012321", velocity=64) bass_drive(bass, 0, CHORDS[0:4], velocity=80) melody(lead, 3, PICKUP) drums(d, 3, 1, "shufflesnare", velocity_scale=0.8) # A (bars 4-11) arp(acomp, 4, CHORDS[4:12], pattern="012321", velocity=70) bass_drive(bass, 4, CHORDS[4:12]) melody(lead, 4, MELODY_A) vibrato(lead, bar(4), bar(12), tempo_bpm=BPM, depth=0.55) drums(d, 4, 8, "rock") drums(d, 6, 1, "dos_roll", velocity_scale=0.7) drums(d, 11, 1, "dos_roll", velocity_scale=0.9) crash(d, bar(12)) # B (bars 12-19) arp(acomp, 12, CHORDS[12:20], pattern="021321", velocity=72) bass_drive(bass, 12, CHORDS[12:20]) melody(lead, 12, MELODY_B) vibrato(lead, bar(12), bar(20), tempo_bpm=BPM, depth=0.6) drums(d, 12, 8, "rock") drums(d, 15, 1, "dos_roll", velocity_scale=0.7) drums(d, 19, 1, "break", velocity_scale=0.9) crash(d, bar(20)) # A' (bars 20-27): same melody + counter-melody, busier hats arp(acomp, 20, CHORDS[20:28], pattern="012321", velocity=74) bass_drive(bass, 20, CHORDS[20:28]) melody(lead, 20, MELODY_A) vibrato(lead, bar(20), bar(28), tempo_bpm=BPM, depth=0.65) melody(counter, 20, COUNTER_A) drums(d, 20, 8, "rock") drums(d, 23, 1, "dos_roll", velocity_scale=0.7) drums(d, 27, 1, "dos_roll") crash(d, bar(28)) # outro arp(acomp, 28, CHORDS[28:32], pattern="0121", velocity=66) bass_drive(bass, 28, CHORDS[28:32], subdivision=1, velocity=75) melody(lead, 28, MELODY_OUTRO) vibrato(lead, bar(28), bar(32), tempo_bpm=BPM, depth=0.5) crash(d, bar(28)) drums(d, 28, 1, "break", velocity_scale=0.8) # final sustained chord (Dm) on counter from midikit import note as N for p in ("D3", "A3", "D4", "F4", "A4", "D5"): counter.note(bar(31), N(p), TPQ * 4, 70) mf.add_track(lead); mf.add_track(counter); mf.add_track(bass) mf.add_track(acomp); mf.add_track(d) # loop points for game engines: intro plays once, A-B-A' loops mf.set_loop(bar(4), bar(28)) return mf if __name__ == "__main__": mf = build() mf.write(OUT) d = verify(OUT, min_sec=40, max_sec=120) print("OK:", OUT) print(" duration:", d["duration_sec"], "s | notes:", sum(len(t["notes"]) for t in d["tracks"]), "| tracks:", len(d["tracks"]))