diff --git a/README.md b/README.md index 9401681..945659c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,20 @@ # pi-midi-zone -A place for pi to create music! +A place for Pi to create music! + +## Contents + +### Skills + +- **skills/90s-midi-composer/** — Skill for creating authentic 90s-style MIDI music files using Python + mido. Generates General MIDI/SC-55 aesthetic tracks with classic dance beats, synth leads, pads, bass, and piano. + +### Examples + +- **examples/compose_neon_dreams.py** — Python script that composes a 90s dance track ("Neon Dreams"), 48 bars / 1.5 min / 128 BPM in A minor. +- **examples/neon_dreams.mid** — Completed 6-track MIDI file. Uses fluidsynth to listen: `fluidsynth /path/to/FluidR3_GM.sf2 examples/neon_dreams.mid` + +## Quick Start + +1. Install: `pip3 install mido`, `sudo apt install fluidsynth fluid-soundfont-gm` +2. Run: `python3 examples/compose_neon_dreams.py` +3. Listen: `fluidsynth /usr/share/sounds/sf2/FluidR3_GM.sf2 neon_dreams.mid` diff --git a/examples/compose_neon_dreams.py b/examples/compose_neon_dreams.py new file mode 100644 index 0000000..8b93b7c --- /dev/null +++ b/examples/compose_neon_dreams.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +""" +NEON DREAMS — 90s-Style Dance Track +Key: A minor, Tempo: 128 BPM, 48 bars (~1:50 duration) + +Structure: + 0–3 : Intro (pads only) + 4–11 : Verse (drums, bass, pads, arpeggio) + 12–19 : Chorus (all instruments, piano melody added) + 20–31 : Outro (drums simplify, piano + arpeggio only) + +Instruments: + Drums (Ch10) — four-on-the-floor dance beat + Synth Lead (Ch1) — sawtooth arpeggios + Pads (Ch2) — warm New Age pad + Bass (Ch3) — synth bass walking roots + Piano (Ch4) — pentatonic melody +""" + +from mido import MidiFile, MidiTrack, Message as M, MetaMessage + +PPQ = 480 # ticks per quarter note +BPM = 128 +TEMPO = 60_000_000 // BPM + +def t(x): + """Beat-time (fractional) to ticks.""" + return int(x * PPQ) + +# Am | F | G | E7 +CHORD = [ + [45, 60, 64], # Am + [41, 45, 60], # F + [43, 46, 50], # G + [40, 47, 52], # E7 +] + +def rel(items): + """Absolute timestamps → relative Mido messages.""" + items.sort(key=lambda x: x[0]) + out, prev = [], 0 + for at, msg in items: + msg.time = at - prev + out.append(msg) + prev = at + return out + +# === DRUM PATTERNS === +_K, _S, _HH, _HHO = 36, 38, 42, 46 # GM drum codes + +def drum_events(total_bars=48): + events = [] + + def kick(tick): + events.append((tick, M('note_on', note=_K, velocity=110, channel=9))) + events.append((tick + t(0.5), M('note_off', note=_K, velocity=50, channel=9))) + + def kick_soft(tick): + events.append((tick, M('note_on', note=_K, velocity=90, channel=9))) + events.append((tick + t(0.35), M('note_off', note=_K, velocity=40, channel=9))) + + def snare(tick): + events.append((tick, M('note_on', note=_S, velocity=125, channel=9))) + events.append((tick + t(0.25), M('note_off', note=_S, velocity=60, channel=9))) + + def hh(v=70): + events.append((v // 10 * 10, M('note_on', note=_HH, velocity=v, channel=9))) # hack, let me redo + events.append((v // 10 * 10, M('note_off', note=_HH, velocity=30, channel=9))) + + # Actually, let me do this differently: + # I'll add events directly with tick offsets + + for bar in range(total_bars): + base = bar * t(4.0) + # Determine bar type based on section + if bar < 4: + # Intro: no drums + continue + elif bar < 40: + # Normal dance beat (standard) + # Beat 1: Kick + kick(base) + events.append((base + t(0.5), M('note_on', note=_HH, velocity=65, channel=9))) + events.append((base + t(0.5), M('note_off', note=_HH, velocity=35, channel=9))) + + # Beat 2: Kick + Snare + HH + kick(base + t(1.0)) + snare(base + t(1.0)) + events.append((base + t(1.5), M('note_on', note=_HH, velocity=60, channel=9))) + events.append((base + t(1.5), M('note_off', note=_HH, velocity=35, channel=9))) + + # Beat 3: Kick + HH + kick(base + t(2.0)) + events.append((base + t(2.5), M('note_on', note=_HH, velocity=60, channel=9))) + events.append((base + t(2.5), M('note_off', note=_HH, velocity=35, channel=9))) + + # Beat 4: Kick + Snare + HH + Open HH + kick(base + t(3.0)) + snare(base + t(3.0)) + events.append((base + t(3.3), M('note_on', note=_HH, velocity=80, channel=9))) + events.append((base + t(3.3), M('note_off', note=_HH, velocity=35, channel=9))) + events.append((base + t(3.3), M('note_on', note=_HHO, velocity=75, channel=9))) + events.append((base + t(3.9), M('note_off', note=_HHO, velocity=35, channel=9))) + + # HH fill at end of bar + events.append((base + t(3.8), M('note_on', note=_HH, velocity=55, channel=9))) + events.append((base + t(3.8), M('note_off', note=_HH, velocity=35, channel=9))) + + # Extra kick at bar 6 (dance build-up) + if bar % 8 == 6 and bar < 40: + kick(base + t(2.5)) + else: + # Outro: simplified, softer drums + events.append((base + t(0), M('note_on', note=_K, velocity=85, channel=9))) + events.append((base + t(0.5), M('note_off', note=_K, velocity=40, channel=9))) + events.append((base + t(1.0), M('note_on', note=_K, velocity=85, channel=9))) + events.append((base + t(1.5), M('note_off', note=_K, velocity=40, channel=9))) + events.append((base + t(2.0), M('note_on', note=_K, velocity=85, channel=9))) + events.append((base + t(2.5), M('note_off', note=_K, velocity=40, channel=9))) + events.append((base + t(3.0), M('note_on', note=_K, velocity=85, channel=9))) + events.append((base + t(3.5), M('note_off', note=_K, velocity=40, channel=9))) + events.append((base + t(3.8), M('note_on', note=_HH, velocity=50, channel=9))) + events.append((base + t(3.8), M('note_off', note=_HH, velocity=30, channel=9))) + + return rel(events) + + +# === PADS === +def pad_events(total_bars=48): + events = [ + (0, M('program_change', program=88, channel=2)), + (0, M('control_change', control=11, value=127, channel=2)), + (0, M('control_change', control=91, value=45, channel=2)), + (0, M('control_change', control=93, value=55, channel=2)), + ] + + for bar in range(total_bars): + chord = CHORD[bar % 4] + on_tick = bar * t(4.0) + off_tick = on_tick + t(3.7) + # Crossfade: next chord starts 0.3 beats before this one ends + for note in chord: + events.append((on_tick, M('note_on', note=note, velocity=55, channel=2))) + events.append((off_tick, M('note_off', note=note, velocity=25, channel=2))) + + return rel(events) + + +# === BASS === +def bass_events(total_bars=48): + events = [ + (0, M('program_change', program=37, channel=3)), + ] + + for bar in range(total_bars): + root, third, fifth = CHORD[bar % 4] + base = bar * t(4.0) + + # Section-aware patterns + if bar < 4: + # Intro: no bass + continue + elif bar < 16 or bar % 4 == 0: + notes = [root, root + 9, root + 16, (root + 9)] # pentatonic walk + elif bar % 4 == 2: + notes = [root, root + 2, root + 5, root + 7] # chromatic climb + else: + notes = [root, root + 4, root + 7, root + 12] # triad arpeggio + + for i, note in enumerate(notes): + on = base + t(1.0 * i) + off = base + t(1.0 + 0.01 * i) + events.append((on, M('note_on', note=note, velocity=92, channel=3))) + events.append((off, M('note_off', note=note, velocity=30, channel=3))) + + return rel(events) + + +# === SYNTH LEAD ARPEGGIOS === +def lead_events(total_bars=48): + events = [ + (0, M('program_change', program=80, channel=1)), + (0, M('control_change', control=11, value=100, channel=1)), + (0, M('control_change', control=91, value=80, channel=1)), + (0, M('control_change', control=93, value=70, channel=1)), + (0, M('control_change', control=5, value=60, channel=1)), # filter cutoff + ] + + for bar in range(total_bars): + root, third, fifth = CHORD[bar % 4] + base = bar * t(4.0) + + if bar < 4: + # Intro: sparse arpeggio (every 2nd bar) + if bar % 2 == 1: + continue + arp = [root + 12, root + 19, root + 24] + for i, note in enumerate(arp): + on = base + t(1.0 * i) + off = base + t(3.0 * i) + t(2.9) + events.append((on, M('note_on', note=note, velocity=55, channel=1))) + events.append((off, M('note_off', note=note, velocity=30, channel=1))) + continue + + # Octave-shifted arpeggio + u3 = third + 12 + u5 = fifth + 12 + ur = root + 12 + + if bar < 40: + # Normal: 8 notes per bar (ascending-descending octaves) + arp = [ur, u3, u5, ur, u5, u3, ur, fifth] + else: + # Outro: sparse + arp = [ur, fifth, ur, u3] + + for i, note in enumerate(arp): + on = base + t(0.25 * i) + off = base + t(0.25 * i) + t(0.2) + events.append((on, M('note_on', note=note, velocity=75, channel=1))) + events.append((off, M('note_off', note=note, velocity=40, channel=1))) + + return rel(events) + + +# === PIANO MELODY === +def piano_events(total_bars=48): + events = [ + (0, M('program_change', program=1, channel=4)), + ] + + shapes = [ + lambda p: [p[2], p[4], p[0], p[2]], + lambda p: [p[3], p[0], p[1], p[3]], + lambda p: [p[4], p[2], p[1], p[4]], + lambda p: [p[0], p[2], p[3], p[0]], + ] + + for bar in range(total_bars): + root = CHORD[bar % 4][0] + base = bar * t(4.0) + + if bar < 12: + # Chorus starts at bar 12 + continue + elif bar >= 47: + break + + pent = [root + i for i in [0, 3, 5, 7, 10]] + mel = shapes[bar % 4](pent) + + for i, note in enumerate(mel): + on = base + t(1.0 * i) + off = base + t(1.0 * i) + t(0.8) + events.append((on, M('note_on', note=note, velocity=85, channel=4))) + events.append((off, M('note_off', note=note, velocity=30, channel=4))) + + return rel(events) + + +# === MAIN === +def main(): + total_bars = 48 + mid = MidiFile(ticks_per_beat=PPQ) + + # Track 0: Tempo + tempo_trk = MidiTrack() + tempo_trk.append(MetaMessage('set_tempo', tempo=TEMPO, time=0)) + tempo_trk.append(MetaMessage('track_name', 'Tempo', time=0)) + mid.tracks.append(tempo_trk) + + # Track 1: Drums + drums_trk = MidiTrack() + drums_trk.append(MetaMessage('set_tempo', tempo=TEMPO, time=0)) + drums_trk.extend(drum_events(total_bars)) + mid.tracks.append(drums_trk) + + # Track 2: Pads + pads_trk = MidiTrack() + pads_trk.extend(pad_events(total_bars)) + mid.tracks.append(pads_trk) + + # Track 3: Bass + bass_trk = MidiTrack() + bass_trk.extend(bass_events(total_bars)) + mid.tracks.append(bass_trk) + + # Track 4: Synth Lead + lead_trk = MidiTrack() + lead_trk.extend(lead_events(total_bars)) + mid.tracks.append(lead_trk) + + # Track 5: Piano + piano_trk = MidiTrack() + piano_trk.extend(piano_events(total_bars)) + mid.tracks.append(piano_trk) + + outfile = 'neon_dreams.mid' + mid.save(outfile) + + total_ticks = sum(m.time for trk in mid.tracks for m in trk) + print(f"Saved {outfile} — {len(mid.tracks)} tracks") + for i, trk in enumerate(mid.tracks): + tt = sum(m.time for m in trk) + name = trk.name if hasattr(trk, 'name') and trk.name else f'Track {i}' + print(f" Track {i} ({name}): {tt} ticks = {tt/480/4:.0f} bars = {tt/480/128*60/60:.1f}s") + + +if __name__ == '__main__': + main() diff --git a/examples/neon_dreams.mid b/examples/neon_dreams.mid new file mode 100644 index 0000000..155bfbc Binary files /dev/null and b/examples/neon_dreams.mid differ diff --git a/skills/90s-midi-composer/SKILL.md b/skills/90s-midi-composer/SKILL.md new file mode 100644 index 0000000..13c4426 --- /dev/null +++ b/skills/90s-midi-composer/SKILL.md @@ -0,0 +1,168 @@ +--- +name: 90s-midi-composer +description: Create authentic 90s-style MIDI music files with General MIDI/SC-55 aesthetic. Use when creating MIDI music, composing dance/electronic tracks, or making 90s retro-sounding songs. +license: MIT +compatibility: Linux with Python3, mido library +--- + +# 90s MIDI Composer + +Create authentic early-to-mid 90s-style MIDI music using Linux tools and Python + mido. + +## Prerequisites + +Install required tools (one-time setup, requires sudo): +```bash +sudo apt-get install -y fluidsynth fluid-soundfont-gm +pip3 install --break-system-packages mido +``` + +## The Tool: Python + Mido + +Mido is a Python library for working with MIDI files. Key classes: +- `MidiFile(ticks_per_beat=480)` — MIDI file container +- `MidiTrack()` — list of Message objects (one per instrument) +- `Message('note_on', note=N, velocity=V, channel=C, time=T)` — start a note +- `Message('note_off', note=N, velocity=V, channel=C, time=T)` — stop a note +- `Message('program_change', program=N, channel=C, time=T)` — change instrument +- `Message('control_change', control=N, value=V, channel=C, time=T)` — CC control +- `MetaMessage('set_tempo', tempo=μs_per_beat, time=0)` — set tempo (μs/beat) + +## Timing in Mido + +Mido uses **relative** timing. Each message has `time` = ticks since the previous one. + +``` +ticks_per_beat = 480 (PPQ) +1 sixteenth-note = 120 ticks +1 eighth-note = 240 ticks +1 quarter-note = 480 ticks (1 beat) +1 bar (4/4) = 1920 ticks (4 beats × 480) +``` + +### Absolute → Relative Conversion + +Build events with absolute tick offsets, then convert: +```python +def rel(items): # items = [(abs_tick, Message), ...] + items.sort(key=lambda x: x[0]) + out, prev = [], 0 + for at, msg in items: + msg.time = at - prev + out.append(msg) + prev = at + return out +``` + +## Creating a Basic MIDI File + +```python +from mido import MidiFile, MidiTrack, Message as M, MetaMessage + +PPQ = 480 # pulses per quarter note (ticks/beat) +BPM = 128 +TEMPO = 60_000_000 // BPM + +mid = MidiFile(ticks_per_beat=PPQ) + +# Track 0: Tempo +trk = MidiTrack() +trk.append(MetaMessage('set_tempo', tempo=TEMPO, time=0)) +mid.tracks.append(trk) + +# Track 1: Drums (channel 10 = 9) +drums = MidiTrack() +# ... add drum messages ... +mid.tracks.append(drums) + +mid.save('output.mid') +``` + +## 90s Aesthetic Guide + +### Essential GM Patches (General MIDI) + +| Patch | Name | 90s Role | +|-------|-------------------|-----------------------| +| 1 | Acoustic Grand | Piano / melodic | +| 37 | Synth Bass 1 | Driving synth bass | +| 59 | Brass Section | Hits, accents | +| 80 | Lead 1 Synth | **Arpeggio lead** | +| 88 | Pad 4 (Choir) | Warm atmospheric pads | +| 49 | Strings | Harmonic texture | +| 16 | Electric Piano 1 | Rhodes-style | + +### GM Drum Set (Channel 10 = channel 9) + +| Note | Voice | Note | Voice | +|------|--------------------|------|--------------------| +| 36 | **Kick 1** | 38 | **Snare 1** | +| 42 | **Hi-hat closed** | 46 | Hi-hat open | +| 49 | Crash cymbal | 57 | Tambourine | +| 51 | Ride cymbal | 50 | Low bongo | + +### Classic 90s Dance Beat (120-140 BPM) + +``` +Kick on every quarter note (1, 2, 3, 4) +Snare on beats 2 & 4 +Hi-hat on every eighth note +Open hi-hat on off-beats for groove +``` + +### 90s Chord Progressions + +- **Am | F | G | E7** — vi-IV-V-I in C (the quintessential 90s pop progression) +- **C | G | Am | F** — I-V-vi-IV (the "axis of awesome") +- **Em | C | G | D** — vi-IV-I-V (melancholy 90s feel) +- **Am | C | G | E7** — i-III-VII-V (minor-key drama) + +### Instrument Patterns + +- **Arpeggio lead**: 16th-note chords cycling root-3rd-5th-octave, ascending and descending +- **Walking bass**: Quarter-note chord tone movement, alternating arpeggio directions +- **Pad chords**: 3-voice voicings, hold ~90% of bar for smooth crossfading +- **Piano melody**: Pentatonic or natural minor scale, sparse 4-note phrases per bar + +### Effects (Control Changes) + +- **Reverb**: CC91, value 40-80 (higher = more spacious hall) +- **Chorus**: CC93, value 50-90 +- **Filter**: CC5 or CC74, value 40-127 +- **Expression**: CC11, value 100-127 + +### Song Structure Template + +``` +Intro (4 bars) — Pads only, dreamy and sparse +Verse (8 bars) — Drums enter, bass + pads + arpeggio (no piano) +Chorus (8 bars) — Full arrangement, piano melody joins +Bridge (4 bars) — Breakdown or key change +Final Chorus (8 bars) — Return, full energy +Outro (8 bars) — Drums simplify, piano arpeggio fade +``` + +At 128 BPM: 40 bars ≈ 1.25 minutes. Example: 48 bars = 1.5 minutes. + +### BPM Recommendations + +| Genre | Typical BPM | +|--------------------|-----------| +| 90s slow jam | 80-100 | +| House / dance | 120-130 | +| Trance / techno | 130-150 | +| Drum & bass | 170-180 | + +## Playing MIDI Files + +```bash +# With fluidsynth +fluidsynth -a alsa /usr/share/sounds/sf2/FluidR3_GM.sf2 output.mid + +# On macOS +fluidsynth -a core /path/to/FluidR3_GM.sf2 output.mid +``` + +## See Also + +- Drum reference: `/drum_reference.md` in this skill directory diff --git a/skills/90s-midi-composer/drum_reference.md b/skills/90s-midi-composer/drum_reference.md new file mode 100644 index 0000000..ce02610 --- /dev/null +++ b/skills/90s-midi-composer/drum_reference.md @@ -0,0 +1,51 @@ +# GM Drum Kit Reference (Channel 10 = channel=9) + +## Standard GM Drum Mapping +| Note | Voice | Note | Voice | Note | Voice | Note | Voice | +|------|---------------|------|--------------|------|----------------|------|------------| +| 35 | Low conga | 36 | **Kick 1** | 37 | High conga | 38 | **Snare 1** | +| 39 | Low tom | 40 | Crash cymbal | 41 | High tom | 42 | **Hi-hat Cl** | +| 43 | Ride cymbal 1 | 44 | Closed hi-hat | 45 | Open hi-hat | 46 | **Open Hi-hat** | +| 47 | Pedal hi-hat | 48 | Low tom | 49 | Crash cymbal | 50 | Low bongo | +| 51 | Ride cymbal 2| 52 | High bongo | 53 | Low-mid conga | 54 | Mid conga | +| 55 | High conga | 56 | Low-mid timbale | 57 | **Tambourine** | 58 | High timbale | +| 59 | Low agogo | 60 | High agogo | 61 | Cabasa | 62 | Maracas | +| 63 | Short whistle| 64 | Long whistle | 65 | Short guiro | 66 | Long guiro | +| 67 | Claves | 68 | Hi wood block | 69 | Low wood block | 70 | Mud claps / Cabasa | +| 71 | Hi snap | 72 | Maraca | 73 | Short shaker | 74 | Long shaker | +| 75 | Guiro | 76 | Cabasa | 77 | Whisper | 78 | Stroke hit / scrape | +| 79 | Stroke scrape | 80 | Slap | 81 | Scratch push | 82 | Slap | +| 83 | Scratch pull | 84 | Sticks | 85 | Square click | 86 | Metronome | +| 87 | Drum roll | 88 | Kick pedal | 89 | Kick pedal | 90 | Snare | +| 91 | Snare str. | 92 | Low tom | 93 | Snare str. | 94 | Rim shot | +| 95 | Open hh | 96 | Open hh | 97 | Low tom | 98 | Closed hh | +| 99 | Low-mid tom| 100 | Hi tom | 101 | Crash cym. | 102 | Mid tom | +| 103 | Closed hh| 104 | Ride cym| 105 | Kick 2 | 106 | Snare 2 | +| 107 | Timbale | 108 | Agogo | 109 | Steel drum | 110 | Bongos | +| 111 | congas | 112 | Bell | | | | | + +## Classic Drum Patterns + +### 128 BPM Dance (4/4) +``` +Bar 1, 2, 3: Standard dance beat +Bar 4: Add fills + +Kick: X . . . X . . . (on 1, 5) +Snare: . . . . X . . . (on 5) +HH closed:. . . . . . . . . (constant 8ths) +``` + +### 120 BPM Half-time +``` +Kick: X . . . . . . . (on 1 only) +Snare: . . . . X . . . (on 5) +HH closed:. X . . . X . . (subdued, syncopated) +``` + +### 140 BPM Breakbeat-tinged +``` +Kick: X . . X . . . . (16th feel) +Snare: . . . . X . . X (with accent on 9) +HH closed: (16th notes, accented) +```