#!/usr/bin/env python3 """ Starlit Conversation — 90s Slow Jam / Love Song ============================================ Romantic, emotional ballad with Rhodes electric piano, warm pads, gentle strings, and soft drum groove. 88 BPM. Structure: 26 bars (~1.8 min at 88 BPM) Intro (2 bars) — Rhodes soft chords only Verse (8 bars) — Rhodes melody + pads Chorus (8 bars) — Rhodes + strings + soft drums Verse 2 (8 bars) — Rhodes + pads + drums Chorus 2 (4 bars) — Rhodes + strings + drums Outro (6 bars) — Rhodes fade only Instruments: T0: Rhodes (GM 16) — electric piano chords + melody T1: Pads (GM 88) — warm choral backdrop T2: Strings (GM 49) — sustained chord accents T3: Drums (Ch 10) — soft 90s slow-jam groove """ from midiutil import MIDIFile # ========== Configuration ============= BPM = 88 PPQ = 960 # Chord progression: Bb | G | Am | F CHORDS = [ {"root": 58, "notes": [58, 62, 66]}, # Bb3: Bb3, D4, F4 {"root": 55, "notes": [55, 59, 63]}, # G3: G3, Bb3, D4 {"root": 57, "notes": [57, 61, 65]}, # Am3: A3, C4, Em4 {"root": 53, "notes": [53, 57, 61]}, # F3: F3, Am3, C4 ] STRUCTURE = [ ("intro", 2), ("verse", 8), ("chorus", 8), ("verse2", 8), ("chorus2", 4), ("outro", 6), ] total_bars = sum(c for _, c in STRUCTURE) # ========== Helper functions ============= def pad_sustain(midi, track, channel, num_bars, chord_root_fn, vol=55): """Add sustained warm pad chords. Each note gets ONE note_on/note_off.""" midi.addControllerEvent(track, channel, 0, 91, 70) # reverb midi.addControllerEvent(track, channel, 0, 93, 80) # chorus midi.addControllerEvent(track, channel, 0, 1, 40) # modulation midi.addControllerEvent(track, channel, 0, 11, 125) # expression for bar in range(num_bars): root = chord_root_fn(bar) beat = bar * 4.0 # Three-note voicing, one note_on each for note in [root + 7, root + 12, root + 19]: midi.addNote(track, channel, note, beat, 3.6, vol) def slow_jam_drums(midi, track, num_bars): """ Soft drum pattern — no four-on-the-floor: Kick on 1. Ghost kick on 3. Snare/CLAP on 2, 3, 4. HH on every 8th note. """ for bar in range(num_bars): b = bar * 4.0 # Kick midi.addNote(track, 9, 36, b + 0.0, 0.45, 85) # beat 1 midi.addNote(track, 9, 36, b + 2.5, 0.25, 60) # ghost on 3 # Snare on 2, 3, 4 for bp in [2.0, 3.0, 3.5]: midi.addNote(track, 9, 38, b + bp, 0.35, 65) # Closed HH on every half-beat for bp in range(8): midi.addNote(track, 9, 42, b + bp * 0.5, 0.25, 45) # Open HH shimmer at end of chorus bars if bar % 8 in (8, 20): midi.addNote(track, 9, 46, b + 3.8, 0.3, 40) def rhodes_melody(midi, track, channel, bars_offset, num_bars, chords, pattern): """ Rhodes electric piano melody. pattern: "intro", "verse", "chorus", "outro" bars_offset is the starting bar index. """ for i in range(num_bars): bar = bars_offset + i chord = chords[bar % len(chords)] root = chord["notes"][0] base = bar * 4.0 if pattern == "intro": # Soft sustained chords — one note on per bar notes = [chord["notes"][(j * 3) % len(chord["notes"])] for j in range(2)] for j, n in enumerate(notes): midi.addNote(track, channel, n + 12, base + j * 1.5, 1.3, 70) elif pattern == "verse": # 3-note phrases leaving space phrases = [ (0.0, root + 7, 1.5), # 5th (2.0, root + 12, 1.0), # root + octave (3.5, root + 16 + (i % 2), 1.0), # variation ] for bp, pitch, dur in phrases: midi.addNote(track, channel, pitch, base + bp, dur, 78) elif pattern == "chorus": # Flowing 1/8-note arpeggio phrases offsets = [0, 3, 7, 12, 7, 3, 0, 4] if i % 2 == 0 else [0, 4, 7, 11, 7, 4, 0, -3] for j, off in enumerate(offsets): pitch = root + off + 12 midi.addNote(track, channel, pitch, base + j * 0.5, 0.35, 82) elif pattern == "outro": # Descending resolution, get quieter notes_seq = [root + 24, root + 19, root + 16, root + 12, root + 7, root + 4] vol = max(40, 80 - i * 8) for j, note in enumerate(notes_seq): midi.addNote(track, channel, note, base + j * 0.85, 0.6, vol) def strings_accent(midi, track, channel, bars_offset, num_bars, chords, pattern="soft"): """ Add string chord accents during chorus sections. Only played on first half of each chorus bar. """ for i in range(num_bars): bar = bars_offset + i chord = chords[bar % len(chords)] root = chord["notes"][0] base = bar * 4.0 vol = 55 if pattern == "soft" else 40 # Add single string chord (one note) to avoid overlap midi.addNote(track, channel, root + 20, base, 2.0, vol) # ========== Build the composition ============= midi = MIDIFile(4, file_format=1, ticks_per_quarternote=PPQ) midi.addTempo(0, 0, BPM) midi.addTrackName(0, 0, "Rhodes Electric") midi.addTrackName(1, 0, "Choir Pads") midi.addTrackName(2, 0, "Strings") midi.addTrackName(3, 0, "Soft Drums") midi.addProgramChange(0, 0, 0, 16) # Rhodes midi.addProgramChange(1, 0, 0, 88) # Choir Pad midi.addProgramChange(2, 0, 0, 49) # Strings # Track 3: Drums (enters at bar 2, verse 1) — but plays all to keep it simple slow_jam_drums(midi, 3, total_bars) # Track 1: Pads (full song) pad_sustain(midi, 1, 0, total_bars, lambda bar: CHORDS[bar % 4]["root"]) # --- Track 0: Rhodes piano sections --- # Intro (bar 0-1): soft sustained rhodes_melody(midi, 0, 0, 0, 2, CHORDS, "intro") # Verse (bar 2-9): verse pattern rhodes_melody(midi, 0, 0, 2, 8, CHORDS, "verse") # Chorus (bar 10-17): flowing pattern rhodes_melody(midi, 0, 0, 10, 8, CHORDS, "chorus") # Verse 2 (bar 18-25): verse pattern rhodes_melody(midi, 0, 0, 18, 8, CHORDS, "verse") # Chorus 2 (bar 26-29): flowing pattern rhodes_melody(midi, 0, 0, 26, 4, CHORDS, "chorus") # Outro (bar 30-35): fading descent rhodes_melody(midi, 0, 0, 30, 6, CHORDS, "outro") # --- Track 2: Strings (only during chorus bars) --- strings_accent(midi, 2, 0, 10, 8, CHORDS, "soft") strings_accent(midi, 2, 0, 26, 4, CHORDS, "soft") # ========== Write to disk ============= output_file = "/home/paperclip/projects/gitea/pi-midi-zone/midi_output/starlit_conversation.mid" with open(output_file, "wb") as f: midi.writeFile(f) print(f"Written: {output_file}") print(f" 4-track, {total_bars} bars at {BPM} BPM") print(f" Style: 90s Slow Jam / Love Song")