
Music Generation
- 2 installs
- 404 repo stars
- Updated August 5, 2026
- aiskillstore/marketplace
music-generation is a Claude Code skill that composes original music with music21 and renders it to downloadable MP3/WAV audio.
About
music-generation is a Claude Code skill for composing original music programmatically with the music21 library and rendering it to downloadable MP3 or WAV files. A developer uses it to generate orchestral and classical pieces via a FluidSynth soundfont pipeline or electronic tracks via real-time synthesis. It bundles scripts for MIDI inventory, transformation, synthesis, and audio-quality validation.
- Composes original music algorithmically with music21 and exports downloadable MP3/WAV
- Two rendering pipelines: orchestral/acoustic via FluidSynth and electronic via real-time synthesis
- Includes scripts for MIDI inventory, transform, drum/melodic synthesis, and audio validation
Music Generation by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,166 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
music-generation capabilities & compatibility
- Capabilities
- music composition · midi rendering · audio synthesis · audio validation
- Use cases
- video generation · image generation
- Pricing
- Free
What music-generation says it does
Tools, patterns, and utilities for generating professional music with realistic instrument sounds.
This skill supports TWO rendering pipelines. You MUST choose based on the musical genre:
npx skills add https://github.com/aiskillstore/marketplace --skill music-generationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 404 |
| Last updated | August 5, 2026 |
| Repository | aiskillstore/marketplace ↗ |
What it does
Compose original music with music21 and render downloadable MP3/WAV, orchestral or electronic.
Who is it for?
Developers who need original compositions or timed music exported as MP3/WAV files.
Skip if: Editing existing recorded audio or producing interactive HTML music players.
When should I use this skill?
A user requests downloadable music, original compositions, classical pieces, or timed music for videos.
What you get
Downloadable MP3/WAV music files composed algorithmically for the requested genre.
- MP3/WAV music files
- MIDI files
By the numbers
- 2 rendering pipelines (traditional FluidSynth and electronic synthesis)
- 6 Python dependencies: music21, midi2audio, pydub, mido, numpy, scipy
Files
Quick Start (Read This First!)
IMPORTANT: This file is located at `/mnt/skills/private/music-generation/SKILL.md`
If you need to reference this skill again during your session, read that exact path directly. Do not explore directories or use find commands - just read the file path above.
Philosophy
This skill provides tools and patterns for music composition, not pre-baked solutions. You should use your intelligence and the music21 library to compose dynamically based on user requests.
Core Principle: Write custom code that composes music algorithmically rather than calling functions with hardcoded melodies.
Installation & Setup
Quick Installation
Run the automated installer for complete setup:
bash /mnt/skills/private/music-generation/install.shThis installs all system dependencies, Python packages, and verifies the installation.
Note: The install script may display "error: externally-managed-environment" messages at the end. These are expected and can be safely ignored - the dependencies are already installed. If you see these messages, the installation was successful.
Manual Installation
Alternatively, install dependencies manually:
System Dependencies:
apt-get update
apt-get install -y fluidsynth fluid-soundfont-gm fluid-soundfont-gs ffmpegPython Dependencies:
pip install -r /mnt/skills/private/music-generation/requirements.txtThe requirements.txt includes: music21, midi2audio, pydub, mido, numpy, scipy.
Available SoundFonts
Traditional Pipeline (Orchestral/Acoustic):
/usr/share/sounds/sf2/FluidR3_GM.sf2(141MB, General MIDI soundfont for orchestral/classical)/usr/share/sounds/sf2/default.sf2(symlink to best available)
Electronic Pipeline:
- No soundfonts required - uses real-time synthesis for all electronic sounds
Quick Start: Write Custom Compositions
Basic Music Generation Pattern
from music21 import stream, note, chord, instrument, tempo, dynamics
from midi2audio import FluidSynth
from pydub import AudioSegment
# 1. Create score and parts
score = stream.Score()
violin_part = stream.Part()
violin_part.insert(0, instrument.Violin())
violin_part.insert(0, tempo.MetronomeMark(number=120))
# 2. Generate notes algorithmically
for measure in range(16):
violin_part.append(note.Note('E5', quarterLength=1.0))
violin_part.append(note.Note('G5', quarterLength=1.0))
violin_part.append(note.Note('A5', quarterLength=2.0))
# 3. Export to MIDI
score.append(violin_part)
midi_path = '/mnt/user-data/outputs/composition.mid'
score.write('midi', fp=midi_path)
# 4. Render with FluidSynth
fs = FluidSynth('/usr/share/sounds/sf2/FluidR3_GM.sf2')
wav_path = '/mnt/user-data/outputs/composition.wav'
fs.midi_to_audio(midi_path, wav_path)
# 5. Convert to MP3
audio = AudioSegment.from_wav(wav_path)
mp3_path = '/mnt/user-data/outputs/composition.mp3'
audio.export(mp3_path, format='mp3', bitrate='192k')Key Concepts
- Always create downloadable MP3 files (not HTML players)
- All output goes to
/mnt/user-data/outputs/ - Use music21.instrument classes:
instrument.Violin(),instrument.Violoncello(),instrument.Piano(),instrument.Trumpet(), etc. - Generate notes programmatically - avoid hardcoded sequences
Choosing the Right Rendering Pipeline
CRITICAL: This skill supports TWO rendering pipelines. You MUST choose based on the musical genre:
Traditional Pipeline (Orchestral, Classical, Acoustic)
Use when creating:
- Orchestral music (violin, cello, trumpet, etc.)
- Classical compositions (Mozart, Beethoven style)
- Piano music, chamber music, symphonies
- Acoustic guitar, brass ensembles
- Any music with traditional/acoustic instruments
How to render:
# After composing with music21 and exporting MIDI...
from midi2audio import FluidSynth
from pydub import AudioSegment
fs = FluidSynth('/usr/share/sounds/sf2/FluidR3_GM.sf2')
fs.midi_to_audio(midi_path, wav_path)
audio = AudioSegment.from_wav(wav_path)
audio.export(mp3_path, format='mp3', bitrate='192k')Electronic Pipeline (House, Techno, EDM, Electronic)
Use when creating:
- House, techno, trance, EDM
- Electronic dance music with synth bass/pads/leads
- DJ beats, club music
- Any music described as "electronic" or "synth-heavy"
- Music referencing DJs like Keinemusik, Black Coffee, etc.
How to render:
# After composing with music21, using mido for instruments, and exporting MIDI...
import subprocess
# Use the electronic rendering script
result = subprocess.run([
'python',
'/mnt/skills/private/music-generation/scripts/render_electronic.py',
midi_path,
mp3_path
], capture_output=True, text=True)
print(result.stdout)
if result.returncode != 0:
print(f"Error: {result.stderr}")Why this matters:
- The orchestral soundfont (FluidR3_GM.sf2) sounds terrible for electronic music
- Its "synth" instruments are basic 1990s approximations
- The electronic pipeline uses real-time synthesis for authentic electronic sound
- Synthesizes 808-style kicks, electronic snares, and hi-hats on-the-fly (NO external samples required)
- Bass/pads/leads use subtractive synthesis with filters and ADSR envelopes
- Genre presets (deep_house, techno, trance, ambient) tune synthesis parameters automatically
Drum Synthesis:
The electronic renderer uses real-time drum synthesis (no external samples needed). All drum sounds (kicks, snares, hi-hats, claps) are synthesized on-the-fly with genre-specific parameters.
Example: House Track
# 1. Compose with music21 (same as always)
score = stream.Score()
drums = stream.Part()
bass = stream.Part()
pads = stream.Part()
# ... compose your music
# 2. Export MIDI
midi_path = '/mnt/user-data/outputs/deep_house.mid'
score.write('midi', fp=midi_path)
# 3. Fix instruments with mido (INSERT program_change messages)
from mido import MidiFile, Message
mid = MidiFile(midi_path)
for i, track in enumerate(mid.tracks):
if i == 1: # Drums
for msg in track:
if hasattr(msg, 'channel'):
msg.channel = 9
elif i == 2: # Bass - INSERT program_change
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=38, time=0))
mid.save(midi_path)
# 4. Render with ELECTRONIC pipeline with deep_house preset!
import subprocess
subprocess.run([
'python',
'/mnt/skills/private/music-generation/scripts/render_electronic.py',
midi_path,
'/mnt/user-data/outputs/deep_house.mp3',
'--genre', 'deep_house'
])Available Genre Presets
The electronic renderer includes pre-tuned synthesis presets with supersaw lead synthesis for thick, professional EDM sounds:
- deep_house: Warm bass with 3-voice leads (120-125 BPM)
- techno: Hard-hitting with 7-voice supersaw leads (125-135 BPM)
- trance: Uplifting with massive 9-voice supersaw leads (130-140 BPM)
- ambient: Soft, atmospheric with 5-voice pads (60-90 BPM)
- acid_house: Squelchy TB-303 bass with 5-voice leads (120-130 BPM)
- default: Balanced 5-voice leads (120-130 BPM)
Supersaw Synthesis (Swedish House Mafia / Progressive House Sound):
The electronic renderer now includes unison voice synthesis for fat, buzzy leads:
- Multiple detuned oscillators: 3-9 sawtooth waves per note (genre-dependent)
- Aggressive detuning: 6-15 cents spread creates buzzy chorus effect
- Enhanced saturation: 2.5x distortion for punch and aggression
- Phase spreading: Creates wide stereo image
How It Works:
- House: 3 voices, ±8 cents (subtle, warm)
- Techno: 7 voices, ±12 cents (aggressive, punchy)
- Trance: 9 voices, ±15 cents (massive, soaring)
- Acid house: 5 voices, ±12 cents (squelchy, aggressive)
This replicates the classic supersaw sound from Swedish House Mafia, Avicii, and modern EDM productions.
Each preset tunes:
- Drum synthesis: kick pitch/decay/punch, snare tone/snap, hat brightness/metallic
- Bass synthesis: waveform, filter cutoff/resonance, ADSR envelope
- Pad synthesis: attack/release times, detune amount, brightness
- Lead synthesis: brightness, envelope, portamento
- Volume balance: intelligent mix levels per instrument with frequency-aware compensation
- Velocity curves: exponential, linear, or logarithmic response to MIDI velocity
Intelligent Volume Management
The electronic renderer uses frequency-aware volume balancing to prevent any instrument from overpowering the mix:
How it works:
- Bass frequencies (<100Hz): Automatically reduced by -4 to -6dB (sub-bass has high perceived energy)
- Mid frequencies (200-800Hz): Balanced naturally
- High frequencies (>800Hz): Slightly boosted for clarity (+1 to +1.5dB)
- Genre-specific balance: Each preset has optimized levels (e.g., House bass gets -3dB)
- Velocity curves: MIDI velocity maps intelligently (not just linear)
- Auto-limiting: Final mix is limited to -1dB to prevent clipping
Why this matters:
- House bass (A1, E2) at 55-82Hz naturally has more power - now automatically compensated
- Prevents "bass overpowering everything" issues
- Maintains balanced mix across all genres
- No manual volume tweaking needed
To see all available presets:
python /mnt/skills/private/music-generation/scripts/render_electronic.py --list-genresCustomizing Synthesis Parameters
For advanced control, you can create custom preset JSON files:
{
"drums": {
"kick": {"pitch": 52.0, "decay": 0.6, "punch": 0.9},
"snare": {"tone_mix": 0.25, "snap": 0.8}
},
"bass": {
"waveform": "sawtooth",
"cutoff": 180,
"resonance": 0.7
},
"pad": {
"attack": 1.0,
"brightness": 0.35
}
}Then use with --preset:
python render_electronic.py track.mid output.mp3 --preset my_preset.jsonAdvanced Workflow: Learn from Existing MIDI
For classical pieces or complex compositions, you can:
1. Extract Structure from ANY MIDI File
python /mnt/skills/private/music-generation/scripts/midi_inventory.py \
path/to/mozart.mid \
/mnt/user-data/outputs/mozart_structure.jsonThis extracts:
- Tempo, key signature, time signature
- Track information and instruments
- Complete note sequences with timing
- Musical structure
2. Modify the JSON Structure
import json
# Load extracted structure
with open('/mnt/user-data/outputs/mozart_structure.json', 'r') as f:
structure = json.load(f)
# Modify instruments, notes, timing, etc.
structure['tracks']['track-0']['instrument'] = 'violin' # Change piano to violin!
# Save modified structure
with open('/mnt/user-data/outputs/mozart_violin.json', 'w') as f:
json.dump(structure, f)3. Render Modified Structure to MP3
python /mnt/skills/private/music-generation/scripts/midi_render.py \
/mnt/user-data/outputs/mozart_violin.json \
/mnt/user-data/outputs/mozart_violin.mp3This workflow lets you "recreate" any classical piece with different instruments!
Available Scripts
All scripts are located in /mnt/skills/private/music-generation/scripts/:
Main Workflow Scripts:
- `render_electronic.py` - Electronic music renderer with real-time synthesis (drums, bass, pads, leads)
- `midi_inventory.py` - Extract complete structure from ANY MIDI file to JSON format
- `midi_render.py` - Render JSON music structure to MP3 using FluidSynth
- `midi_transform.py` - Generic MIDI transformations (transpose, tempo change, instrument swap)
- `audio_validate.py` - Validate audio file quality and format
Synthesis Engine (used by render_electronic.py):
- `drum_synthesizer.py` - Synthesizes kicks, snares, hi-hats, claps on-the-fly
- `melodic_synthesizer.py` - Synthesizes bass, pads, and lead sounds using subtractive synthesis
- `synthesis_presets.py` - Genre presets (deep_house, techno, trance, ambient, etc.)
- `midi_utils.py` - MIDI parsing utilities for extracting events and metadata
- `__init__.py` - Python package marker (allows importing scripts as modules)
Utility Scripts:
Music Theory Reference
Complete General MIDI Instrument Map (Programs 0-127)
CRITICAL: music21 has limited instrument support. For most sounds (especially electronic), you MUST use mido to set program numbers after export.
# Piano (0-7)
0: "Acoustic Grand Piano"
1: "Bright Acoustic Piano"
2: "Electric Grand Piano"
3: "Honky-tonk Piano"
4: "Electric Piano 1"
5: "Electric Piano 2"
6: "Harpsichord"
7: "Clavinet"
# Chromatic Percussion (8-15)
8: "Celesta"
9: "Glockenspiel"
10: "Music Box"
11: "Vibraphone"
12: "Marimba"
13: "Xylophone"
14: "Tubular Bells"
15: "Dulcimer"
# Organ (16-23)
16: "Drawbar Organ"
17: "Percussive Organ"
18: "Rock Organ"
19: "Church Organ"
20: "Reed Organ"
21: "Accordion"
22: "Harmonica"
23: "Tango Accordion"
# Guitar (24-31)
24: "Acoustic Guitar (nylon)"
25: "Acoustic Guitar (steel)"
26: "Electric Guitar (jazz)"
27: "Electric Guitar (clean)"
28: "Electric Guitar (muted)"
29: "Overdriven Guitar"
30: "Distortion Guitar"
31: "Guitar Harmonics"
# Bass (32-39)
32: "Acoustic Bass"
33: "Electric Bass (finger)"
34: "Electric Bass (pick)"
35: "Fretless Bass"
36: "Slap Bass 1"
37: "Slap Bass 2"
38: "Synth Bass 1"
39: "Synth Bass 2"
# Strings (40-47)
40: "Violin"
41: "Viola"
42: "Cello"
43: "Contrabass"
44: "Tremolo Strings"
45: "Pizzicato Strings"
46: "Orchestral Harp"
47: "Timpani"
# Ensemble (48-55)
48: "String Ensemble 1"
49: "String Ensemble 2"
50: "Synth Strings 1"
51: "Synth Strings 2"
52: "Choir Aahs"
53: "Voice Oohs"
54: "Synth Voice"
55: "Orchestra Hit"
# Brass (56-63)
56: "Trumpet"
57: "Trombone"
58: "Tuba"
59: "Muted Trumpet"
60: "French Horn"
61: "Brass Section"
62: "Synth Brass 1"
63: "Synth Brass 2"
# Reed (64-71)
64: "Soprano Sax"
65: "Alto Sax"
66: "Tenor Sax"
67: "Baritone Sax"
68: "Oboe"
69: "English Horn"
70: "Bassoon"
71: "Clarinet"
# Pipe (72-79)
72: "Piccolo"
73: "Flute"
74: "Recorder"
75: "Pan Flute"
76: "Blown Bottle"
77: "Shakuhachi"
78: "Whistle"
79: "Ocarina"
# Synth Lead (80-87)
80: "Lead 1 (square)"
81: "Lead 2 (sawtooth)"
82: "Lead 3 (calliope)"
83: "Lead 4 (chiff)"
84: "Lead 5 (charang)"
85: "Lead 6 (voice)"
86: "Lead 7 (fifths)"
87: "Lead 8 (bass + lead)"
# Synth Pad (88-95)
88: "Pad 1 (new age)"
89: "Pad 2 (warm)"
90: "Pad 3 (polysynth)"
91: "Pad 4 (choir)"
92: "Pad 5 (bowed)"
93: "Pad 6 (metallic)"
94: "Pad 7 (halo)"
95: "Pad 8 (sweep)"
# Synth Effects (96-103)
96: "FX 1 (rain)"
97: "FX 2 (soundtrack)"
98: "FX 3 (crystal)"
99: "FX 4 (atmosphere)"
100: "FX 5 (brightness)"
101: "FX 6 (goblins)"
102: "FX 7 (echoes)"
103: "FX 8 (sci-fi)"
# Ethnic (104-111)
104: "Sitar"
105: "Banjo"
106: "Shamisen"
107: "Koto"
108: "Kalimba"
109: "Bag pipe"
110: "Fiddle"
111: "Shanai"
# Percussive (112-119)
112: "Tinkle Bell"
113: "Agogo"
114: "Steel Drums"
115: "Woodblock"
116: "Taiko Drum"
117: "Melodic Tom"
118: "Synth Drum"
119: "Reverse Cymbal"
# Sound Effects (120-127)
120: "Guitar Fret Noise"
121: "Breath Noise"
122: "Seashore"
123: "Bird Tweet"
124: "Telephone Ring"
125: "Helicopter"
126: "Applause"
127: "Gunshot"Complete Drum Map (MIDI Channel 10, Notes 35-81)
Drums use note numbers for different sounds, NOT pitch. Must be on channel 10 (9 in 0-indexed).
# Bass Drums
35: "Acoustic Bass Drum"
36: "Bass Drum 1" # Most common kick
# Snares
38: "Acoustic Snare" # Standard snare
40: "Electric Snare"
# Toms
41: "Low Floor Tom"
43: "High Floor Tom"
45: "Low Tom"
47: "Low-Mid Tom"
48: "Hi-Mid Tom"
50: "High Tom"
# Hi-Hats
42: "Closed Hi-Hat" # Most used
44: "Pedal Hi-Hat"
46: "Open Hi-Hat"
# Cymbals
49: "Crash Cymbal 1"
51: "Ride Cymbal 1"
52: "Chinese Cymbal"
53: "Ride Bell"
55: "Splash Cymbal"
57: "Crash Cymbal 2"
59: "Ride Cymbal 2"
# Percussion
37: "Side Stick"
39: "Hand Clap"
54: "Tambourine"
56: "Cowbell"
58: "Vibraslap"
60: "Hi Bongo"
61: "Low Bongo"
62: "Mute Hi Conga"
63: "Open Hi Conga"
64: "Low Conga"
65: "High Timbale"
66: "Low Timbale"
67: "High Agogo"
68: "Low Agogo"
69: "Cabasa"
70: "Maracas"
71: "Short Whistle"
72: "Long Whistle"
73: "Short Guiro"
74: "Long Guiro"
75: "Claves"
76: "Hi Wood Block"
77: "Low Wood Block"
78: "Mute Cuica"
79: "Open Cuica"
80: "Mute Triangle"
81: "Open Triangle"How to Use Any Instrument (mido workflow)
music21 has built-in classes for orchestral instruments (Violin, Piano, Trumpet, etc.) but NO support for synths, electronic instruments, or many others. To use any GM instrument:
CRITICAL RULE: When you create a stream.Part() WITHOUT assigning a music21 instrument class, music21 WILL NOT create program_change messages in the MIDI file. You MUST use mido to INSERT these messages manually. Simply trying to modify them with if msg.type == 'program_change': msg.program = X will fail silently because no such messages exist!
Helper Function for Setting Instruments:
from mido import Message
def set_track_instrument(track, program):
"""Insert a program_change message at the beginning of a MIDI track."""
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=program, time=0))
# Usage after loading MIDI with mido:
# set_track_instrument(mid.tracks[2], 33) # Set track 2 to Electric BassStep 1: Compose with music21 (use placeholder or skip instrument)
from music21 import stream, note, chord, tempo
score = stream.Score()
# Create parts - don't worry about instrument assignment yet
synth_lead = stream.Part()
synth_pad = stream.Part()
bass = stream.Part()
# Add your notes/chords
synth_lead.append(note.Note('E5', quarterLength=1.0))
# ... compose your music
score.append(synth_lead)
score.append(synth_pad)
score.append(bass)
# Export to MIDI
midi_path = '/mnt/user-data/outputs/track.mid'
score.write('midi', fp=midi_path)Step 2: Assign correct instruments with mido
from mido import MidiFile, Message
mid = MidiFile(midi_path)
# Track 0 is tempo/metadata, actual parts start at track 1
# CRITICAL: You must INSERT program_change messages, not just modify existing ones!
# music21 doesn't create program_change messages if you don't assign instruments
for i, track in enumerate(mid.tracks):
if i == 1: # First part (synth_lead)
# Insert program_change at beginning of track (after track name if present)
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=80, time=0))
elif i == 2: # Second part (synth_pad)
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=88, time=0))
elif i == 3: # Third part (bass)
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=38, time=0))
mid.save(midi_path)Step 3: For drums, ALSO set channel to 9 (channel 10)
# If track is drums, set ALL messages to channel 9
for i, track in enumerate(mid.tracks):
if i == 1: # This is the drum track
for msg in track:
if hasattr(msg, 'channel'):
msg.channel = 9 # Channel 10 in 1-indexedStep 4: Render to audio
from midi2audio import FluidSynth
from pydub import AudioSegment
fs = FluidSynth('/usr/share/sounds/sf2/FluidR3_GM.sf2')
wav_path = '/mnt/user-data/outputs/track.wav'
fs.midi_to_audio(midi_path, wav_path)
audio = AudioSegment.from_wav(wav_path)
mp3_path = '/mnt/user-data/outputs/track.mp3'
audio.export(mp3_path, format='mp3', bitrate='192k')Common Chord Progressions & Styles
# Standard Progressions (Roman numerals)
"pop": ["I", "V", "vi", "IV"] # C-G-Am-F (Journey, Adele)
"epic": ["i", "VI", "III", "VII"] # Am-F-C-G (Epic trailer music)
"sad": ["i", "VI", "iv", "V"] # Am-F-Dm-E (Melancholic)
"jazz": ["ii", "V", "I", "vi"] # Dm-G-C-Am (Jazz standard)
"classical": ["I", "IV", "V", "I"] # C-F-G-C (Classical cadence)
"blues": ["I", "I", "I", "I", "IV", "IV", "I", "I", "V", "IV", "I", "I"] # 12-bar blues
"house": ["i", "VI", "III", "VII"] # Minor house progression
"reggae": ["I", "V", "vi", "IV"] # Offbeat rhythm style
"country": ["I", "IV", "V", "I"] # Simple and direct
"rock": ["I", "bVII", "IV", "I"] # Power chord style
"r&b": ["I", "V", "vi", "iii", "IV", "I", "IV", "V"] # Complex R&B
# Genre-Specific Characteristics
STYLES = {
"house": {
"bpm": 120-128,
"time_signature": "4/4",
"drum_pattern": "4-on-floor kick, offbeat hats",
"bass": "Synth bass with groove",
"common_instruments": [38, 80, 88, 4] # Synth bass, lead, pad, e-piano
},
"jazz": {
"bpm": 100-180,
"time_signature": "4/4 or 3/4",
"chords": "Extended (7th, 9th, 11th, 13th)",
"common_instruments": [0, 32, 64, 56, 73] # Piano, bass, sax, trumpet, drums
},
"orchestral": {
"bpm": 60-140,
"sections": ["strings", "woodwinds", "brass", "percussion"],
"common_instruments": [40, 41, 42, 56, 73, 47] # Violin, viola, cello, trumpet, flute, timpani
},
"rock": {
"bpm": 100-140,
"time_signature": "4/4",
"guitars": "Distorted (30) or clean (27)",
"common_instruments": [30, 33, 0, 128] # Distortion guitar, bass, piano, drums
},
"ambient": {
"bpm": 60-90,
"characteristics": "Long sustained notes, atmospheric pads",
"common_instruments": [88, 89, 90, 91, 52] # Various pads, choir
},
"trap": {
"bpm": 130-170,
"drums": "Tight snare rolls, 808 bass kicks",
"hi_hats": "Fast hi-hat patterns (1/16 or 1/32 notes)",
"common_instruments": [38, 128] # Synth bass, drums
}
}music21 Instrument Classes
from music21 import instrument
# Strings
instrument.Violin()
instrument.Viola()
instrument.Violoncello() # Note: NOT Cello()
instrument.Contrabass()
instrument.Harp()
# Piano
instrument.Piano()
instrument.Harpsichord()
# Brass
instrument.Trumpet()
instrument.Trombone()
instrument.Tuba()
instrument.Horn() # French horn
# Woodwinds
instrument.Flute()
instrument.Clarinet()
instrument.Oboe()
instrument.Bassoon()
instrument.SopranoSaxophone()
instrument.AltoSaxophone()
instrument.TenorSaxophone() # Most common for jazz
instrument.BaritoneSaxophone()
# Other
instrument.AcousticGuitar()
instrument.ElectricGuitar()
instrument.Bass()
instrument.Timpani()
# CRITICAL: music21 has LIMITED support for electronic instruments and drums
# For synths, drums, and electronic sounds, you MUST:
# 1. Create a Part without an instrument (or use a placeholder like Piano())
# 2. Use mido library to INSERT program_change messages after export
# 3. Set drums to MIDI channel 10 (channel 9 in 0-indexed) or they won't sound like drums
#
# Common mistakes:
# - instrument.Cello() doesn't exist - use Violoncello()
# - instrument.FrenchHorn() doesn't exist - use Horn()
# - Setting part.partName doesn't change the sound - you must set MIDI program with mido
# - Drums on channel 0 will play as pitched notes, not drum soundsNote Durations (Quarter Note = 1.0)
- Whole note: 4.0
- Half note: 2.0
- Quarter note: 1.0
- Eighth note: 0.5
- Sixteenth note: 0.25
- Dotted quarter: 1.5
- Triplet quarter: 0.667
mido Quick Reference
For electronic music and drums, use mido to set MIDI programs after music21 export:
from mido import MidiFile, Message
mid = MidiFile(midi_path)
# Insert program_change message
for i, track in enumerate(mid.tracks):
if i == 1: # Your track (tracks start at 1, not 0)
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=38, time=0))
# For drums: Set channel to 9 (channel 10 in 1-indexed)
for i, track in enumerate(mid.tracks):
if i == 1: # Drum track
for msg in track:
if hasattr(msg, 'channel'):
msg.channel = 9
mid.save(midi_path)Common MIDI Programs:
- 38: Synth Bass 1
- 80: Square Lead
- 81: Sawtooth Lead
- 88: Pad 1 (New Age)
- 25: Acoustic Guitar (Steel) - loud, cuts through
- 33: Acoustic Bass
Common Techniques
Drum Programming (4-on-floor house beat)
CRITICAL: music21's .append() adds notes sequentially (one after another), not simultaneously. For layered drums where kicks, snares, and hats play at the same time, you MUST use .insert(offset, note) with explicit timing.
⚠️ ALWAYS USE .insert() FOR ALL TRACKS:
Since layering is needed for nearly all good music composition, you should ALWAYS use .insert(offset, note) for ALL tracks - drums, bass, guitar, pads, everything. This prevents timing bugs and ensures proper synchronization.
NEVER mix .insert() and .append() - If you use .insert() for drums and .append() for other instruments, music21 will miscalculate track lengths and create tracks that are 5-10× longer than intended (8 minutes instead of 1.5 minutes), with only the first 20-25% containing actual sound.
The .append() method should only be used in rare cases where you have a single melodic line with no other instruments.
# WRONG: This plays kick, then 32 hats, then snare pattern (not layered!)
# for beat in range(16):
# drums.append(note.Note(36, quarterLength=1.0)) # Kicks play first
# for eighth in range(32):
# drums.append(note.Note(42, quarterLength=0.5)) # Hats play AFTER all kicks
# # Result: Timing is completely wrong!
# CORRECT: Use .insert() with explicit offsets for simultaneous layering
bars = 32
beats_per_bar = 4
total_beats = bars * beats_per_bar
# Layer 1: Four-on-the-floor kicks (every beat)
for beat in range(total_beats):
offset = float(beat) # Beat 0, 1, 2, 3, 4, 5, ...
drums.insert(offset, note.Note(36, quarterLength=1.0))
# Layer 2: Snare on beats 2 and 4 of each bar
for bar in range(bars):
# Snare on beat 2 (second beat of bar)
offset = float(bar * beats_per_bar + 1)
drums.insert(offset, note.Note(38, quarterLength=1.0))
# Snare on beat 4 (fourth beat of bar)
offset = float(bar * beats_per_bar + 3)
drums.insert(offset, note.Note(38, quarterLength=1.0))
# Layer 3: Hi-hats on eighth notes (every 0.5 beats) - creates groove
for bar in range(bars):
for eighth in range(8): # 8 eighth notes per bar
offset = float(bar * beats_per_bar) + (eighth * 0.5)
if eighth % 2 == 0:
# Closed hat on even eighths (on the beat)
drums.insert(offset, note.Note(42, quarterLength=0.5))
else:
# Open hat on odd eighths (offbeat) - signature house groove
drums.insert(offset, note.Note(46, quarterLength=0.5))
# Result: Properly layered four-on-the-floor with offbeat open hats
# Bar 0: Kicks at 0.0, 1.0, 2.0, 3.0
# Snares at 1.0, 3.0 (on top of kicks)
# Hats at 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5 (layered throughout)Reggae Specific Guidelines
CRITICAL: Reggae has a unique rhythmic identity that requires precise drum patterns, offbeat accents, and heavy bass. If you don't follow these rules, it won't sound like reggae.
Drum Rules (Non-Negotiable):
- "One Drop" pattern: Kick drum on beat 3 ONLY (not beat 1), creating the signature reggae "drop"
- Snare/Rimshot: Beats 2 and 4 (or just beat 3 with the kick)
- Hi-hats: OFFBEAT eighth notes only (never on the beat), creating the "skank" rhythm
- Cross-stick (note 37): Optional on beats 2 and 4 for classic sound
- NO four-on-floor kicks - this is house/electronic, not reggae
# CORRECT Reggae "One Drop" Drum Pattern
bars = 32
beats_per_bar = 4
for bar in range(bars):
for beat in range(beats_per_bar):
offset = float(bar * beats_per_bar + beat)
# Kick ONLY on beat 3 (the "drop")
if beat == 2: # Beat 3 in 0-indexed (0, 1, 2, 3)
drums_part.insert(offset, note.Note(36, quarterLength=1.0))
# Snare on beats 2 and 4
if beat == 1 or beat == 3:
drums_part.insert(offset, note.Note(38, quarterLength=1.0))
# OFFBEAT hi-hats (the "skank") - CRITICAL for reggae feel
for eighth in range(8):
offset = float(bar * beats_per_bar) + (eighth * 0.5)
# ONLY odd eighths (offbeat) - never on the beat
if eighth % 2 == 1: # 0.5, 1.5, 2.5, 3.5 (offbeat)
drums_part.insert(offset, note.Note(42, quarterLength=0.5))
# WRONG - Four-on-floor (this is house, not reggae!)
# for beat in range(total_beats):
# drums_part.insert(float(beat), note.Note(36, quarterLength=1.0)) # ❌ Kick on every beatBass Rules (Non-Negotiable):
- Heavy and prominent - Bass is the lead instrument in reggae
- Octave 1-2 (A1, C2, E2, F1, G1) - not too low, not too high
- Syncopated rhythm - plays between beats, not just on downbeats
- Walking patterns - moves between root, third, fifth of chords
- Quarter to half notes (1.0-2.0 quarterLength) - NOT whole notes like House
# CORRECT Reggae Bass (Am-D-F-G progression, 8-bar pattern)
# This pattern has movement and syncopation - it "walks"
bass_pattern = [
# Bar 1-2: Am (root A)
('A1', 1.0), ('A1', 0.5), ('C2', 0.5), ('A1', 2.0), # Bar 1
('A1', 1.0), ('E2', 1.0), ('A1', 2.0), # Bar 2
# Bar 3-4: D (root D)
('D2', 1.0), ('D2', 0.5), ('F2', 0.5), ('D2', 2.0), # Bar 3
('D2', 1.0), ('A1', 1.0), ('D2', 2.0), # Bar 4
# Bar 5-6: F (root F)
('F1', 1.0), ('F1', 0.5), ('A1', 0.5), ('F1', 2.0), # Bar 5
('F1', 1.0), ('C2', 1.0), ('F1', 2.0), # Bar 6
# Bar 7-8: G (root G, with E for resolution)
('G1', 1.0), ('G1', 0.5), ('B1', 0.5), ('E2', 2.0), # Bar 7
('G1', 1.0), ('D2', 1.0), ('E2', 2.0), # Bar 8
]
# Use .insert() to place bass notes at explicit offsets (synchronizes with drums)
offset = 0.0
for repetition in range(bars // 8):
for pitch, duration in bass_pattern:
bass_part.insert(offset, note.Note(pitch, quarterLength=duration))
offset += duration
# WRONG - Using .append() will cause 8-minute tracks when mixed with .insert() drums
# for pitch, duration in bass_pattern * (bars // 8):
# bass_part.append(note.Note(pitch, quarterLength=duration)) # ❌ Causes timing bug!Guitar "Skank" Rules (Non-Negotiable):
- OFFBEAT chords only - plays on upbeats (the "and" of beats), never downbeats
- CRITICAL: Sufficient duration - Minimum 0.35-0.4 quarterLength (NOT 0.25) to be audible in mix
- Mid-register voicings - Octaves 3-4 (A3, C4, E4)
- Muted/percussive - In real reggae, these are muted strums creating rhythm
⚠️ CRITICAL NOTE DURATION WARNING:
At 0.25 quarterLength, guitar will be completely inaudible in the mix:
- At 82 BPM:
(60 / 82) × 0.25 = 0.183 seconds(183 milliseconds) - Human perceptual threshold: ~200-300ms needed to register in dense mix
- Organ plays at 0.5 (366ms) - TWICE as long
Use 0.4 quarterLength minimum:
- At 82 BPM:
(60 / 82) × 0.4 = 0.293 seconds(293 milliseconds) - Crosses perceptual threshold while maintaining staccato feel
- Adjust rest to 0.1 to maintain 1.0 beat total per skank cycle
# CORRECT Reggae Guitar "Skank" (offbeat chords with AUDIBLE duration)
guitar_chords = [
['A3', 'C4', 'E4'], # Am
['A3', 'C4', 'E4'], # Am (repeat for 2 bars)
['D3', 'F#3', 'A3'], # D
['D3', 'F#3', 'A3'], # D (repeat for 2 bars)
['F3', 'A3', 'C4'], # F
['F3', 'A3', 'C4'], # F (repeat for 2 bars)
['G3', 'B3', 'D4'], # G
['G3', 'B3', 'D4'], # G (repeat for 2 bars)
]
# Use .insert() to place guitar at explicit offsets (synchronizes with drums/bass)
offset = 0.0
for repetition in range(bars // 8):
for chord_notes in guitar_chords:
# Each bar: 4 offbeat skanks
for beat in range(4):
# REST on the beat (downbeat)
guitar_part.insert(offset, note.Rest(quarterLength=0.5))
offset += 0.5
# CHORD on the offbeat (upbeat) - 0.4 duration for audibility
guitar_part.insert(offset, chord.Chord(chord_notes, quarterLength=0.4))
offset += 0.4
# SHORT REST after chord (creates staccato effect)
guitar_part.insert(offset, note.Rest(quarterLength=0.1))
offset += 0.1
# WRONG - Using .append() causes 8-minute tracks when mixed with .insert() drums
# guitar_part.append(chord.Chord(chord_notes, quarterLength=0.4)) # ❌ Causes timing bug!
# WRONG - Duration too short (will be inaudible!)
# guitar_part.insert(offset, chord.Chord(chord_notes, quarterLength=0.25)) # ❌ Only 183ms @ 82 BPMOrgan "Bubble" Rules:
- Alternating on-and-off pattern - Creates rhythmic "bubbling" effect
- Higher register (octaves 4-5) - Sits above guitar
- Plays same chords as guitar but different rhythm
- Shorter duration (0.5 quarterLength) with rests between
# CORRECT Reggae Organ "Bubble"
organ_chords = [
['A4', 'C5', 'E5'], # Am (high register)
['A4', 'C5', 'E5'],
['D4', 'F#4', 'A4'], # D
['D4', 'F#4', 'A4'],
['F4', 'A4', 'C5'], # F
['F4', 'A4', 'C5'],
['G4', 'B4', 'D5'], # G
['G4', 'B4', 'D5'],
]
# Use .insert() to place organ at explicit offsets (synchronizes with drums/bass/guitar)
offset = 0.0
for repetition in range(bars // 8):
for organ_chord in organ_chords:
# Each bar: bubble pattern (chord, rest, chord, rest)
for _ in range(2): # Twice per bar
organ_part.insert(offset, chord.Chord(organ_chord, quarterLength=0.5))
offset += 0.5
organ_part.insert(offset, note.Rest(quarterLength=0.5))
offset += 0.5
organ_part.insert(offset, chord.Chord(organ_chord, quarterLength=0.5))
offset += 0.5
organ_part.insert(offset, note.Rest(quarterLength=0.5))
offset += 0.5
# WRONG - Using .append() causes 8-minute tracks when mixed with .insert() drums
# organ_part.append(chord.Chord(organ_chord, quarterLength=0.5)) # ❌ Causes timing bug!Reggae Instruments (MIDI Programs):
- Drums: Channel 9 (MIDI channel 10) - ALWAYS required
- Bass: Program 33 (Electric Bass - finger) or 34 (Electric Bass - pick)
- Guitar:
- PRIMARY: Program 25 (Acoustic Guitar - steel) - Bright, percussive, cuts through mix
- Alternative: Program 28 (Electric Guitar - muted) - Percussive skank sound
- ⚠️ AVOID: Program 27 (Electric Guitar - clean) - Recorded 12-15dB quieter in FluidR3_GM, will be inaudible even at velocity 95
- Organ: Program 16 (Drawbar Organ) or 17 (Percussive Organ)
Setting Instruments with mido (CRITICAL):
Since reggae Parts don't use music21 instrument classes, you MUST use mido to INSERT program_change messages:
from mido import MidiFile, Message
# After score.write('midi', fp=midi_path)
mid = MidiFile(midi_path)
for i, track in enumerate(mid.tracks):
if i == 1: # Drums track
for msg in track:
if hasattr(msg, 'channel'):
msg.channel = 9 # Drums on channel 9
elif i == 2: # Bass track
# INSERT program_change message (don't try to modify - it doesn't exist!)
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=33, time=0))
elif i == 3: # Guitar track
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=25, time=0)) # Acoustic steel - bright and audible
elif i == 4: # Organ track
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=16, time=0))
mid.save(midi_path)Mixing and Balance (CRITICAL - Guitar Will Be Inaudible Without This!):
Setting the correct instrument programs and velocities is NOT enough. In reggae, the guitar will still be completely inaudible if you don't address THREE issues:
1. Soundfont level: Program 27 (Electric Guitar - clean) recorded 12-15dB quieter than program 16 (Drawbar Organ) in FluidR3_GM 2. Note duration: 0.25 quarterLength = 183ms @ 82 BPM (below perceptual threshold) 3. Velocity difference: Need 40+ point separation
Complete Solution:
def set_track_velocity(track, velocity):
"""Set velocity for all note_on messages in a track."""
for msg in track:
if msg.type == 'note_on' and msg.velocity > 0:
msg.velocity = velocity
# After setting instruments, BEFORE saving
for i, track in enumerate(mid.tracks):
if i == 1: # Drums
set_track_velocity(track, 70)
elif i == 2: # Bass - prominent in reggae
set_track_velocity(track, 80)
elif i == 3: # Guitar - RHYTHM INSTRUMENT, needs to cut through
# Use program 25 (steel acoustic) instead of 27 (too quiet)
# Use 0.4 quarterLength instead of 0.25 (too short)
set_track_velocity(track, 95) # LOUD - this is critical!
elif i == 4: # Organ - BACKGROUND atmosphere
set_track_velocity(track, 55) # QUIET - don't overpower guitar
mid.save(midi_path)Why this THREE-PART solution works:
- Program 25 (Acoustic Guitar - steel): Recorded 8-10dB louder than program 27, bright harmonics, percussive attack
- Duration 0.4 (293ms): Crosses perceptual threshold vs 0.25 (183ms) which is too short
- Velocity 95 vs 55: 40-point difference creates clear separation
- Combined effect: Guitar now has 3× more presence (program × duration × velocity)
Tempo: 70-90 BPM (classic roots reggae: 80-85 BPM, modern: 85-90 BPM)
Common Mistakes:
- Creating
drums_partbut callingdrums.insert()- use correct variable name - Trying to MODIFY
program_changemessages that don't exist - must INSERT them - Not setting drums to channel 9 - drums will sound like melody notes
- CRITICAL: Using program 27 (too quiet) instead of program 25
- CRITICAL: Using 0.25 quarterLength (too short) instead of 0.4
- CRITICAL: Not setting velocities - guitar will be completely inaudible, organ will dominate
House Specific Guidelines
CRITICAL: House requires extreme repetition and minimal variation to create the hypnotic, groovy feel. Standard composition rules don't apply.
Bass Rules (Non-Negotiable):
- ONE NOTE held for 8-16 bars minimum (32.0-64.0 quarterLength)
- Octave 1-2 RANGE (A1, C2, E2, F1, G1) - the actual bass guitar range (55-110 Hz)
- NEVER use octave 0 (A0, F0, G0, etc.) - these are 20-30 Hz sub-sonic frequencies inaudible on 99% of playback systems (laptop speakers, headphones, even many studio monitors can't reproduce them)
- Whole notes or longer (4.0+ quarterLength minimum, prefer 32.0+)
- Simple patterns: Root for 8 bars → Fifth for 8 bars → repeat
- NO octave jumps, NO busy basslines, NO quarter notes
# CORRECT House Bass (audible on all playback systems)
bass_pattern = [
('A1', 32.0), # A for 8 bars - 55 Hz (bass guitar's lowest note)
('E2', 32.0), # E for 8 bars - 82 Hz (bass guitar's open E string)
('F1', 32.0), # F for 8 bars - 43.7 Hz
('C2', 32.0), # C for 8 bars - 65.4 Hz
]
for pitch, duration in bass_pattern:
bass_part.append(note.Note(pitch, quarterLength=duration))
# WRONG - Octave 0 is inaudible on most systems!
bass_notes = ['A0', 'F0', 'C1', 'G0'] # ❌ 20-35 Hz - below hearing/speaker range!
for bar in range(8):
bass_part.append(note.Note(bass_notes[bar % 4], quarterLength=32.0)) # ❌ Inaudible!
# ALSO WRONG - Too busy, octave jumps
bass_notes = ['A1', 'A2', 'C2', 'F1'] # ❌ Octave jumps
for bar in range(32):
bass_part.append(note.Note(bass_notes[bar % 4], quarterLength=1.0)) # ❌ Too short!Pad Rules:
- ONE CHORD held for 8-16 bars (32.0-64.0 quarterLength)
- Mid-range octaves (2-4): A2, C3, E3 voicings
- Long attack/release for smooth transitions
- Change chords rarely (every 8-16 bars, not every 4 bars)
# CORRECT House Pads
pad_progression = [
(['A2', 'C3', 'E3'], 64.0), # Am for 16 bars
(['F2', 'A2', 'C3'], 64.0), # F for 16 bars
]
for chord_notes, duration in pad_progression:
pad_part.append(chord.Chord(chord_notes, quarterLength=duration))Lead Rules:
- Sparse - only play every 4-8 bars, lots of silence
- Long notes (4.0-16.0 quarterLength)
- Enter late (bar 16+, not immediately)
- Mid octaves (A4, C5, E5 max)
# CORRECT House Lead (enters bar 16)
lead_pattern = [
('A4', 8.0), # 2 bars
('C5', 8.0), # 2 bars
('E5', 16.0), # 4 bars
]Core Principle: If it feels repetitive, you're doing it right. House = hypnotic loop repeated for minutes with minimal changes.
Mixing and Balance:
In House, the bass is the star. But if you don't control velocities, the pads and leads will overpower everything:
def set_track_velocity(track, velocity):
"""Set velocity for all note_on messages in a track."""
for msg in track:
if msg.type == 'note_on' and msg.velocity > 0:
msg.velocity = velocity
# After setting instruments with mido
for i, track in enumerate(mid.tracks):
if i == 1: # Drums
set_track_velocity(track, 90) # Driving rhythm
elif i == 2: # Bass (program 38, octaves 1-2)
set_track_velocity(track, 75) # Prominent but not overpowering
elif i == 3: # Pad (program 88, octaves 2-4)
set_track_velocity(track, 50) # Atmospheric background
elif i == 4: # Lead (program 80, octaves 4-5)
set_track_velocity(track, 95) # Melodic focus (when present)
mid.save(midi_path)Bassline Patterns
# Groovy syncopated house bass
bass_pattern = [
('A1', 1.0), # Downbeat
('A1', 0.5), # Short hit
('rest', 0.25), # Space
('A1', 0.25), # Syncopation
('A2', 0.5), # Octave jump
('C2', 0.5), # Chord tone
('A1', 1.0) # Resolution
]
for pitch, duration in bass_pattern:
if pitch == 'rest':
bass_part.append(note.Rest(quarterLength=duration))
else:
bass_part.append(note.Note(pitch, quarterLength=duration))Chord Voicings
# Jazz voicing (7th chords)
jazz_chords = [
['C4', 'E4', 'G4', 'B4'], # Cmaj7
['D4', 'F4', 'A4', 'C5'], # Dm7
['G3', 'B3', 'D4', 'F4'] # G7
]
# House pad voicing (open, atmospheric)
house_pads = [
['A3', 'C4', 'E4'], # Am
['F3', 'A3', 'C4'], # F
['C3', 'E3', 'G3'] # C
]
# Classical voicing (close position)
classical_chords = [
['C4', 'E4', 'G4'], # C major
['B3', 'D4', 'G4'], # G major
['C4', 'F4', 'A4'] # F major
]Melody Construction
# Pentatonic scale (versatile, no "wrong" notes)
pentatonic_c = ['C', 'D', 'E', 'G', 'A']
# Major scale
major_c = ['C', 'D', 'E', 'F', 'G', 'A', 'B']
# Minor scale
minor_a = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
# Blues scale
blues_c = ['C', 'Eb', 'F', 'F#', 'G', 'Bb']
# Generate melody algorithmically
for i in range(16):
octave = 4 + (i // 8) # Move up octave halfway through
scale_degree = i % len(pentatonic_c)
pitch = pentatonic_c[scale_degree] + str(octave)
melody.append(note.Note(pitch, quarterLength=0.5))Dynamic Control (Crescendos, Volume Changes)
from music21 import dynamics
# Set initial volume
part.insert(0, dynamics.Dynamic('p')) # Piano (soft)
# Add crescendo at bar 8
part.insert(32, dynamics.Crescendo()) # 32 quarter notes = 8 bars
# Peak at bar 10
part.insert(40, dynamics.Dynamic('ff')) # Fortissimo (very loud)
# Decrescendo
part.insert(60, dynamics.Diminuendo())
# Return to soft
part.insert(72, dynamics.Dynamic('p'))
# Dynamic markings: ppp, pp, p, mp, mf, f, ff, fffTiming & Tempo
# Set tempo (BPM)
part.insert(0, tempo.MetronomeMark(number=120)) # 120 BPM
# Tempo changes
part.insert(32, tempo.MetronomeMark(number=140)) # Speed up at bar 8
# Timing variations (humanization)
import random
note.Note('C5', quarterLength=1.0 + random.uniform(-0.05, 0.05))
# Common time signatures (set on first part)
from music21 import meter
part.insert(0, meter.TimeSignature('4/4')) # Most common
part.insert(0, meter.TimeSignature('3/4')) # Waltz
part.insert(0, meter.TimeSignature('6/8')) # Compound meterComplete Example: Deep House Track
from music21 import stream, note, chord, tempo
from mido import MidiFile
import subprocess
# 1. Compose with music21
score = stream.Score()
drums = stream.Part()
bass_part = stream.Part()
pad_part = stream.Part()
lead_part = stream.Part()
# Set tempo
drums.insert(0, tempo.MetronomeMark(number=122))
# Add drums using .insert() for proper layering (not .append()!)
bars = 32
beats_per_bar = 4
# Layer 1: Four-on-the-floor kicks
for bar in range(bars):
for beat in range(beats_per_bar):
offset = float(bar * beats_per_bar + beat)
drums.insert(offset, note.Note(36, quarterLength=1.0))
# Layer 2: Snare on beats 2 and 4 of each bar
for bar in range(bars):
drums.insert(float(bar * beats_per_bar + 1), note.Note(38, quarterLength=1.0)) # Beat 2
drums.insert(float(bar * beats_per_bar + 3), note.Note(38, quarterLength=1.0)) # Beat 4
# Layer 3: Hi-hats on eighth notes (offbeat open hats for groove)
for bar in range(bars):
for eighth in range(8):
offset = float(bar * beats_per_bar) + (eighth * 0.5)
if eighth % 2 == 0:
drums.insert(offset, note.Note(42, quarterLength=0.5)) # Closed hat
else:
drums.insert(offset, note.Note(46, quarterLength=0.5)) # Open hat (offbeat)
# House BASS: Long sustained notes using .insert() (NOT .append())
bass_offset = 0.0
bass_pattern = [
('A1', 32.0), # A root for 8 bars - 55 Hz (audible on all systems)
('E2', 32.0), # E fifth for 8 bars - 82 Hz
('F1', 32.0), # F for 8 bars - 43.7 Hz
('C2', 32.0), # C for 8 bars - 65.4 Hz
]
for pitch, duration in bass_pattern:
bass_part.insert(bass_offset, note.Note(pitch, quarterLength=duration))
bass_offset += duration
# House PADS: Long sustained chords using .insert() (NOT .append())
pad_offset = 0.0
pad_progression = [
(['A2', 'C3', 'E3'], 64.0), # Am for 16 bars
(['F2', 'A2', 'C3'], 64.0), # F for 16 bars
]
for chord_notes, duration in pad_progression:
pad_part.insert(pad_offset, chord.Chord(chord_notes, quarterLength=duration))
pad_offset += duration
# House LEAD: Sparse, long notes, enters late using .insert() (NOT .append())
lead_pattern = [
('A4', 8.0), # 2 bars
('C5', 8.0), # 2 bars
('E5', 16.0), # 4 bars (long sustain)
]
# Lead enters at bar 16 (64 beats in)
lead_offset = 64.0
for pitch, duration in lead_pattern:
lead_part.insert(lead_offset, note.Note(pitch, quarterLength=duration))
lead_offset += duration
score.append(drums)
score.append(bass_part)
score.append(pad_part)
score.append(lead_part)
midi_path = '/mnt/user-data/outputs/deep_house_track.mid'
score.write('midi', fp=midi_path)
# 2. Fix instruments with mido (INSERT program_change messages)
from mido import Message
mid = MidiFile(midi_path)
for i, track in enumerate(mid.tracks):
if i == 1: # Drums track
for msg in track:
if hasattr(msg, 'channel'):
msg.channel = 9 # Drums must be on channel 9
elif i == 2: # Bass track - INSERT program_change
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=38, time=0))
elif i == 3: # Pad track - INSERT program_change
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=88, time=0))
elif i == 4: # Lead track - INSERT program_change
insert_pos = 0
for j, msg in enumerate(track):
if msg.type == 'track_name':
insert_pos = j + 1
break
track.insert(insert_pos, Message('program_change', program=81, time=0))
mid.save(midi_path)
# 3. Render with ELECTRONIC pipeline with deep_house preset!
mp3_path = '/mnt/user-data/outputs/deep_house_track.mp3'
result = subprocess.run([
'python',
'/mnt/skills/private/music-generation/scripts/render_electronic.py',
midi_path,
mp3_path,
'--genre', 'deep_house'
], capture_output=True, text=True)
print(result.stdout)
if result.returncode == 0:
print(f"✓ House track created: {mp3_path}")
else:
print(f"Error: {result.stderr}")House Characteristics in This Example:
- Bass: ONE note (A1, E2, F1, C2) held for 8 bars each - deep bass in octave 1-2 (55-82 Hz range, audible on all playback systems)
- Pads: ONE chord held for 16 bars - extreme sustain creates hypnotic atmosphere
- Lead: Sparse (enters bar 16), long notes (2-4 bars each) - not busy
- Repetition: Minimal variation = hypnotic, groovy House feel
- Uses
render_electronic.pywithdeep_housepreset for warm, subby synthesis - Bass synthesized with proper low-end frequencies that consumer audio equipment can reproduce
- Pads use slow attack (0.9s) and long release (1.2s) for smooth transitions
- Genre preset automatically tunes all synthesis parameters
- No external samples or soundfonts needed - fully self-contained
Best Practices
Composition Quality
- Generate variety: Don't repeat the same 4 bars for entire piece
- Use music theory: Real chord progressions, proper voice leading
- Respect instrument ranges: Violin (G3-E7), Cello (C2-C6), Trumpet (E3-C6)
- Add dynamics: Use p, mp, mf, f, ff markings and crescendos
- Structure: Intro → Development → Climax → Resolution
- Add humanization: Vary timing and velocity to avoid robotic sound
import random
n = note.Note('C5', quarterLength=1.0 + random.uniform(-0.05, 0.05))
n.volume.velocity = 80 + random.randint(-5, 5)Technical Quality
- SoundFont: Use FluidR3_GM.sf2 for best quality
- Bitrate: 192kbps minimum, 320kbps for high quality
- Timing precision: Use quarterLength values carefully
- Cleanup: Remove temporary MIDI/WAV files after MP3 conversion
Common Pitfalls
- CRITICAL: Always use .insert() for ALL tracks - Never mix
.insert()and.append(). See "Drum Programming" section for details - CRITICAL: INSERT program_change messages - Use
track.insert(pos, Message('program_change', ...))notmsg.program = X. See mido Quick Reference - CRITICAL: Set velocities - Lead 90-105, background 50-65. See "Mixing and Balance" section
- CRITICAL: Bass octaves - Use A1-A2 (55-110 Hz), never A0-G0 (inaudible on most systems)
- instrument.Cello() doesn't exist - use
Violoncello() - Forgetting tempo - Add
tempo.MetronomeMark()to first part - Drums not sounding like drums - Set channel to 9 with mido (see mido Quick Reference)
Mixing and Balance
CRITICAL: Setting MIDI program numbers alone is not enough. Without explicit velocity control, some instruments will be completely inaudible.
Setting Velocities
music21 uses default velocity 64 for all notes, which causes poor mixing. Use mido to set velocities after MIDI export:
from mido import MidiFile
def set_track_velocity(track, velocity):
"""Set velocity for all note_on messages in a track."""
for msg in track:
if msg.type == 'note_on' and msg.velocity > 0:
msg.velocity = velocity
mid = MidiFile(midi_path)
for i, track in enumerate(mid.tracks):
if i == 1: # Drums
set_track_velocity(track, 75)
elif i == 2: # Bass
set_track_velocity(track, 80)
elif i == 3: # Lead instrument (sax, guitar, trumpet)
set_track_velocity(track, 95)
elif i == 4: # Background (organ, pads)
set_track_velocity(track, 55)
mid.save(midi_path)Velocity Guidelines
By Role:
- Lead instruments (melody, solos): 90-105
- Rhythm instruments (guitar skanks, comping): 85-100
- Bass: 75-85
- Drums: 70-90
- Background (pads, organs): 50-65
By Frequency Range:
- Low (20-250 Hz): Bass, kick - only ONE dominant at 75-85
- Mid (250-2000 Hz): Most crowded - use velocity to separate (lead 90+, background 50-65)
- High (2000+ Hz): Hi-hats, cymbals - 70-85 for clarity without harshness
Soundfont Level Issues
FluidR3_GM instruments are recorded at different levels. Even with correct velocities, some instruments may be inaudible:
Quiet programs (avoid for lead/rhythm):
- Program 27 (Electric Guitar - clean) - Very quiet
- Program 24 (Acoustic Guitar - nylon)
- Program 73 (Flute)
Better alternatives:
- Program 25 (Acoustic Guitar - steel) - 8-10dB louder, cuts through
- Program 28 (Electric Guitar - muted) - Percussive
- Program 30 (Distortion Guitar) - Aggressive
Additional fixes:
- Increase note duration (0.4 quarterLength minimum vs 0.25)
- Use octave separation (move competing instruments to different octaves)
- Extreme velocity contrast (quiet instrument at 110, loud at 40)
Mixing Checklist
Before rendering:
- ✅ Lead at velocity 90-105
- ✅ Background at velocity 50-65
- ✅ Bass at velocity 75-85
- ✅ Check for quiet instruments (programs 24, 27, 73) and use alternatives
- ✅ Minimum 0.4 quarterLength for rhythm instruments
Resources
- music21 Documentation: https://web.mit.edu/music21/doc/
- General MIDI Spec: https://www.midi.org/specifications-old/item/gm-level-1-sound-set
- Music Theory: https://www.musictheory.net/
- IMSLP (Free Scores): https://imslp.org/ - Download classical MIDIs here!
Limitations
- Instrumental only - No lyrics/vocals
- MIDI-based synthesis - Not studio-quality recordings
- No real-time playback - Files must be rendered before playback
- SoundFont quality - Good but not as realistic as sample libraries
When to Use This Skill
✅ User requests:
- Original compositions with specific moods/styles
- Classical music in MP3 format
- Timed music for videos/presentations
- Specific instrumentation (orchestral, piano, strings, etc.)
- Dynamic music with crescendos, tempo changes
❌ Not suitable for:
- Vocal/lyrical music
- Audio mixing/mastering (reverb, EQ, compression)
- Real-time MIDI playback
- Professional studio recording quality
#!/bin/bash
echo "====================================="
echo "Music Generation Skill Installation"
echo "====================================="
echo ""
# Update package list
echo "Updating package list..."
apt-get update -qq
# Install FluidSynth and SoundFonts
echo "Installing FluidSynth and SoundFonts..."
apt-get install -y fluidsynth fluid-soundfont-gm fluid-soundfont-gs
# Install FFmpeg for audio conversion
echo "Installing FFmpeg..."
apt-get install -y ffmpeg
# Install Python dependencies
echo "Installing Python dependencies..."
pip install --quiet --upgrade pip
pip install --quiet -r requirements.txt
# Verify installations
echo ""
echo "====================================="
echo "Verification"
echo "====================================="
# Check FluidSynth
if command -v fluidsynth &> /dev/null; then
echo "✓ FluidSynth installed successfully"
else
echo "✗ FluidSynth installation failed"
fi
# Check FFmpeg
if command -v ffmpeg &> /dev/null; then
echo "✓ FFmpeg installed successfully"
else
echo "✗ FFmpeg installation failed"
fi
# Check SoundFont files
if [ -f "/usr/share/sounds/sf2/FluidR3_GM.sf2" ] || [ -f "/usr/share/sounds/sf2/default.sf2" ]; then
echo "✓ SoundFont files found"
ls -lh /usr/share/sounds/sf2/*.sf2 2>/dev/null || ls -lh /usr/share/soundfonts/*.sf2
else
echo "✗ SoundFont files not found"
fi
# Check Python packages
echo ""
python3 -c "import music21; print('✓ music21 version:', music21.__version__)"
python3 -c "import midi2audio; print('✓ midi2audio installed')"
python3 -c "import pydub; print('✓ pydub installed')"
echo ""
echo "====================================="
echo "Installation Complete!"
echo "====================================="
echo ""
echo "You can now run: python music_generator.py"
echo "Or import the module in your own code"
music21>=9.1.0
midi2audio>=0.1.1
pydub>=0.25.1
mido>=1.3.0
numpy>=1.24.0
scipy>=1.10.0
"""Generic music operation scripts for musicgeneration skill."""#!/usr/bin/env python3
"""
Validate audio quality and detect issues in MP3/WAV files.
This script performs quality control checks on any audio file:
- Audio clipping detection (peaks > 0dB)
- Silence detection (empty sections)
- Duration verification
- Abrupt volume changes
- Dynamic range analysis
Usage:
python audio_validate.py output.mp3 [--expected-duration 90]
"""
import argparse
import sys
from pathlib import Path
import numpy as np
from pydub import AudioSegment
from pydub.utils import db_to_float
class ValidationResult:
def __init__(self):
self.passed = True
self.errors = []
self.warnings = []
def add_error(self, message: str):
self.errors.append(message)
self.passed = False
def add_warning(self, message: str):
self.warnings.append(message)
def print_results(self):
if self.passed and not self.warnings:
print("\n✓ VALIDATION PASSED - No issues detected")
return
if self.errors:
print("\n✗ VALIDATION FAILED")
print("\nErrors (must fix):")
for error in self.errors:
print(f" ✗ {error}")
if self.warnings:
print("\nWarnings (should review):")
for warning in self.warnings:
print(f" ⚠ {warning}")
if not self.errors:
print("\n⚠ VALIDATION PASSED WITH WARNINGS")
def validate_audio(audio_path: Path, expected_duration: float = None) -> ValidationResult:
"""Perform comprehensive audio validation."""
result = ValidationResult()
try:
audio = AudioSegment.from_file(str(audio_path))
except Exception as e:
result.add_error(f"Failed to load audio file: {e}")
return result
actual_duration = len(audio) / 1000.0
if expected_duration:
duration_diff = abs(actual_duration - expected_duration)
if duration_diff > 2.0:
result.add_error(
f"Duration mismatch: expected {expected_duration:.1f}s, got {actual_duration:.1f}s "
f"(diff: {duration_diff:.1f}s)"
)
elif duration_diff > 0.5:
result.add_warning(
f"Duration slightly off: expected {expected_duration:.1f}s, got {actual_duration:.1f}s "
f"(diff: {duration_diff:.1f}s)"
)
max_db = audio.max_dBFS
if max_db > -0.1:
result.add_error(
f"Audio clipping detected: peak level is {max_db:.1f} dBFS "
f"(should be below -0.1 dBFS to avoid distortion)"
)
elif max_db > -1.0:
result.add_warning(
f"Audio very loud: peak level is {max_db:.1f} dBFS "
f"(recommended: -3 to -6 dBFS for headroom)"
)
rms_db = audio.dBFS
if rms_db < -40.0:
result.add_warning(
f"Audio very quiet: RMS level is {rms_db:.1f} dBFS "
f"(recommended: -15 to -20 dBFS)"
)
silent_threshold = -50.0
chunk_length = 1000
silent_chunks = 0
total_chunks = len(audio) // chunk_length
for i in range(0, len(audio), chunk_length):
chunk = audio[i:i + chunk_length]
if chunk.dBFS < silent_threshold:
silent_chunks += 1
silence_percent = (silent_chunks / total_chunks) * 100 if total_chunks > 0 else 0
if silence_percent > 50:
result.add_error(
f"Excessive silence detected: {silence_percent:.1f}% of audio is silent "
f"(threshold: {silent_threshold} dBFS)"
)
elif silence_percent > 20:
result.add_warning(
f"Significant silence: {silence_percent:.1f}% of audio is silent"
)
window_size = 2000
volumes = []
for i in range(0, len(audio) - window_size, window_size // 2):
chunk = audio[i:i + window_size]
volumes.append(chunk.dBFS)
if len(volumes) > 1:
volume_changes = [abs(volumes[i+1] - volumes[i]) for i in range(len(volumes)-1)]
max_change = max(volume_changes) if volume_changes else 0
if max_change > 20:
result.add_warning(
f"Abrupt volume change detected: {max_change:.1f} dB jump "
f"(may sound jarring to listeners)"
)
dynamic_range = max_db - rms_db
if dynamic_range < 3:
result.add_warning(
f"Limited dynamic range: {dynamic_range:.1f} dB "
f"(audio may sound compressed or lifeless)"
)
print(f"\nAudio Analysis:")
print(f" Duration: {actual_duration:.2f}s")
print(f" Peak level: {max_db:.1f} dBFS")
print(f" RMS level: {rms_db:.1f} dBFS")
print(f" Dynamic range: {dynamic_range:.1f} dB")
print(f" Silence: {silence_percent:.1f}%")
print(f" Channels: {audio.channels}")
print(f" Sample rate: {audio.frame_rate} Hz")
print(f" Bit depth: {audio.sample_width * 8} bit")
return result
def main():
parser = argparse.ArgumentParser(
description="Validate audio quality and detect issues",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python audio_validate.py output.mp3
Check audio file for quality issues
python audio_validate.py output.mp3 --expected-duration 90
Verify duration matches expected 90 seconds
python audio_validate.py output.wav --expected-duration 60
Validate WAV file with expected duration
Checks performed:
- Audio clipping (peaks > 0dB)
- Excessive silence
- Duration accuracy
- Abrupt volume changes
- Dynamic range
- Overall loudness levels
"""
)
parser.add_argument("input", help="Input audio file (MP3, WAV, etc.)")
parser.add_argument(
"--expected-duration",
type=float,
help="Expected duration in seconds (for verification)"
)
args = parser.parse_args()
input_path = Path(args.input)
if not input_path.exists():
print(f"Error: Input file not found: {args.input}")
sys.exit(1)
print(f"Validating: {args.input}")
result = validate_audio(input_path, args.expected_duration)
result.print_results()
sys.exit(0 if result.passed else 1)
if __name__ == "__main__":
main()"""
Real-time drum synthesis for electronic music.
Synthesizes 808-style kicks, snares, hi-hats, and other percussion
on-the-fly without requiring external samples. All parameters are tunable
for different genres and styles.
"""
import numpy as np
from scipy import signal
from scipy.io import wavfile
def synthesize_kick(
pitch=56.0,
decay=0.5,
punch=0.8,
click_level=0.3,
sample_rate=44100
) -> np.ndarray:
"""
Synthesize an 808-style kick drum.
Args:
pitch: Fundamental frequency in Hz (808 default: 56Hz)
decay: Decay time in seconds (shorter = tighter)
punch: Amount of pitch sweep (0-1, higher = more punch)
click_level: Attack click amount (0-1)
sample_rate: Audio sample rate
Returns:
16-bit mono audio array
"""
duration = max(decay * 2, 0.5)
t = np.linspace(0, duration, int(sample_rate * duration))
freq_start = pitch * (1 + punch * 3)
freq_end = pitch
freq = np.linspace(freq_start, freq_end, len(t))
phase = 2 * np.pi * np.cumsum(freq) / sample_rate
body = np.sin(phase)
envelope_body = np.exp(-8 * t / decay)
body = body * envelope_body
click_freq = 2000
click = np.sin(2 * np.pi * click_freq * t)
envelope_click = np.exp(-100 * t)
click = click * envelope_click * click_level
kick = body + click
kick = kick / np.max(np.abs(kick)) * 0.85
return (kick * 32767).astype(np.int16)
def synthesize_snare(
tone_mix=0.3,
pitch=200.0,
snap=0.7,
decay=0.25,
sample_rate=44100
) -> np.ndarray:
"""
Synthesize an electronic snare drum.
Args:
tone_mix: Balance between tone and noise (0=pure noise, 1=pure tone)
pitch: Fundamental frequency of tone in Hz
snap: Brightness/sharpness of attack (0-1)
decay: Decay time in seconds
sample_rate: Audio sample rate
Returns:
16-bit mono audio array
"""
duration = max(decay * 2, 0.3)
t = np.linspace(0, duration, int(sample_rate * duration))
pink_noise = np.random.randn(len(t))
b, a = signal.butter(1, 0.5, btype='high')
noise = signal.filtfilt(b, a, pink_noise)
tone1 = np.sin(2 * np.pi * pitch * t)
tone2 = np.sin(2 * np.pi * (pitch * 1.3) * t)
tone = (tone1 + tone2 * 0.7) / 1.7
snare = noise * (1 - tone_mix) + tone * tone_mix
envelope = np.exp(-10 * t / decay)
snare = snare * envelope
if snap > 0:
attack = np.exp(-80 * t) * snap
snare = snare + attack * np.random.randn(len(t)) * 0.5
snare = snare / np.max(np.abs(snare)) * 0.7
return (snare * 32767).astype(np.int16)
def synthesize_hat_closed(
brightness=0.7,
decay=0.12,
metallic=0.5,
sample_rate=44100
) -> np.ndarray:
"""
Synthesize a closed hi-hat.
Args:
brightness: Frequency content (0=dark, 1=bright)
decay: Decay time in seconds
metallic: Amount of metallic overtones (0-1)
sample_rate: Audio sample rate
Returns:
16-bit mono audio array
"""
duration = max(decay * 2, 0.15)
t = np.linspace(0, duration, int(sample_rate * duration))
noise = np.random.randn(len(t))
cutoff_low = 0.2 + brightness * 0.3
cutoff_high = 0.5 + brightness * 0.4
b, a = signal.butter(2, [cutoff_low, cutoff_high], btype='band')
hat = signal.filtfilt(b, a, noise)
if metallic > 0:
freqs = [8000, 10500, 13000, 15500]
for freq in freqs:
metallic_tone = np.sin(2 * np.pi * freq * t)
hat = hat + metallic_tone * metallic * 0.1
envelope = np.exp(-50 * t / decay)
hat = hat * envelope
hat = hat / np.max(np.abs(hat)) * 0.45
return (hat * 32767).astype(np.int16)
def synthesize_hat_open(
brightness=0.8,
decay=0.4,
metallic=0.6,
sample_rate=44100
) -> np.ndarray:
"""
Synthesize an open hi-hat.
Args:
brightness: Frequency content (0=dark, 1=bright)
decay: Decay time in seconds (longer than closed)
metallic: Amount of metallic overtones (0-1)
sample_rate: Audio sample rate
Returns:
16-bit mono audio array
"""
duration = max(decay * 1.5, 0.4)
t = np.linspace(0, duration, int(sample_rate * duration))
noise = np.random.randn(len(t))
cutoff_low = 0.25 + brightness * 0.25
cutoff_high = 0.6 + brightness * 0.3
b, a = signal.butter(2, [cutoff_low, cutoff_high], btype='band')
hat = signal.filtfilt(b, a, noise)
if metallic > 0:
freqs = [7500, 9000, 11000, 13500]
for freq in freqs:
metallic_tone = np.sin(2 * np.pi * freq * t)
hat = hat + metallic_tone * metallic * 0.12
envelope = np.exp(-8 * t / decay)
hat = hat * envelope
hat = hat / np.max(np.abs(hat)) * 0.4
return (hat * 32767).astype(np.int16)
def synthesize_clap(
room_size=0.5,
density=0.7,
brightness=0.6,
sample_rate=44100
) -> np.ndarray:
"""
Synthesize a hand clap.
Args:
room_size: Simulated room size (0=tight, 1=large)
density: Number of clap layers (0=sparse, 1=dense)
brightness: Frequency content (0=dark, 1=bright)
sample_rate: Audio sample rate
Returns:
16-bit mono audio array
"""
duration = 0.2 + room_size * 0.3
t = np.linspace(0, duration, int(sample_rate * duration))
noise = np.random.randn(len(t))
cutoff_low = 0.1 + brightness * 0.15
cutoff_high = 0.3 + brightness * 0.3
b, a = signal.butter(2, [cutoff_low, cutoff_high], btype='band')
clap = signal.filtfilt(b, a, noise)
burst_count = int(3 + density * 3)
burst_times = [0.0]
for i in range(1, burst_count):
burst_times.append(0.005 + i * 0.008 * (1 - density * 0.5))
envelope = np.zeros_like(t)
for bt in burst_times:
burst_env = np.exp(-100 * (t - bt))
burst_env[t < bt] = 0
envelope += burst_env
clap = clap * envelope
if room_size > 0:
tail = np.exp(-15 * t) * room_size * 0.3
clap = clap + tail * np.random.randn(len(t))
clap = clap / np.max(np.abs(clap)) * 0.6
return (clap * 32767).astype(np.int16)
def synthesize_rim(
pitch=800.0,
decay=0.08,
click_mix=0.7,
sample_rate=44100
) -> np.ndarray:
"""
Synthesize a rim shot / side stick.
Args:
pitch: Resonant frequency in Hz
decay: Decay time in seconds
click_mix: Balance between click and tone (0=pure tone, 1=pure click)
sample_rate: Audio sample rate
Returns:
16-bit mono audio array
"""
duration = max(decay * 2, 0.1)
t = np.linspace(0, duration, int(sample_rate * duration))
tone = np.sin(2 * np.pi * pitch * t)
envelope_tone = np.exp(-40 * t / decay)
tone = tone * envelope_tone
click = np.random.randn(len(t))
b, a = signal.butter(1, 0.6, btype='high')
click = signal.filtfilt(b, a, click)
envelope_click = np.exp(-100 * t)
click = click * envelope_click
rim = tone * (1 - click_mix) + click * click_mix
rim = rim / np.max(np.abs(rim)) * 0.5
return (rim * 32767).astype(np.int16)
def save_drum_sample(audio_array: np.ndarray, filepath: str, sample_rate=44100):
"""Save synthesized drum audio to WAV file."""
wavfile.write(filepath, sample_rate, audio_array)"""
Real-time melodic synthesis for electronic music.
Synthesizes bass, pads, and leads with ADSR envelopes, filters,
and modulation. All parameters tunable for different genres.
"""
import numpy as np
from scipy import signal
def apply_adsr_envelope(
audio: np.ndarray,
attack=0.01,
decay=0.1,
sustain=0.7,
release=0.2,
sample_rate=44100
) -> np.ndarray:
"""
Apply ADSR envelope to audio signal.
Args:
audio: Input audio array
attack: Attack time in seconds
decay: Decay time in seconds
sustain: Sustain level (0-1)
release: Release time in seconds
sample_rate: Audio sample rate
Returns:
Audio with ADSR envelope applied
"""
total_samples = len(audio)
duration = total_samples / sample_rate
attack_samples = int(attack * sample_rate)
decay_samples = int(decay * sample_rate)
release_samples = int(release * sample_rate)
total_adsr_samples = attack_samples + decay_samples + release_samples
if total_adsr_samples > total_samples:
scale_factor = total_samples / total_adsr_samples
attack_samples = int(attack_samples * scale_factor)
decay_samples = int(decay_samples * scale_factor)
release_samples = int(release_samples * scale_factor)
sustain_samples = total_samples - attack_samples - decay_samples - release_samples
sustain_samples = max(sustain_samples, 0)
envelope = np.zeros(total_samples)
attack_end = min(attack_samples, total_samples)
if attack_end > 0:
envelope[:attack_end] = np.linspace(0, 1, attack_end)
decay_start = attack_end
decay_end = min(decay_start + decay_samples, total_samples)
if decay_end > decay_start:
envelope[decay_start:decay_end] = np.linspace(1, sustain, decay_end - decay_start)
sustain_start = decay_end
sustain_end = min(sustain_start + sustain_samples, total_samples)
if sustain_end > sustain_start:
envelope[sustain_start:sustain_end] = sustain
release_start = sustain_end
remaining_samples = total_samples - release_start
if remaining_samples > 0:
envelope[release_start:] = np.linspace(sustain, 0, remaining_samples)
return audio * envelope
def apply_lowpass_filter(
audio: np.ndarray,
cutoff_hz=1000,
resonance=0.5,
sample_rate=44100
) -> np.ndarray:
"""
Apply low-pass filter with resonance.
Args:
audio: Input audio array
cutoff_hz: Cutoff frequency in Hz
resonance: Filter resonance (0-1)
sample_rate: Audio sample rate
Returns:
Filtered audio
"""
nyquist = sample_rate / 2
normalized_cutoff = cutoff_hz / nyquist
normalized_cutoff = np.clip(normalized_cutoff, 0.01, 0.99)
order = 2 + int(resonance * 2)
b, a = signal.butter(order, normalized_cutoff, btype='low')
filtered = signal.filtfilt(b, a, audio)
return filtered
def apply_highpass_filter(
audio: np.ndarray,
cutoff_hz=200,
sample_rate=44100
) -> np.ndarray:
"""
Apply high-pass filter to remove low frequencies.
Args:
audio: Input audio array
cutoff_hz: Cutoff frequency in Hz
sample_rate: Audio sample rate
Returns:
Filtered audio
"""
nyquist = sample_rate / 2
normalized_cutoff = cutoff_hz / nyquist
normalized_cutoff = np.clip(normalized_cutoff, 0.01, 0.99)
b, a = signal.butter(2, normalized_cutoff, btype='high')
filtered = signal.filtfilt(b, a, audio)
return filtered
def synthesize_bass_note(
frequency=55.0,
duration=1.0,
waveform='sawtooth',
cutoff=200,
resonance=0.6,
attack=0.01,
decay=0.15,
sustain=0.7,
release=0.2,
sample_rate=44100
) -> np.ndarray:
"""
Synthesize a synth bass note.
Args:
frequency: Note frequency in Hz (A1 = 55Hz)
duration: Note duration in seconds
waveform: 'sawtooth', 'square', or 'sine'
cutoff: Low-pass filter cutoff in Hz
resonance: Filter resonance (0-1)
attack: ADSR attack time
decay: ADSR decay time
sustain: ADSR sustain level (0-1)
release: ADSR release time
sample_rate: Audio sample rate
Returns:
16-bit mono audio array
"""
t = np.linspace(0, duration, int(sample_rate * duration))
if waveform == 'sawtooth':
oscillator = signal.sawtooth(2 * np.pi * frequency * t)
elif waveform == 'square':
oscillator = signal.square(2 * np.pi * frequency * t)
elif waveform == 'sine':
oscillator = np.sin(2 * np.pi * frequency * t)
else:
oscillator = signal.sawtooth(2 * np.pi * frequency * t)
if frequency < 60:
sub_bass_mix = 0.0
elif frequency < 100:
sub_bass_mix = 0.05
elif frequency < 150:
sub_bass_mix = 0.1
else:
sub_bass_mix = 0.15
if sub_bass_mix > 0:
sub_bass = np.sin(2 * np.pi * (frequency / 2) * t)
bass_signal = oscillator * (1.0 - sub_bass_mix) + sub_bass * sub_bass_mix
else:
bass_signal = oscillator
bass = apply_lowpass_filter(bass_signal, cutoff, resonance, sample_rate)
bass = apply_adsr_envelope(bass, attack, decay, sustain, release, sample_rate)
bass = np.tanh(bass * 1.2) / 1.2
bass = bass / np.max(np.abs(bass)) * 0.5
return (bass * 32767).astype(np.int16)
def synthesize_pad_chord(
frequencies: list[float],
duration=4.0,
attack=0.8,
release=1.0,
brightness=0.4,
detune=0.03,
sample_rate=44100
) -> np.ndarray:
"""
Synthesize an atmospheric pad chord.
Args:
frequencies: List of note frequencies in Hz (e.g., [220, 261.63, 329.63] for Am)
duration: Chord duration in seconds
attack: Slow attack time for pad character
release: Release time
brightness: Filter brightness (0=dark, 1=bright)
detune: Detuning amount for chorus effect (0-0.1)
sample_rate: Audio sample rate
Returns:
16-bit mono audio array
"""
t = np.linspace(0, duration, int(sample_rate * duration))
pad = np.zeros(len(t))
detune_amounts = [-0.02, 0, 0.02]
phase_offsets = [0, 0, 0.3]
for freq in frequencies:
for detune_amt, phase_offset in zip(detune_amounts, phase_offsets):
phase = 2 * np.pi * freq * (1 + detune_amt * detune) * t + phase_offset
osc = np.sin(phase)
pad += osc
pad = pad / (len(frequencies) * len(detune_amounts))
pad = apply_highpass_filter(pad, cutoff_hz=200, sample_rate=sample_rate)
cutoff = 400 + brightness * 1000
pad = apply_lowpass_filter(pad, cutoff, resonance=0.2, sample_rate=sample_rate)
sustain = 0.7
pad = apply_adsr_envelope(pad, attack, 0.1, sustain, release, sample_rate)
pad = np.tanh(pad * 1.0) / 1.0
pad = pad / np.max(np.abs(pad)) * 0.35
return (pad * 32767).astype(np.int16)
def synthesize_lead_note(
frequency=440.0,
duration=0.5,
brightness=0.8,
attack=0.005,
decay=0.1,
sustain=0.6,
release=0.1,
portamento=0.0,
unison_voices=7,
unison_detune=0.12,
sample_rate=44100
) -> np.ndarray:
"""
Synthesize a synth lead note with supersaw (multiple detuned oscillators).
Args:
frequency: Note frequency in Hz (A4 = 440Hz)
duration: Note duration in seconds
brightness: Filter brightness (0=dark, 1=bright)
attack: Fast attack for plucky character
decay: Decay time
sustain: Sustain level (0-1)
release: Release time
portamento: Pitch glide time (0=none)
unison_voices: Number of detuned voices (1=single osc, 7=supersaw)
unison_detune: Detune amount in semitones (0.12 = ±12 cents)
sample_rate: Audio sample rate
Returns:
16-bit mono audio array
"""
t = np.linspace(0, duration, int(sample_rate * duration))
if unison_voices == 1:
if portamento > 0:
freq_start = frequency * 0.9
portamento_samples = int(portamento * sample_rate)
freq_curve = np.ones(len(t)) * frequency
freq_curve[:portamento_samples] = np.linspace(
freq_start, frequency, portamento_samples
)
phase = 2 * np.pi * np.cumsum(freq_curve) / sample_rate
oscillator = signal.sawtooth(phase)
else:
oscillator = signal.sawtooth(2 * np.pi * frequency * t)
else:
supersaw = np.zeros(len(t))
detune_range = unison_detune
detune_step = (2 * detune_range) / max(1, unison_voices - 1)
for voice_idx in range(unison_voices):
detune_semitones = -detune_range + (voice_idx * detune_step)
detune_ratio = 2 ** (detune_semitones / 12)
detuned_freq = frequency * detune_ratio
phase_offset = (voice_idx * 0.15) % (2 * np.pi)
if portamento > 0:
freq_start = detuned_freq * 0.9
portamento_samples = int(portamento * sample_rate)
freq_curve = np.ones(len(t)) * detuned_freq
freq_curve[:portamento_samples] = np.linspace(
freq_start, detuned_freq, portamento_samples
)
phase = 2 * np.pi * np.cumsum(freq_curve) / sample_rate + phase_offset
voice = signal.sawtooth(phase)
else:
voice = signal.sawtooth(2 * np.pi * detuned_freq * t + phase_offset)
supersaw += voice
oscillator = supersaw / unison_voices
cutoff = 300 + brightness * 1200
lead = apply_lowpass_filter(oscillator, cutoff, resonance=0.4, sample_rate=sample_rate)
lead = apply_adsr_envelope(lead, attack, decay, sustain, release, sample_rate)
if unison_voices > 1:
lead = np.tanh(lead * 2.5) / 2.0
else:
lead = np.tanh(lead * 1.5) / 1.5
lead = lead / np.max(np.abs(lead)) * 0.65
return (lead * 32767).astype(np.int16)
def midi_note_to_frequency(midi_note: int) -> float:
"""Convert MIDI note number to frequency in Hz."""
return 440.0 * (2.0 ** ((midi_note - 69) / 12.0))
def frequency_to_note_name(frequency: float) -> str:
"""Convert frequency to nearest note name for debugging."""
notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
midi_note = int(69 + 12 * np.log2(frequency / 440.0))
octave = (midi_note // 12) - 1
note = notes[midi_note % 12]
return f"{note}{octave}"
def get_frequency_compensation_db(frequency: float, instrument_type: str = "bass") -> float:
"""
Calculate frequency-aware volume compensation in dB.
Lower frequencies naturally have more perceived energy and should be reduced.
Higher frequencies can be boosted for clarity.
Args:
frequency: Frequency in Hz
instrument_type: Type of instrument ('bass', 'pad', 'lead')
Returns:
Compensation in dB (negative for reduction, positive for boost)
"""
if instrument_type == "bass":
if frequency < 40:
return -9.0
elif frequency < 60:
return -7.0
elif frequency < 100:
return -5.0
elif frequency < 150:
return -3.0
elif frequency < 250:
return -1.5
else:
return 0.0
elif instrument_type == "pad":
if frequency < 120:
return -4.0
elif frequency < 200:
return -3.0
elif frequency < 300:
return -2.0
elif frequency < 500:
return -1.0
elif frequency > 1000:
return 1.0
else:
return 0.0
elif instrument_type == "lead":
if frequency < 200:
return -1.0
elif frequency > 800:
return 1.5
elif frequency > 500:
return 0.5
else:
return 0.0
return 0.0
def velocity_to_db(velocity: int, curve: str = "linear") -> float:
"""
Convert MIDI velocity (0-127) to dB adjustment with proper curve.
Args:
velocity: MIDI velocity (0-127)
curve: Response curve ('linear', 'exponential', 'logarithmic')
Returns:
Volume adjustment in dB
"""
normalized = velocity / 127.0
if curve == "exponential":
response = normalized ** 2
return (response - 0.5) * 12
elif curve == "logarithmic":
response = np.log1p(normalized * 2) / np.log1p(2)
return (response - 0.5) * 10
else:
return (velocity - 90) * 0.15#!/usr/bin/env python3
"""
Extract structured information from MIDI files to JSON.
This script analyzes any MIDI file and extracts:
- Tempo, key signature, time signature
- Track information and instrument assignments
- Note sequences with timing and velocity
- Chord progressions and musical structure
Usage:
python midi_inventory.py input.mid output.json
"""
import argparse
import json
import sys
from pathlib import Path
from typing import Any
import mido
def extract_midi_inventory(midi_path: Path) -> dict[str, Any]:
"""Extract complete musical structure from MIDI file."""
midi = mido.MidiFile(midi_path)
inventory = {
"filename": midi_path.name,
"type": midi.type,
"ticks_per_beat": midi.ticks_per_beat,
"length_seconds": midi.length,
"tempo": 120,
"time_signature": "4/4",
"key_signature": None,
"tracks": {}
}
current_tempo = 500000
for track_idx, track in enumerate(midi.tracks):
track_name = f"track-{track_idx}"
track_info = {
"name": track.name or track_name,
"instrument": None,
"midi_program": None,
"notes": []
}
absolute_time = 0
active_notes = {}
for msg in track:
absolute_time += msg.time
if msg.type == 'set_tempo':
current_tempo = msg.tempo
inventory["tempo"] = int(60000000 / current_tempo)
elif msg.type == 'time_signature':
inventory["time_signature"] = f"{msg.numerator}/{msg.denominator}"
elif msg.type == 'key_signature':
inventory["key_signature"] = msg.key
elif msg.type == 'program_change':
track_info["midi_program"] = msg.program
track_info["instrument"] = get_instrument_name(msg.program)
elif msg.type == 'note_on' and msg.velocity > 0:
active_notes[msg.note] = {
"start_ticks": absolute_time,
"velocity": msg.velocity
}
elif msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity == 0):
if msg.note in active_notes:
note_start = active_notes[msg.note]
duration_ticks = absolute_time - note_start["start_ticks"]
start_seconds = ticks_to_seconds(
note_start["start_ticks"],
midi.ticks_per_beat,
current_tempo
)
duration_seconds = ticks_to_seconds(
duration_ticks,
midi.ticks_per_beat,
current_tempo
)
track_info["notes"].append({
"pitch": midi_note_to_name(msg.note),
"midi_note": msg.note,
"start": round(start_seconds, 3),
"duration": round(duration_seconds, 3),
"velocity": note_start["velocity"]
})
del active_notes[msg.note]
if track_info["notes"]:
inventory["tracks"][track_name] = track_info
return inventory
def ticks_to_seconds(ticks: int, ticks_per_beat: int, tempo: int) -> float:
"""Convert MIDI ticks to seconds."""
seconds_per_tick = (tempo / 1000000) / ticks_per_beat
return ticks * seconds_per_tick
def midi_note_to_name(midi_note: int) -> str:
"""Convert MIDI note number to note name (e.g., 60 -> C4)."""
note_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
octave = (midi_note // 12) - 1
note = note_names[midi_note % 12]
return f"{note}{octave}"
def get_instrument_name(program: int) -> str:
"""Get instrument name from General MIDI program number."""
instruments = {
0: "piano", 1: "bright_piano", 4: "electric_piano",
24: "acoustic_guitar", 27: "electric_guitar", 32: "bass",
40: "violin", 41: "viola", 42: "cello", 43: "contrabass",
46: "harp", 47: "timpani",
56: "trumpet", 57: "trombone", 58: "tuba", 60: "french_horn",
68: "oboe", 70: "bassoon", 71: "clarinet", 73: "flute",
88: "synth_pad", 80: "synth_lead"
}
return instruments.get(program, f"program_{program}")
def main():
parser = argparse.ArgumentParser(
description="Extract musical structure from MIDI files to JSON",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python midi_inventory.py song.mid structure.json
Extract complete structure from any MIDI file
python midi_inventory.py mozart.mid mozart-analysis.json
Analyze classical MIDI file
Output JSON structure:
{
"tempo": 120,
"time_signature": "4/4",
"key_signature": "C",
"tracks": {
"track-0": {
"instrument": "violin",
"midi_program": 40,
"notes": [
{"pitch": "E5", "start": 0.0, "duration": 0.5, "velocity": 80}
]
}
}
}
"""
)
parser.add_argument("input", help="Input MIDI file (.mid)")
parser.add_argument("output", help="Output JSON file")
args = parser.parse_args()
input_path = Path(args.input)
if not input_path.exists():
print(f"Error: Input file not found: {args.input}")
sys.exit(1)
if not input_path.suffix.lower() in ['.mid', '.midi']:
print("Error: Input must be a MIDI file (.mid or .midi)")
sys.exit(1)
try:
print(f"Extracting MIDI structure from: {args.input}")
inventory = extract_midi_inventory(input_path)
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
json.dump(inventory, f, indent=2)
print(f"Inventory saved to: {args.output}")
print(f" Tempo: {inventory['tempo']} BPM")
print(f" Time signature: {inventory['time_signature']}")
print(f" Duration: {inventory['length_seconds']:.1f}s")
print(f" Tracks: {len(inventory['tracks'])}")
except Exception as e:
print(f"Error extracting MIDI structure: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()#!/usr/bin/env python3
"""
Render JSON music structure to MP3 audio file.
This script takes a JSON structure (from midi_inventory.py or custom)
and renders it to MP3 using FluidSynth and the existing rendering pipeline.
Usage:
python midi_render.py structure.json output.mp3
"""
import argparse
import json
import os
import sys
from pathlib import Path
from music21 import stream, note, instrument, tempo, key, meter
from midi2audio import FluidSynth
from pydub import AudioSegment
# Standard system paths for soundfont files
# These are installed by the skill's install.sh script via: apt-get install fluid-soundfont-gm
SOUNDFONT_PATHS = [
'/usr/share/sounds/sf2/FluidR3_GM.sf2',
'/usr/share/sounds/sf2/default.sf2',
'/usr/share/soundfonts/default.sf2',
]
def get_soundfont_path():
"""Find the best available SoundFont."""
for path in SOUNDFONT_PATHS:
if os.path.exists(path):
return path
raise FileNotFoundError("No SoundFont file found. Install with: apt-get install fluid-soundfont-gm")
INSTRUMENTS = {
"piano": (instrument.Piano(), 0),
"bright_piano": (instrument.Piano(), 1),
"electric_piano": (instrument.Piano(), 4),
"acoustic_guitar": (instrument.AcousticGuitar(), 24),
"electric_guitar": (instrument.ElectricGuitar(), 27),
"bass": (instrument.Bass(), 32),
"violin": (instrument.Violin(), 40),
"viola": (instrument.Viola(), 41),
"cello": (instrument.Violoncello(), 42),
"contrabass": (instrument.Contrabass(), 43),
"harp": (instrument.Harp(), 46),
"timpani": (instrument.Timpani(), 47),
"trumpet": (instrument.Trumpet(), 56),
"trombone": (instrument.Trombone(), 57),
"tuba": (instrument.Tuba(), 58),
"french_horn": (instrument.Horn(), 60),
"oboe": (instrument.Oboe(), 68),
"bassoon": (instrument.Bassoon(), 70),
"clarinet": (instrument.Clarinet(), 71),
"flute": (instrument.Flute(), 73),
"synth_pad": (instrument.Piano(), 88),
}
def render_json_to_mp3(json_structure: dict, output_mp3: Path) -> str:
"""Render JSON music structure to MP3 file."""
score = stream.Score()
if "tempo" in json_structure:
score.insert(0, tempo.MetronomeMark(number=json_structure["tempo"]))
if "key_signature" in json_structure and json_structure["key_signature"]:
score.insert(0, key.Key(json_structure["key_signature"]))
if "time_signature" in json_structure:
score.insert(0, meter.TimeSignature(json_structure["time_signature"]))
for track_id, track_data in json_structure.get("tracks", {}).items():
part = stream.Part()
inst_name = track_data.get("instrument", "piano")
if inst_name in INSTRUMENTS:
inst_obj, midi_num = INSTRUMENTS[inst_name]
part.insert(0, inst_obj)
else:
part.insert(0, instrument.Piano())
if "tempo" in json_structure:
part.insert(0, tempo.MetronomeMark(number=json_structure["tempo"]))
for note_data in track_data.get("notes", []):
pitch = note_data.get("pitch")
duration = note_data.get("duration", 0.5)
start_time = note_data.get("start", 0.0)
tempo_bpm = json_structure.get("tempo", 120)
quarter_length = duration * (tempo_bpm / 60)
n = note.Note(pitch, quarterLength=quarter_length)
part.insert(start_time, n)
score.append(part)
title = output_mp3.stem
midi_path = output_mp3.parent / f"{title}.mid"
wav_path = output_mp3.parent / f"{title}.wav"
score.write('midi', fp=str(midi_path))
print(f"MIDI created: {midi_path}")
sf_path = get_soundfont_path()
fs = FluidSynth(sf_path)
fs.midi_to_audio(str(midi_path), str(wav_path))
print(f"WAV rendered: {wav_path}")
audio = AudioSegment.from_wav(str(wav_path))
if json_structure.get("length_seconds"):
target_ms = int(json_structure["length_seconds"] * 1000)
if len(audio) > target_ms:
audio = audio[:target_ms]
audio = audio.fade_out(2000)
audio.export(str(output_mp3), format='mp3', bitrate='192k')
print(f"MP3 exported: {output_mp3}")
try:
os.remove(midi_path)
os.remove(wav_path)
except:
pass
return str(output_mp3)
def main():
parser = argparse.ArgumentParser(
description="Render JSON music structure to MP3 audio",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python midi_render.py structure.json output.mp3
Render JSON structure to MP3
python midi_render.py modified-song.json new-version.mp3
Render modified structure
JSON structure format:
{
"tempo": 120,
"key_signature": "C",
"time_signature": "4/4",
"tracks": {
"track-0": {
"instrument": "violin",
"notes": [
{"pitch": "E5", "start": 0.0, "duration": 0.5}
]
}
}
}
"""
)
parser.add_argument("input", help="Input JSON structure file")
parser.add_argument("output", help="Output MP3 file")
args = parser.parse_args()
input_path = Path(args.input)
if not input_path.exists():
print(f"Error: Input file not found: {args.input}")
sys.exit(1)
if not input_path.suffix.lower() == '.json':
print("Error: Input must be a JSON file")
sys.exit(1)
try:
print(f"Loading JSON structure from: {args.input}")
with open(input_path, 'r') as f:
structure = json.load(f)
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
if not output_path.suffix.lower() == '.mp3':
output_path = output_path.with_suffix('.mp3')
result = render_json_to_mp3(structure, output_path)
print(f"\nRendering complete!")
print(f"Output: {result}")
except Exception as e:
print(f"Error rendering audio: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()#!/usr/bin/env python3
"""
Transform MIDI structure JSON with various operations.
This script applies generic transformations to JSON music structures:
- Transpose (shift all pitches)
- Tempo scaling (speed up/slow down)
- Duration extension (loop/repeat)
- Instrument swapping
- Section looping
- Reversal
Usage:
python midi_transform.py input.json output.json --transpose 2 --tempo-scale 1.5
"""
import argparse
import copy
import json
import sys
from pathlib import Path
def transpose_notes(notes: list[dict], semitones: int) -> list[dict]:
"""Transpose all notes by semitones."""
note_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
transposed = []
for note in notes:
pitch = note['pitch']
note_part = ''.join(c for c in pitch if c.isalpha() or c == '#')
octave = int(''.join(c for c in pitch if c.isdigit()))
if note_part in note_names:
current_index = note_names.index(note_part)
total_semitones = (octave * 12) + current_index + semitones
new_octave = total_semitones // 12
new_note_index = total_semitones % 12
new_pitch = f"{note_names[new_note_index]}{new_octave}"
new_note = copy.deepcopy(note)
new_note['pitch'] = new_pitch
new_note['midi_note'] = total_semitones + 12
transposed.append(new_note)
return transposed
def scale_tempo(structure: dict, tempo_scale: float) -> dict:
"""Scale tempo by multiplying BPM."""
if 'tempo' in structure:
structure['tempo'] = int(structure['tempo'] * tempo_scale)
return structure
def extend_duration(structure: dict, target_seconds: float) -> dict:
"""Extend composition to target duration by repeating patterns."""
current_duration = structure.get('length_seconds', 0)
if current_duration >= target_seconds:
return structure
repetitions_needed = int((target_seconds / current_duration) + 1) if current_duration > 0 else 1
for track_id, track in structure.get('tracks', {}).items():
original_notes = track['notes']
extended_notes = []
for rep in range(repetitions_needed):
time_offset = rep * current_duration
for note in original_notes:
new_note = copy.deepcopy(note)
new_note['start'] = note['start'] + time_offset
extended_notes.append(new_note)
track['notes'] = extended_notes
structure['length_seconds'] = target_seconds
return structure
def swap_instrument(structure: dict, from_inst: str, to_inst: str) -> dict:
"""Swap instrument in all tracks."""
instrument_map = {
"piano": 0, "bright_piano": 1, "electric_piano": 4,
"acoustic_guitar": 24, "electric_guitar": 27, "bass": 32,
"violin": 40, "viola": 41, "cello": 42, "contrabass": 43,
"harp": 46, "timpani": 47,
"trumpet": 56, "trombone": 57, "tuba": 58, "french_horn": 60,
"oboe": 68, "bassoon": 70, "clarinet": 71, "flute": 73,
}
for track_id, track in structure.get('tracks', {}).items():
if track.get('instrument') == from_inst:
track['instrument'] = to_inst
if to_inst in instrument_map:
track['midi_program'] = instrument_map[to_inst]
return structure
def loop_section(structure: dict, start_time: float, end_time: float, repetitions: int) -> dict:
"""Loop a time section multiple times."""
for track_id, track in structure.get('tracks', {}).items():
section_notes = [n for n in track['notes'] if start_time <= n['start'] < end_time]
section_duration = end_time - start_time
looped_notes = []
for rep in range(repetitions):
time_offset = end_time + (rep * section_duration)
for note in section_notes:
new_note = copy.deepcopy(note)
new_note['start'] = (note['start'] - start_time) + time_offset
looped_notes.append(new_note)
track['notes'].extend(looped_notes)
track['notes'].sort(key=lambda n: n['start'])
if 'length_seconds' in structure:
structure['length_seconds'] += section_duration * repetitions
return structure
def reverse_composition(structure: dict) -> dict:
"""Reverse the entire composition."""
total_duration = structure.get('length_seconds', 0)
for track_id, track in structure.get('tracks', {}).items():
for note in track['notes']:
note['start'] = total_duration - note['start'] - note['duration']
track['notes'].sort(key=lambda n: n['start'])
return structure
def main():
parser = argparse.ArgumentParser(
description="Transform MIDI structure JSON with various operations",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python midi_transform.py input.json output.json --transpose 2
Transpose all notes up by 2 semitones (whole step)
python midi_transform.py input.json output.json --tempo-scale 1.5
Speed up by 50% (120 BPM → 180 BPM)
python midi_transform.py input.json output.json --extend-to 90
Extend composition to 90 seconds by repeating patterns
python midi_transform.py input.json output.json --swap-instrument violin cello
Change all violin tracks to cello
python midi_transform.py input.json output.json --transpose 2 --tempo-scale 1.2
Combine multiple transformations
python midi_transform.py input.json output.json --loop-section 10 20 3
Loop the section from 10s-20s three times
python midi_transform.py input.json output.json --reverse
Reverse the entire composition
"""
)
parser.add_argument("input", help="Input JSON structure file")
parser.add_argument("output", help="Output JSON structure file")
parser.add_argument("--transpose", type=int, help="Transpose by N semitones")
parser.add_argument("--tempo-scale", type=float, help="Multiply tempo by factor")
parser.add_argument("--extend-to", type=float, help="Extend to N seconds")
parser.add_argument("--swap-instrument", nargs=2, metavar=('FROM', 'TO'),
help="Swap instrument (e.g., violin cello)")
parser.add_argument("--loop-section", nargs=3, type=float,
metavar=('START', 'END', 'TIMES'),
help="Loop section from START to END for TIMES repetitions")
parser.add_argument("--reverse", action='store_true', help="Reverse composition")
args = parser.parse_args()
input_path = Path(args.input)
if not input_path.exists():
print(f"Error: Input file not found: {args.input}")
sys.exit(1)
try:
print(f"Loading structure from: {args.input}")
with open(input_path, 'r') as f:
structure = json.load(f)
if args.transpose:
print(f"Transposing by {args.transpose} semitones...")
for track_id, track in structure.get('tracks', {}).items():
track['notes'] = transpose_notes(track['notes'], args.transpose)
if args.tempo_scale:
print(f"Scaling tempo by {args.tempo_scale}x...")
structure = scale_tempo(structure, args.tempo_scale)
if args.extend_to:
print(f"Extending to {args.extend_to} seconds...")
structure = extend_duration(structure, args.extend_to)
if args.swap_instrument:
from_inst, to_inst = args.swap_instrument
print(f"Swapping {from_inst} → {to_inst}...")
structure = swap_instrument(structure, from_inst, to_inst)
if args.loop_section:
start, end, times = args.loop_section
print(f"Looping section {start}s-{end}s {int(times)} times...")
structure = loop_section(structure, start, end, int(times))
if args.reverse:
print("Reversing composition...")
structure = reverse_composition(structure)
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
json.dump(structure, f, indent=2)
print(f"\nTransformation complete!")
print(f"Output: {args.output}")
except Exception as e:
print(f"Error transforming structure: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()from mido import MidiFile, MidiTrack, MetaMessage, Message
import os
def get_midi_bpm(midi_path: str) -> int:
"""Extract BPM from MIDI file, default to 120 if not found."""
mid = MidiFile(midi_path)
for track in mid.tracks:
for msg in track:
if msg.type == 'set_tempo':
return int(60_000_000 / msg.tempo)
return 120
def get_midi_duration_ms(midi_path: str) -> int:
"""Calculate total duration of MIDI file in milliseconds."""
mid = MidiFile(midi_path)
total_ticks = 0
for track in mid.tracks:
track_ticks = sum(msg.time for msg in track)
total_ticks = max(total_ticks, track_ticks)
bpm = get_midi_bpm(midi_path)
seconds_per_tick = 60 / (bpm * mid.ticks_per_beat)
total_seconds = total_ticks * seconds_per_tick
return int(total_seconds * 1000)
def extract_drum_events(midi_path: str) -> list[dict]:
"""
Extract all drum events (channel 9/10) from MIDI file.
Returns list of dicts with: time_ms, note, velocity
"""
mid = MidiFile(midi_path)
bpm = get_midi_bpm(midi_path)
seconds_per_tick = 60 / (bpm * mid.ticks_per_beat)
events = []
for track in mid.tracks:
current_time_ticks = 0
for msg in track:
current_time_ticks += msg.time
if msg.type == 'note_on' and msg.channel == 9 and msg.velocity > 0:
time_ms = int(current_time_ticks * seconds_per_tick * 1000)
events.append({
'time_ms': time_ms,
'note': msg.note,
'velocity': msg.velocity
})
return events
def strip_channel(midi_path: str, channel: int, output_path: str = None) -> str:
"""
Remove all events from a specific MIDI channel.
Returns path to new MIDI file without that channel.
"""
if output_path is None:
base, ext = os.path.splitext(midi_path)
output_path = f"{base}_no_ch{channel}{ext}"
mid = MidiFile(midi_path)
new_mid = MidiFile(ticks_per_beat=mid.ticks_per_beat)
for track in mid.tracks:
new_track = MidiTrack()
for msg in track:
if not hasattr(msg, 'channel') or msg.channel != channel:
new_track.append(msg.copy())
if len(new_track) > 0:
new_mid.tracks.append(new_track)
new_mid.save(output_path)
return output_path
def extract_drum_channel(midi_path: str, output_path: str = None) -> str:
"""
Extract ONLY drum channel (9) from MIDI file.
Returns path to drums-only MIDI file.
"""
if output_path is None:
base, ext = os.path.splitext(midi_path)
output_path = f"{base}_drums_only{ext}"
mid = MidiFile(midi_path)
new_mid = MidiFile(ticks_per_beat=mid.ticks_per_beat)
tempo_track = MidiTrack()
for track in mid.tracks:
for msg in track:
if msg.type == 'set_tempo':
tempo_track.append(msg.copy())
if len(tempo_track) > 0:
new_mid.tracks.append(tempo_track)
for track in mid.tracks:
new_track = MidiTrack()
for msg in track:
if hasattr(msg, 'channel') and msg.channel == 9:
new_track.append(msg.copy())
elif msg.type in ['track_name', 'end_of_track']:
new_track.append(msg.copy())
if any(msg.type == 'note_on' for msg in new_track):
new_mid.tracks.append(new_track)
new_mid.save(output_path)
return output_path#!/usr/bin/env python3
"""
Electronic Music Renderer with Real-Time Synthesis
Renders MIDI files using pure synthesis - no external samples or soundfonts needed.
Synthesizes drums, bass, pads, and leads on-the-fly for authentic electronic sound.
Usage:
python render_electronic.py input.mid output.mp3
python render_electronic.py input.mid output.mp3 --genre deep_house
python render_electronic.py input.mid output.mp3 --preset custom_params.json
"""
import sys
import os
import argparse
import json
from pathlib import Path
from pydub import AudioSegment
import numpy as np
from midi_utils import (
extract_drum_events,
get_midi_duration_ms,
get_midi_bpm
)
from drum_synthesizer import (
synthesize_kick,
synthesize_snare,
synthesize_hat_closed,
synthesize_hat_open,
synthesize_clap,
synthesize_rim
)
from melodic_synthesizer import (
synthesize_bass_note,
synthesize_pad_chord,
synthesize_lead_note,
midi_note_to_frequency,
get_frequency_compensation_db,
velocity_to_db
)
from synthesis_presets import get_preset, list_genres
DRUM_SYNTH_MAP = {
35: 'kick',
36: 'kick',
37: 'rim',
38: 'snare',
39: 'clap',
40: 'snare',
42: 'hat_closed',
44: 'hat_closed',
46: 'hat_open',
}
def synthesize_drum_sample(drum_type: str, params: dict, sample_rate=44100) -> np.ndarray:
"""
Synthesize a drum sample based on type and parameters.
Args:
drum_type: Type of drum ('kick', 'snare', 'hat_closed', etc.)
params: Synthesis parameters from preset
sample_rate: Audio sample rate
Returns:
Audio array (16-bit PCM)
"""
if drum_type == 'kick':
return synthesize_kick(**params, sample_rate=sample_rate)
elif drum_type == 'snare':
return synthesize_snare(**params, sample_rate=sample_rate)
elif drum_type == 'hat_closed':
return synthesize_hat_closed(**params, sample_rate=sample_rate)
elif drum_type == 'hat_open':
return synthesize_hat_open(**params, sample_rate=sample_rate)
elif drum_type == 'clap':
return synthesize_clap(**params, sample_rate=sample_rate)
elif drum_type == 'rim':
return synthesize_rim(**params, sample_rate=sample_rate)
else:
return synthesize_kick(sample_rate=sample_rate)
def render_drums_synthesized(
drum_events: list[dict],
total_duration_ms: int,
preset: dict,
sample_rate=44100
) -> AudioSegment:
"""
Render drum track using real-time synthesis.
Args:
drum_events: List of drum events from MIDI
total_duration_ms: Total duration in milliseconds
preset: Genre preset with drum parameters
sample_rate: Audio sample rate
Returns:
Mixed drum audio
"""
drum_audio = AudioSegment.silent(duration=total_duration_ms)
drum_params = preset.get('drums', {})
volume_balance = preset.get('volume_balance', {})
base_drum_level = volume_balance.get('drums', 0.0)
velocity_curve = volume_balance.get('velocity_curve', 'linear')
for event in drum_events:
drum_type = DRUM_SYNTH_MAP.get(event['note'])
if drum_type is None:
continue
params = drum_params.get(drum_type, {})
try:
sample_array = synthesize_drum_sample(drum_type, params, sample_rate)
sample = AudioSegment(
sample_array.tobytes(),
frame_rate=sample_rate,
sample_width=2,
channels=1
)
velocity = event['velocity']
velocity_db = velocity_to_db(velocity, velocity_curve)
total_volume_db = base_drum_level + velocity_db
sample = sample + total_volume_db
position_ms = event['time_ms']
drum_audio = drum_audio.overlay(sample, position=position_ms)
except Exception as e:
print(f"Warning: Could not synthesize {drum_type} at {event['time_ms']}ms: {e}")
return drum_audio
def extract_melodic_events(midi_path: str) -> dict:
"""
Extract melodic events (bass, pads, leads) from MIDI file.
Returns:
Dictionary with 'bass', 'pad', 'lead' keys, each containing note events
"""
from mido import MidiFile
mid = MidiFile(midi_path)
bpm = get_midi_bpm(midi_path)
seconds_per_tick = 60 / (bpm * mid.ticks_per_beat)
melodic_events = {
'bass': [],
'pad': [],
'lead': []
}
for i, track in enumerate(mid.tracks):
if i == 0:
continue
current_time_ticks = 0
track_program = 0
track_channel = 0
active_notes = {}
for msg in track:
current_time_ticks += msg.time
if msg.type == 'program_change':
track_program = msg.program
track_channel = msg.channel
elif msg.type == 'note_on' and msg.channel != 9:
time_ms = int(current_time_ticks * seconds_per_tick * 1000)
if msg.velocity > 0:
note_key = msg.note
active_notes[note_key] = {
'start_ms': time_ms,
'velocity': msg.velocity,
'program': track_program
}
else:
note_key = msg.note
if note_key in active_notes:
note_info = active_notes.pop(note_key)
duration_ms = time_ms - note_info['start_ms']
if duration_ms < 50:
duration_ms = 2000
if 32 <= note_info['program'] <= 39:
instrument_type = 'bass'
elif 80 <= note_info['program'] <= 87:
instrument_type = 'lead'
elif 88 <= note_info['program'] <= 95:
instrument_type = 'pad'
elif 0 <= note_info['program'] <= 7:
instrument_type = 'lead'
else:
instrument_type = 'pad'
melodic_events[instrument_type].append({
'time_ms': note_info['start_ms'],
'note': note_key,
'velocity': note_info['velocity'],
'duration_ms': duration_ms
})
elif msg.type == 'note_off' and msg.channel != 9:
time_ms = int(current_time_ticks * seconds_per_tick * 1000)
note_key = msg.note
if note_key in active_notes:
note_info = active_notes.pop(note_key)
duration_ms = time_ms - note_info['start_ms']
if duration_ms < 50:
duration_ms = 2000
if 32 <= note_info['program'] <= 39:
instrument_type = 'bass'
elif 80 <= note_info['program'] <= 87:
instrument_type = 'lead'
elif 88 <= note_info['program'] <= 95:
instrument_type = 'pad'
elif 0 <= note_info['program'] <= 7:
instrument_type = 'lead'
else:
instrument_type = 'pad'
melodic_events[instrument_type].append({
'time_ms': note_info['start_ms'],
'note': note_key,
'velocity': note_info['velocity'],
'duration_ms': duration_ms
})
for note_key, note_info in active_notes.items():
duration_ms = 2000
if 32 <= note_info['program'] <= 39:
instrument_type = 'bass'
elif 80 <= note_info['program'] <= 87:
instrument_type = 'lead'
elif 88 <= note_info['program'] <= 95:
instrument_type = 'pad'
elif 0 <= note_info['program'] <= 7:
instrument_type = 'lead'
else:
instrument_type = 'pad'
melodic_events[instrument_type].append({
'time_ms': note_info['start_ms'],
'note': note_key,
'velocity': note_info['velocity'],
'duration_ms': duration_ms
})
return melodic_events
def render_melodic_synthesized(
melodic_events: dict,
total_duration_ms: int,
preset: dict,
sample_rate=44100
) -> AudioSegment:
"""
Render melodic parts (bass, pads, leads) using synthesis.
Args:
melodic_events: Dictionary of melodic note events
total_duration_ms: Total duration in milliseconds
preset: Genre preset with melodic parameters
sample_rate: Audio sample rate
Returns:
Mixed melodic audio
"""
melodic_audio = AudioSegment.silent(duration=total_duration_ms)
volume_balance = preset.get('volume_balance', {})
base_bass_level = volume_balance.get('bass', 0.0)
base_lead_level = volume_balance.get('lead', 0.0)
base_pad_level = volume_balance.get('pad', 0.0)
velocity_curve = volume_balance.get('velocity_curve', 'linear')
bass_params = preset.get('bass', {})
for event in melodic_events.get('bass', []):
try:
frequency = midi_note_to_frequency(event['note'])
duration = event['duration_ms'] / 1000.0
bass_array = synthesize_bass_note(
frequency=frequency,
duration=duration,
**bass_params,
sample_rate=sample_rate
)
bass_segment = AudioSegment(
bass_array.tobytes(),
frame_rate=sample_rate,
sample_width=2,
channels=1
)
velocity = event['velocity']
velocity_db = velocity_to_db(velocity, velocity_curve)
frequency_comp = get_frequency_compensation_db(frequency, "bass")
total_volume_db = base_bass_level + velocity_db + frequency_comp
bass_segment = bass_segment + total_volume_db
position_ms = event['time_ms']
melodic_audio = melodic_audio.overlay(bass_segment, position=position_ms)
except Exception as e:
print(f"Warning: Could not synthesize bass note at {event['time_ms']}ms: {e}")
lead_params = preset.get('lead', {})
for event in melodic_events.get('lead', []):
try:
frequency = midi_note_to_frequency(event['note'])
duration = event['duration_ms'] / 1000.0
lead_array = synthesize_lead_note(
frequency=frequency,
duration=duration,
**lead_params,
sample_rate=sample_rate
)
lead_segment = AudioSegment(
lead_array.tobytes(),
frame_rate=sample_rate,
sample_width=2,
channels=1
)
velocity = event['velocity']
velocity_db = velocity_to_db(velocity, velocity_curve)
frequency_comp = get_frequency_compensation_db(frequency, "lead")
total_volume_db = base_lead_level + velocity_db + frequency_comp
lead_segment = lead_segment + total_volume_db
position_ms = event['time_ms']
melodic_audio = melodic_audio.overlay(lead_segment, position=position_ms)
except Exception as e:
print(f"Warning: Could not synthesize lead note at {event['time_ms']}ms: {e}")
pad_params = preset.get('pad', {})
for event in melodic_events.get('pad', []):
try:
frequency = midi_note_to_frequency(event['note'])
duration = event['duration_ms'] / 1000.0
pad_array = synthesize_pad_chord(
frequencies=[frequency],
duration=duration,
**pad_params,
sample_rate=sample_rate
)
pad_segment = AudioSegment(
pad_array.tobytes(),
frame_rate=sample_rate,
sample_width=2,
channels=1
)
velocity = event['velocity']
velocity_db = velocity_to_db(velocity, velocity_curve)
frequency_comp = get_frequency_compensation_db(frequency, "pad")
total_volume_db = base_pad_level + velocity_db + frequency_comp
pad_segment = pad_segment + total_volume_db
position_ms = event['time_ms']
melodic_audio = melodic_audio.overlay(pad_segment, position=position_ms)
except Exception as e:
print(f"Warning: Could not synthesize pad note at {event['time_ms']}ms: {e}")
return melodic_audio
def render_electronic(
midi_path: str,
output_path: str,
genre: str = "default",
custom_preset: dict = None
) -> str:
"""
Render MIDI file using electronic music synthesis pipeline.
Args:
midi_path: Path to input MIDI file
output_path: Path for output MP3 file
genre: Genre preset name (e.g., "deep_house", "techno")
custom_preset: Optional custom preset dictionary
Returns:
Path to rendered MP3 file
"""
midi_path = str(Path(midi_path).resolve())
output_path = str(Path(output_path).resolve())
if not os.path.exists(midi_path):
raise FileNotFoundError(f"MIDI file not found: {midi_path}")
if custom_preset:
preset = custom_preset
print(f"Using custom preset")
else:
preset = get_preset(genre)
print(f"Using '{genre}' preset")
print(f"Rendering electronic music: {os.path.basename(midi_path)}")
bpm = get_midi_bpm(midi_path)
total_duration_ms = get_midi_duration_ms(midi_path)
print(f" BPM: {bpm}")
print(f" Duration: {total_duration_ms / 1000:.1f}s")
print(" Step 1/3: Synthesizing drums...")
drum_events = extract_drum_events(midi_path)
print(f" Found {len(drum_events)} drum hits")
drum_audio = render_drums_synthesized(drum_events, total_duration_ms, preset)
print(" Step 2/3: Synthesizing melodic parts (bass, pads, leads)...")
melodic_events = extract_melodic_events(midi_path)
total_melodic = sum(len(events) for events in melodic_events.values())
print(f" Found {total_melodic} melodic notes")
melodic_audio = render_melodic_synthesized(melodic_events, total_duration_ms, preset)
print(" Step 3/3: Mixing and exporting...")
final_audio = drum_audio.overlay(melodic_audio)
max_db = final_audio.max_dBFS
if max_db > -1.0:
headroom_db = -1.0 - max_db
print(f" Applying limiter: reducing {-headroom_db:.1f} dB to prevent clipping")
final_audio = final_audio + headroom_db
final_audio.export(output_path, format='mp3', bitrate='192k')
print(f"✓ Electronic music rendering complete!")
print(f" Output: {output_path}")
return output_path
def main():
"""Command-line interface."""
parser = argparse.ArgumentParser(
description='Render MIDI files with real-time electronic music synthesis'
)
parser.add_argument(
'input',
help='Input MIDI file path'
)
parser.add_argument(
'output',
help='Output MP3 file path'
)
parser.add_argument(
'--genre',
default='default',
choices=list_genres(),
help='Genre preset to use (default: default)'
)
parser.add_argument(
'--preset',
help='Path to custom preset JSON file'
)
parser.add_argument(
'--list-genres',
action='store_true',
help='List available genre presets and exit'
)
args = parser.parse_args()
if args.list_genres:
print("Available genre presets:")
for genre in list_genres():
preset = get_preset(genre)
desc = preset.get('description', '')
print(f" {genre}: {desc}")
return 0
try:
custom_preset = None
if args.preset:
with open(args.preset, 'r') as f:
custom_preset = json.load(f)
render_electronic(args.input, args.output, args.genre, custom_preset)
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
return 1
if __name__ == '__main__':
sys.exit(main())Related skills
FAQ
What are the two rendering pipelines?
A traditional FluidSynth soundfont pipeline for orchestral, classical, and acoustic music, and an electronic pipeline using real-time synthesis for house, techno, and EDM.
What output does it produce?
Downloadable MP3 files (not HTML players), written to /mnt/user-data/outputs/, optionally via WAV first.