Files
pytheory/pytheory/rhythm.py
T
kennethreitz 1e2f09e2ab LilyPond, MusicXML, and tablature export — v0.42.0
Three new export methods on Score:
- to_lilypond() — complete LilyPond source files for PDF engraving
- to_musicxml() — MusicXML 4.0 for MuseScore/Sibelius/Finale
- to_tab() — ASCII guitar/bass tablature (also on Part)

All three handle multi-part scores, bass clef detection, tied notes
across barlines, chords, and drum tone filtering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:02:09 -04:00

5557 lines
189 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Rhythm and duration primitives for PyTheory."""
import math
import struct
from dataclasses import dataclass
from enum import Enum
from typing import Optional
# ── Instrument presets ────────────────────────────────────────────────────────
# Predefined combinations of synth, envelope, effects, and parameters that
# approximate real instruments. Used by ``Score.part(instrument=...)``.
INSTRUMENTS = {
# ── Keys ──
"piano": {
"synth": "piano_synth", "envelope": "none",
"vel_to_filter": 3000,
},
"electric_piano": { # Rhodes
"synth": "rhodes_synth", "envelope": "none",
"chorus": 0.15, "chorus_rate": 1.0,
"tremolo_depth": 0.12, "tremolo_rate": 4.5,
"analog": 0.15,
},
"wurlitzer": {
"synth": "wurlitzer_synth", "envelope": "none",
"tremolo_depth": 0.18, "tremolo_rate": 5.0,
"analog": 0.2,
},
"pipe_organ": {
"synth": "pipe_organ_synth", "envelope": "none",
"reverb": 0.5, "reverb_type": "cathedral",
},
"organ": {
"synth": "organ_synth", "envelope": "organ",
"chorus": 0.2, "chorus_rate": 5.5,
"lowpass": 5000,
"phaser": 0.15, "phaser_rate": 0.4,
"analog": 0.15,
},
"harpsichord": {
"synth": "harpsichord_synth", "envelope": "none",
},
"celesta": {
"synth": "fm", "envelope": "mallet",
"fm_ratio": 3.0, "fm_index": 5.0,
"lowpass": 8000,
"reverb": 0.3, "reverb_type": "plate",
},
"music_box": {
"synth": "sine", "envelope": "mallet",
"lowpass": 6000,
"reverb": 0.25, "reverb_type": "plate",
},
# ── Strings ──
"violin": {
"synth": "strings_synth", "envelope": "bowed",
"detune": 2, "lowpass": 5000,
"humanize": 0.15, "vel_to_filter": 1500,
"noise_mix": 0.03,
},
"viola": {
"synth": "strings_synth", "envelope": "bowed",
"detune": 2, "lowpass": 3500,
"humanize": 0.15, "vel_to_filter": 1200,
"noise_mix": 0.03,
},
"cello": {
"synth": "cello_synth", "envelope": "bowed",
"humanize": 0.15, "vel_to_filter": 1000,
},
"contrabass": {
"synth": "strings_synth", "envelope": "bowed",
"detune": 2, "lowpass": 1500,
"humanize": 0.1, "vel_to_filter": 800,
"sub_osc": 0.15,
},
"string_ensemble": {
"synth": "strings_synth", "envelope": "pad",
"detune": 10, "spread": 0.5,
"chorus": 0.2, "chorus_rate": 0.5,
"lowpass": 4000,
"noise_mix": 0.02, "saturation": 0.05,
},
# ── Woodwinds ──
"flute": {
"synth": "flute_synth", "envelope": "strings",
"humanize": 0.2,
"vel_to_filter": 2000,
},
"clarinet": {
"synth": "clarinet_synth", "envelope": "strings",
"humanize": 0.15,
"vel_to_filter": 1500,
},
"oboe": {
"synth": "oboe_synth", "envelope": "strings",
"humanize": 0.15,
"vel_to_filter": 1000,
},
"bassoon": {
"synth": "saw", "envelope": "strings",
"lowpass": 2000,
"humanize": 0.15, "noise_mix": 0.04,
"vel_to_filter": 800,
},
# ── Brass ──
"trumpet": {
"synth": "trumpet_synth", "envelope": "bowed",
"humanize": 0.15, "vel_to_filter": 2000,
},
"trombone": {
"synth": "trumpet_synth", "envelope": "strings",
"lowpass": 2500,
"humanize": 0.15, "vel_to_filter": 1500,
},
"french_horn": {
"synth": "saw", "envelope": "strings",
"detune": 4, "lowpass": 2000,
"chorus": 0.1,
"humanize": 0.15, "vel_to_filter": 1200,
"saturation": 0.1,
},
"tuba": {
"synth": "saw", "envelope": "strings",
"detune": 3, "lowpass": 1200,
"humanize": 0.1, "vel_to_filter": 600,
"sub_osc": 0.2,
},
"brass_ensemble": {
"synth": "saw", "envelope": "strings",
"detune": 10, "spread": 0.4,
"lowpass": 3000,
"chorus": 0.15,
},
# ── Plucked ──
"acoustic_guitar": {
"synth": "acoustic_guitar_synth", "envelope": "none",
"humanize": 0.2, "saturation": 0.05,
},
"electric_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"cabinet": 1.0, "cabinet_brightness": 0.6,
"humanize": 0.15,
},
"clean_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"cabinet": 1.0, "cabinet_brightness": 0.7,
"chorus": 0.15, "chorus_rate": 1.0,
"reverb": 0.2, "reverb_type": "spring",
"humanize": 0.15,
},
"crunch_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.3,
"distortion": 0.5, "distortion_drive": 4.0,
"cabinet": 1.0, "cabinet_brightness": 0.5,
"humanize": 0.15,
},
"distorted_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.3,
"distortion": 0.7, "distortion_drive": 5.0,
"cabinet": 1.0, "cabinet_brightness": 0.5,
"humanize": 0.15,
},
"orange_crunch": {
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.4,
"distortion": 0.7, "distortion_drive": 6.0,
"cabinet": 1.0, "cabinet_brightness": 0.4,
"humanize": 0.15,
},
"metal_guitar": {
"synth": "electric_guitar_synth", "envelope": "none",
"saturation": 0.35,
"distortion": 0.8, "distortion_drive": 7.0,
"cabinet": 1.0, "cabinet_brightness": 0.5,
"highpass": 80,
"detune": 4,
"humanize": 0.1,
},
"bass_guitar": {
"synth": "bass_guitar_synth", "envelope": "none",
"humanize": 0.1, "sub_osc": 0.15,
},
"upright_bass": {
"synth": "upright_bass_synth", "envelope": "none",
"humanize": 0.15, "saturation": 0.1,
},
"harp": {
"synth": "harp_synth", "envelope": "none",
"reverb": 0.3, "reverb_type": "plate",
},
"sitar": {
"synth": "saw", "envelope": "pluck",
"detune": 12, "lowpass": 3000, "lowpass_q": 1.5,
"humanize": 0.2,
},
"pedal_steel": {
"synth": "pedal_steel_synth", "envelope": "strings",
"reverb": 0.3, "reverb_type": "spring",
"humanize": 0.15,
},
"theremin": {
"synth": "theremin_synth", "envelope": "pad",
"legato": True, "glide": 0.05,
"reverb": 0.3, "reverb_type": "plate",
},
"kalimba": {
"synth": "kalimba_synth", "envelope": "none",
"reverb": 0.35, "reverb_type": "plate",
},
"steel_drum": {
"synth": "steel_drum_synth", "envelope": "none",
"reverb": 0.3, "reverb_type": "plate",
},
"harmonium": {
"synth": "harmonium_synth", "envelope": "organ",
"reverb": 0.2, "reverb_type": "taj_mahal",
"humanize": 0.15,
},
"accordion": {
"synth": "accordion_synth", "envelope": "organ",
"humanize": 0.15,
},
"didgeridoo": {
"synth": "didgeridoo_synth", "envelope": "pad",
"lowpass": 1500,
"reverb": 0.4, "reverb_type": "cave",
},
"bagpipe": {
"synth": "bagpipe_synth", "envelope": "organ",
"lowpass": 4000,
},
"banjo": {
"synth": "banjo_synth", "envelope": "none",
"humanize": 0.2,
},
"mandolin": {
"synth": "mandolin_synth", "envelope": "none",
"humanize": 0.2,
},
"mandola": {
"synth": "mandolin_synth", "envelope": "none",
"lowpass": 3000,
"humanize": 0.2,
},
"ukulele": {
"synth": "ukulele_synth", "envelope": "none",
"humanize": 0.2,
},
"koto": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 4000,
"reverb": 0.2,
},
"sitar": {
"synth": "sitar_synth", "envelope": "none",
"lowpass": 4500,
"humanize": 0.2,
},
"crotales": {
"synth": "crotales_synth", "envelope": "none",
"reverb": 0.3,
"humanize": 0.2,
},
"tingsha": {
"synth": "tingsha_synth", "envelope": "none",
"reverb": 0.4,
"humanize": 0.2,
},
"singing_bowl": {
"synth": "singing_bowl_strike_synth", "envelope": "none",
"reverb": 0.5,
"humanize": 0.2,
},
"singing_bowl_ring": {
"synth": "singing_bowl_ring_synth", "envelope": "none",
"reverb": 0.5,
"humanize": 0.2,
},
# ── Synth presets ──
"synth_lead": {
"synth": "saw", "envelope": "pluck",
"detune": 8, "lowpass": 3000,
"delay": 0.2, "delay_time": 0.25, "delay_feedback": 0.3,
"filter_attack": 0.01, "filter_decay": 0.3,
"filter_sustain": 0.2, "filter_amount": 3000,
"analog": 0.3,
},
"synth_pad": {
"synth": "supersaw", "envelope": "pad",
"detune": 12, "spread": 0.6,
"chorus": 0.2,
"phaser": 0.3, "phaser_rate": 0.3,
"sub_osc": 0.2,
"analog": 0.4,
},
"synth_bass": {
"synth": "saw", "envelope": "pluck",
"lowpass": 800, "lowpass_q": 1.3,
"filter_attack": 0.005, "filter_decay": 0.2,
"filter_sustain": 0.0, "filter_amount": 2000,
"sub_osc": 0.4,
"analog": 0.2,
},
"acid_bass": {
"synth": "saw", "envelope": "pad",
"legato": True, "glide": 0.03,
"distortion": 0.7, "distortion_drive": 8.0,
"lowpass": 800, "lowpass_q": 5.0,
"filter_attack": 0.005, "filter_decay": 0.15,
"filter_sustain": 0.0, "filter_amount": 4000,
"vel_to_filter": 3000,
"analog": 0.3,
},
"granular_pad": {
"synth": "granular_synth", "envelope": "pad",
"reverb": 0.4, "reverb_type": "cathedral",
"analog": 0.3,
},
"vocal": {
"synth": "vocal_synth", "envelope": "strings",
"reverb": 0.3, "reverb_type": "hall",
"humanize": 0.15,
},
"choir": {
"synth": "choir_synth", "envelope": "none",
"detune": 6, "spread": 0.3, "ensemble": 6,
"reverb": 0.45, "reverb_type": "cathedral",
},
"granular_texture": {
"synth": "granular_synth", "envelope": "none",
"reverb": 0.5, "reverb_type": "taj_mahal",
"delay": 0.3, "delay_time": 0.4, "delay_feedback": 0.4,
},
"808_bass": {
"synth": "sine", "envelope": "piano",
"distortion": 0.4, "distortion_drive": 2.5,
"lowpass": 200, "lowpass_q": 1.5,
"sub_osc": 0.5, "saturation": 0.2,
},
# ── Mellotron ──
"mellotron": {
"synth": "mellotron_synth", "envelope": "organ",
"reverb": 0.3, "reverb_type": "plate",
"humanize": 0.2,
},
"mellotron_strings": {
"synth": "mellotron_synth", "envelope": "organ",
"reverb": 0.3, "reverb_type": "plate",
"humanize": 0.2,
},
"mellotron_flute": {
"synth": "mellotron_synth", "envelope": "organ",
"synth_kw": {"tape": "flute"},
"reverb": 0.35, "reverb_type": "hall",
"humanize": 0.2,
},
"mellotron_choir": {
"synth": "mellotron_synth", "envelope": "organ",
"synth_kw": {"tape": "choir"},
"reverb": 0.4, "reverb_type": "cathedral",
"humanize": 0.2,
},
# ── Analog oscillator presets ──
"sync_lead": {
"synth": "hard_sync", "envelope": "pluck",
"synth_kw": {"slave_ratio": 1.5},
"detune": 8, "lowpass": 4000,
"filter_attack": 0.01, "filter_decay": 0.25,
"filter_sustain": 0.3, "filter_amount": 3000,
"delay": 0.15, "delay_time": 0.2, "delay_feedback": 0.25,
"analog": 0.3,
},
"sync_lead_bright": {
"synth": "hard_sync", "envelope": "pluck",
"synth_kw": {"slave_ratio": 2.5},
"detune": 10, "lowpass": 6000,
"filter_attack": 0.005, "filter_decay": 0.2,
"filter_sustain": 0.1, "filter_amount": 4000,
"analog": 0.3,
},
"ring_mod_bell": {
"synth": "ring_mod", "envelope": "bell",
"synth_kw": {"mod_ratio": 2.1},
"reverb": 0.4, "reverb_type": "plate",
},
"ring_mod_metallic": {
"synth": "ring_mod", "envelope": "mallet",
"synth_kw": {"mod_ratio": 3.7},
"reverb": 0.3, "reverb_type": "hall",
"delay": 0.2, "delay_time": 0.3, "delay_feedback": 0.3,
},
"wavefold_warm": {
"synth": "wavefold", "envelope": "organ",
"synth_kw": {"folds": 2.0},
"lowpass": 3000, "lowpass_q": 1.2,
"analog": 0.3,
},
"wavefold_gnarly": {
"synth": "wavefold", "envelope": "pluck",
"synth_kw": {"folds": 5.0},
"lowpass": 2000, "lowpass_q": 2.5,
"filter_attack": 0.01, "filter_decay": 0.3,
"filter_sustain": 0.1, "filter_amount": 4000,
"distortion": 0.3, "distortion_drive": 2.0,
"analog": 0.3,
},
"drift_saw": {
"synth": "drift", "envelope": "organ",
"synth_kw": {"shape": "saw", "drift_amount": 0.15},
"detune": 10,
"analog": 0.4,
},
"drift_square": {
"synth": "drift", "envelope": "organ",
"synth_kw": {"shape": "square", "drift_amount": 0.15},
"detune": 10,
"analog": 0.4,
},
"analog_pad": {
"synth": "drift", "envelope": "pad",
"synth_kw": {"shape": "saw", "drift_amount": 0.12},
"detune": 12, "spread": 0.5,
"chorus": 0.2,
"lowpass": 2500, "lowpass_q": 1.0,
"analog": 0.5,
},
"analog_bass": {
"synth": "drift", "envelope": "pluck",
"synth_kw": {"shape": "saw", "drift_amount": 0.1},
"lowpass": 600, "lowpass_q": 2.0,
"filter_attack": 0.005, "filter_decay": 0.15,
"filter_sustain": 0.0, "filter_amount": 2000,
"sub_osc": 0.4,
"analog": 0.3,
},
# ── Percussion / Mallet ──
"vibraphone": {
"synth": "vibraphone_synth", "envelope": "none",
"reverb": 0.3, "reverb_type": "plate",
},
"marimba": {
"synth": "marimba_synth", "envelope": "mallet",
},
"xylophone": {
"synth": "fm", "envelope": "pluck",
"fm_ratio": 3.0, "fm_index": 5.0,
"lowpass": 6000,
},
"glockenspiel": {
"synth": "fm", "envelope": "mallet",
"fm_ratio": 4.0, "fm_index": 6.0,
"lowpass": 8000,
"reverb": 0.2,
},
"tubular_bells": {
"synth": "fm", "envelope": "mallet",
"fm_ratio": 2.0, "fm_index": 3.0,
"reverb": 0.4, "reverb_type": "cathedral",
},
"timpani": {
"synth": "timpani_synth", "envelope": "none",
"reverb": 0.4, "reverb_type": "cathedral",
},
# ── Woodwinds (continued) ──
"saxophone": {
"synth": "saxophone_synth", "envelope": "bowed",
"humanize": 0.15,
},
"alto_sax": {
"synth": "saxophone_synth", "envelope": "bowed",
"humanize": 0.15,
},
"tenor_sax": {
"synth": "saxophone_synth", "envelope": "bowed",
"humanize": 0.15,
},
"bari_sax": {
"synth": "saxophone_synth", "envelope": "bowed",
"humanize": 0.15, "sub_osc": 0.15,
},
}
class Duration(Enum):
"""Note durations in beats (quarter note = 1 beat)."""
WHOLE = 4.0
HALF = 2.0
QUARTER = 1.0
EIGHTH = 0.5
SIXTEENTH = 0.25
DOTTED_HALF = 3.0
DOTTED_QUARTER = 1.5
TRIPLET_QUARTER = 2 / 3
# Arithmetic — lets you write ``Duration.WHOLE * 2`` → 8.0 beats.
def __mul__(self, other):
return self.value * other
def __rmul__(self, other):
return self.value * other
def __truediv__(self, other):
return self.value / other
def __add__(self, other):
if isinstance(other, Duration):
return self.value + other.value
return self.value + other
def __radd__(self, other):
return other + self.value
class TimeSignature:
"""A musical time signature like 4/4 or 6/8."""
def __init__(self, beats: int = 4, unit: int = 4):
self.beats = beats
self.unit = unit
@classmethod
def from_string(cls, s: str) -> "TimeSignature":
"""Parse '4/4', '3/4', '6/8' etc."""
top, bottom = s.split("/")
return cls(beats=int(top), unit=int(bottom))
@property
def beats_per_measure(self) -> float:
"""Total beats in one measure (in quarter-note units)."""
return self.beats * (4 / self.unit)
def __repr__(self):
return f"{self.beats}/{self.unit}"
def __eq__(self, other):
if isinstance(other, TimeSignature):
return self.beats == other.beats and self.unit == other.unit
return NotImplemented
@dataclass
class Note:
"""A pairing of a sound (Tone, Chord, or None for rest) with a duration.
The optional ``bend`` field specifies a pitch bend in semitones
applied over the note's duration. Positive = bend up, negative = down.
For example, ``bend=2`` bends the note up a whole step by the end.
"""
tone: object
duration: Duration
velocity: int = 100
bend: float = 0.0
bend_type: str = "smooth" # "smooth" (log), "linear", "late"
lyric: str = "" # syllable for vocal synth
articulation: str = "" # "", "staccato", "legato", "marcato", "tenuto", "accent", "fermata"
_hold: bool = False # if True, don't advance beat position
@property
def beats(self) -> float:
if self._hold:
return 0.0
return self.duration.value
class _RawDuration:
"""A duck-typed Duration wrapper for raw float beat values."""
__slots__ = ("value",)
def __init__(self, beats: float):
self.value = float(beats)
def __repr__(self):
return f"{self.value} beats"
def Rest(duration=Duration.QUARTER) -> Note:
"""Create a rest (silent note) with the given duration."""
if isinstance(duration, (int, float)):
duration = _RawDuration(duration)
return Note(tone=None, duration=duration, velocity=0)
# ---------------------------------------------------------------------------
# MIDI variable-length quantity encoder (copied from play.py to avoid
# pulling in the PortAudio dependency).
# ---------------------------------------------------------------------------
def _vlq(value):
"""Encode an integer as MIDI variable-length quantity bytes."""
result = []
result.append(value & 0x7F)
value >>= 7
while value:
result.append((value & 0x7F) | 0x80)
value >>= 7
return bytes(reversed(result))
# ── Drum patterns ─────────────────────────────────────────────────────────
class DrumSound(Enum):
"""General MIDI percussion note numbers (channel 10).
These map to the GM drum map standard supported by virtually
all MIDI devices and DAWs.
"""
KICK = 36
SNARE = 38
RIMSHOT = 37
CLAP = 39
CLOSED_HAT = 42
OPEN_HAT = 46
PEDAL_HAT = 44
LOW_TOM = 45
MID_TOM = 47
HIGH_TOM = 50
CRASH = 49
RIDE = 51
RIDE_BELL = 53
COWBELL = 56
CLAVE = 75
SHAKER = 70
TAMBOURINE = 54
CONGA_HIGH = 63
CONGA_LOW = 64
BONGO_HIGH = 60
BONGO_LOW = 61
TIMBALE_HIGH = 65
TIMBALE_LOW = 66
AGOGO_HIGH = 67
AGOGO_LOW = 68
GUIRO = 73
MARACAS = 70
# Tabla sounds
TABLA_NA = 86 # sharp dayan (right drum) rim hit
TABLA_TIN = 87 # open dayan ring
TABLA_GE = 88 # deep bayan (left drum) bass
TABLA_DHA = 89 # both drums (Na + Ge)
TABLA_TIT = 90 # light dayan flick
TABLA_KE = 91 # muted bayan slap
# Dhol sounds
DHOL_DAGGA = 92 # heavy bass side (dagga stick)
DHOL_TILLI = 93 # thin treble side (tilli stick)
DHOL_BOTH = 94 # both sides
# Dholak sounds
DHOLAK_GE = 95 # bass side (open palm)
DHOLAK_NA = 96 # treble side (fingers)
DHOLAK_TIT = 97 # light treble tap
# Mridangam sounds
MRIDANGAM_THAM = 98 # bass stroke (thoppi/left head)
MRIDANGAM_NAM = 99 # treble ring (valanthalai/right head)
MRIDANGAM_DIN = 100 # both heads
MRIDANGAM_THA = 101 # muted treble
TABLA_GE_BEND = 108 # bayan with upward pitch bend (palm press)
# Djembe sounds
DJEMBE_BASS = 102 # open bass (center of head)
DJEMBE_TONE = 103 # open tone (edge, fingers together)
DJEMBE_SLAP = 104 # slap (edge, fingers spread, sharp crack)
# Doumbek (darbuka) sounds
DOUMBEK_DUM = 112 # center of head, deep bass
DOUMBEK_TEK = 113 # edge of head, sharp high
DOUMBEK_KA = 114 # muted edge slap
# Cajon sounds
CAJON_BASS = 108 # center of face, deep thump
CAJON_SLAP = 109 # top edge, wood crack (no snare wires)
CAJON_TAP = 110 # light finger tap
CAJON_SLAP_SNARE = 111 # top edge with snare wires engaged
# Metal kit — tighter, punchier, more attack
METAL_KICK = 105 # clicky, punchy, tight
METAL_SNARE = 106 # crack, bright, cutting
METAL_HAT = 107 # tight, short, precise
# Marching percussion
MARCH_SNARE = 115 # tight, high-tension kevlar head, snare buzz
MARCH_RIMSHOT = 116 # stick hits rim + head simultaneously, cracking
MARCH_CLICK = 118 # stick click — sticks hit together, no drum
# Quads (tenor drums) — 4 drums high to low + spock (rim)
QUAD_1 = 119 # highest tenor drum
QUAD_2 = 120 # second tenor
QUAD_3 = 121 # third tenor
QUAD_4 = 122 # lowest tenor (floor tom-ish)
QUAD_SPOCK = 123 # rim click on quad shell
# Marching bass drums — 5 drums pitched high to low
BASS_1 = 124 # highest (smallest) bass drum
BASS_2 = 125 # second
BASS_3 = 126 # middle
BASS_4 = 127 # fourth
BASS_5 = 80 # lowest (biggest) bass drum
# Effects / world percussion
RAINSTICK = 81 # cascading pebbles through cactus tube (steep angle)
RAINSTICK_SLOW = 128 # gentle trickle (shallow angle)
OCEAN_DRUM = 82 # tilting drum with steel beads — surf wash
CABASA = 83 # metal bead chain wrapped around cylinder
WIND_CHIMES = 84 # suspended metal tubes struck by wind/hand
FINGER_CYMBAL = 85 # single small cymbal tap (zill)
class _DrumTone:
"""Wrapper so a DrumSound can be placed in a Part's note list."""
__slots__ = ('sound',)
def __init__(self, sound: DrumSound):
self.sound = sound
def pitch(self, **kwargs):
return -self.sound.value
class _Hit:
"""A single drum hit at a specific position in a pattern."""
__slots__ = ("sound", "position", "velocity")
def __init__(self, sound: DrumSound, position: float, velocity: int = 100):
self.sound = sound
self.position = position # in beats
self.velocity = velocity
def __repr__(self):
return f"Hit({self.sound.name}, beat={self.position}, vel={self.velocity})"
class Pattern:
"""A drum pattern — a repeating rhythmic figure.
Patterns are defined as a list of hits within a fixed number of beats.
They can be rendered to a Score for MIDI export, or combined with
chord progressions.
Example::
>>> pattern = Pattern.preset("rock")
>>> print(pattern)
<Pattern 'rock' 4/4 4.0 beats 12 hits>
"""
def __init__(self, name: str, hits: list[_Hit], beats: float = 4.0,
time_signature: str = "4/4"):
self.name = name
self.hits = hits
self.beats = beats
self.time_signature_str = time_signature
def __repr__(self):
return (f"<Pattern {self.name!r} {self.time_signature_str} "
f"{self.beats} beats {len(self.hits)} hits>")
def to_score(self, repeats: int = 4, bpm: int = 120) -> "Score":
"""Render this pattern to a Score for MIDI export.
Args:
repeats: Number of times to repeat the pattern.
bpm: Tempo in beats per minute.
Returns:
A Score containing drum hits as MIDI percussion notes.
"""
score = Score(self.time_signature_str, bpm=bpm)
score.add_pattern(self, repeats=repeats)
return score
# ── Fills ─────────────────────────────────────────────────────────
_FILLS: dict[str, dict] = {}
@classmethod
def fill(cls, name: str) -> "Pattern":
"""Load a named 1-bar drum fill.
Available fills: rock, rock crash, jazz, jazz brush, salsa, samba,
funk, metal, blast, buildup, breakdown.
Example::
>>> Pattern.fill("rock")
<Pattern 'rock fill' 4/4 4.0 beats ...>
"""
if name not in cls._FILLS:
raise ValueError(
f"Unknown fill: {name!r}. "
f"Available: {', '.join(cls.list_fills())}")
data = cls._FILLS[name]
return cls(**data)
@classmethod
def list_fills(cls) -> list[str]:
"""Return a list of all available fill names."""
return sorted(cls._FILLS.keys())
# ── Presets ───────────────────────────────────────────────────────
_PRESETS: dict[str, dict] = {}
@classmethod
def preset(cls, name: str) -> "Pattern":
"""Load a named drum pattern preset.
Available presets:
- **rock** — standard 4/4 rock beat (kick-snare-hat)
- **jazz** — swing ride pattern with ghost notes
- **bebop** — fast jazz ride with syncopated kick/snare
- **bossa nova** — Brazilian 2-bar pattern with cross-stick
- **salsa** — clave-driven Afro-Cuban pattern
- **funk** — syncopated 16th-note groove
- **reggae** — one-drop pattern (snare on 3)
- **waltz** — 3/4 pattern (oom-pah-pah)
- **12/8 blues** — slow blues shuffle
- **samba** — Brazilian carnival pattern
- **son clave 3-2** — the Afro-Cuban rhythmic key
- **son clave 2-3** — reversed clave
Example::
>>> Pattern.preset("salsa")
<Pattern 'salsa' 4/4 8.0 beats ...>
"""
if name not in cls._PRESETS:
raise ValueError(
f"Unknown preset: {name!r}. "
f"Available: {', '.join(cls.list_presets())}")
data = cls._PRESETS[name]
return cls(**data)
@classmethod
def list_presets(cls) -> list[str]:
"""Return a list of all available preset names."""
return sorted(cls._PRESETS.keys())
def _h(sound, position, velocity=100):
"""Shorthand for building hit lists."""
return _Hit(sound, position, velocity)
K = DrumSound.KICK
S = DrumSound.SNARE
CH = DrumSound.CLOSED_HAT
OH = DrumSound.OPEN_HAT
RD = DrumSound.RIDE
RB = DrumSound.RIDE_BELL
RS = DrumSound.RIMSHOT
CR = DrumSound.CRASH
CL = DrumSound.CLAVE
CB = DrumSound.COWBELL
CGA = DrumSound.CONGA_HIGH
CGB = DrumSound.CONGA_LOW
BGH = DrumSound.BONGO_HIGH
BGL = DrumSound.BONGO_LOW
TB = DrumSound.TAMBOURINE
TBH = DrumSound.TIMBALE_HIGH
TBL = DrumSound.TIMBALE_LOW
SH = DrumSound.SHAKER
HT = DrumSound.HIGH_TOM
MT = DrumSound.MID_TOM
LT = DrumSound.LOW_TOM
# ── Pattern presets ───────────────────────────────────────────────────────
Pattern._PRESETS["rock"] = dict(
name="rock",
time_signature="4/4",
beats=4.0,
hits=[
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.5),
_h(S, 1.0), _h(CH, 1.0), _h(CH, 1.5),
_h(K, 2.0), _h(CH, 2.0), _h(CH, 2.5),
_h(S, 3.0), _h(CH, 3.0), _h(CH, 3.5),
],
)
Pattern._PRESETS["jazz"] = dict(
name="jazz",
time_signature="4/4",
beats=4.0,
hits=[
# Ride: swing pattern (1, 2-and, 3, 4-and)
_h(RD, 0.0), _h(RD, 1.0), _h(CH, 1.67, 60),
_h(RD, 2.0), _h(RD, 3.0), _h(CH, 3.67, 60),
# Kick feathered on 1 and 3
_h(K, 0.0, 50), _h(K, 2.0, 50),
# Hi-hat foot on 2 and 4
_h(DrumSound.PEDAL_HAT, 1.0), _h(DrumSound.PEDAL_HAT, 3.0),
],
)
Pattern._PRESETS["bebop"] = dict(
name="bebop",
time_signature="4/4",
beats=4.0,
hits=[
# Ride: all four beats + swing upbeats
_h(RD, 0.0), _h(RD, 0.67, 70),
_h(RD, 1.0), _h(RD, 1.67, 70),
_h(RD, 2.0), _h(RD, 2.67, 70),
_h(RD, 3.0), _h(RD, 3.67, 70),
# Hi-hat on 2 and 4
_h(DrumSound.PEDAL_HAT, 1.0), _h(DrumSound.PEDAL_HAT, 3.0),
# Syncopated kick
_h(K, 0.0, 60), _h(K, 2.67, 50),
# Ghost snare
_h(S, 3.5, 40),
],
)
Pattern._PRESETS["bossa nova"] = dict(
name="bossa nova",
time_signature="4/4",
beats=8.0, # 2-bar pattern
hits=[
# Bar 1
_h(RS, 0.0), _h(K, 0.0, 80),
_h(CH, 0.5), _h(CH, 1.0), _h(CH, 1.5),
_h(RS, 2.0), _h(CH, 2.5),
_h(RS, 3.0), _h(CH, 3.0), _h(CH, 3.5),
# Bar 2
_h(CH, 4.0), _h(K, 4.0, 80), _h(CH, 4.5),
_h(RS, 5.0), _h(CH, 5.5),
_h(CH, 6.0), _h(RS, 6.5),
_h(CH, 7.0), _h(CH, 7.5),
],
)
Pattern._PRESETS["salsa"] = dict(
name="salsa",
time_signature="4/4",
beats=8.0, # 2-bar clave cycle
hits=[
# Son clave 3-2
_h(CL, 0.0), _h(CL, 1.5), _h(CL, 3.0),
_h(CL, 5.0), _h(CL, 6.5),
# Congas (tumbao)
_h(CGB, 0.0, 80), _h(CGA, 0.5, 70), _h(CGA, 1.0, 90),
_h(CGB, 2.0, 80), _h(CGA, 2.5, 70), _h(CGA, 3.0, 90),
_h(CGB, 4.0, 80), _h(CGA, 4.5, 70), _h(CGA, 5.0, 90),
_h(CGB, 6.0, 80), _h(CGA, 6.5, 70), _h(CGA, 7.0, 90),
# Cowbell (campana)
_h(CB, 0.0), _h(CB, 1.0), _h(CB, 2.0), _h(CB, 3.0),
_h(CB, 4.0), _h(CB, 5.0), _h(CB, 6.0), _h(CB, 7.0),
# Kick on 1 and the-and-of-2
_h(K, 0.0, 90), _h(K, 2.5, 70),
_h(K, 4.0, 90), _h(K, 6.5, 70),
],
)
Pattern._PRESETS["funk"] = dict(
name="funk",
time_signature="4/4",
beats=4.0,
hits=[
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.25, 60),
_h(CH, 0.5), _h(CH, 0.75, 60),
_h(S, 1.0), _h(CH, 1.0), _h(CH, 1.25, 60),
_h(K, 1.5), _h(CH, 1.5), _h(CH, 1.75, 60),
_h(CH, 2.0), _h(K, 2.25, 80), _h(CH, 2.25, 60),
_h(CH, 2.5), _h(CH, 2.75, 60),
_h(S, 3.0), _h(CH, 3.0), _h(CH, 3.25, 60),
_h(CH, 3.5), _h(K, 3.75, 70), _h(CH, 3.75, 60),
],
)
Pattern._PRESETS["reggae"] = dict(
name="reggae",
time_signature="4/4",
beats=4.0,
hits=[
# One-drop: kick + snare on beat 3
_h(CH, 0.0), _h(CH, 0.5),
_h(CH, 1.0), _h(CH, 1.5),
_h(K, 2.0), _h(S, 2.0), _h(CH, 2.0), _h(CH, 2.5),
_h(CH, 3.0), _h(CH, 3.5),
],
)
Pattern._PRESETS["waltz"] = dict(
name="waltz",
time_signature="3/4",
beats=3.0,
hits=[
_h(K, 0.0), _h(CR, 0.0, 60),
_h(CH, 1.0), _h(CH, 2.0),
],
)
Pattern._PRESETS["12/8 blues"] = dict(
name="12/8 blues",
time_signature="12/8",
beats=6.0, # 12 eighth notes = 6 quarter beats
hits=[
# Shuffle feel
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.67, 70), _h(CH, 1.0),
_h(S, 1.5), _h(CH, 1.67, 70), _h(CH, 2.0),
_h(K, 3.0), _h(CH, 3.0), _h(CH, 3.67, 70), _h(CH, 4.0),
_h(S, 4.5), _h(CH, 4.67, 70), _h(CH, 5.0),
],
)
Pattern._PRESETS["samba"] = dict(
name="samba",
time_signature="4/4",
beats=4.0,
hits=[
_h(K, 0.0), _h(SH, 0.0), _h(SH, 0.25),
_h(SH, 0.5), _h(SH, 0.75),
_h(S, 1.0, 80), _h(SH, 1.0), _h(SH, 1.25),
_h(K, 1.5), _h(SH, 1.5), _h(SH, 1.75),
_h(SH, 2.0), _h(K, 2.25, 70), _h(SH, 2.25),
_h(SH, 2.5), _h(SH, 2.75),
_h(S, 3.0, 80), _h(SH, 3.0), _h(SH, 3.25),
_h(K, 3.5), _h(SH, 3.5), _h(SH, 3.75),
],
)
Pattern._PRESETS["son clave 3-2"] = dict(
name="son clave 3-2",
time_signature="4/4",
beats=8.0,
hits=[
_h(CL, 0.0), _h(CL, 1.5), _h(CL, 3.0),
_h(CL, 5.0), _h(CL, 6.5),
],
)
Pattern._PRESETS["son clave 2-3"] = dict(
name="son clave 2-3",
time_signature="4/4",
beats=8.0,
hits=[
_h(CL, 1.0), _h(CL, 2.5),
_h(CL, 4.0), _h(CL, 5.5), _h(CL, 7.0),
],
)
Pattern._PRESETS["rumba clave 3-2"] = dict(
name="rumba clave 3-2",
time_signature="4/4",
beats=8.0,
hits=[
_h(CL, 0.0), _h(CL, 1.5), _h(CL, 3.5),
_h(CL, 5.0), _h(CL, 6.5),
],
)
Pattern._PRESETS["rumba clave 2-3"] = dict(
name="rumba clave 2-3",
time_signature="4/4",
beats=8.0,
hits=[
_h(CL, 1.0), _h(CL, 2.5),
_h(CL, 4.0), _h(CL, 5.5), _h(CL, 7.5),
],
)
Pattern._PRESETS["cascara"] = dict(
name="cascara",
time_signature="4/4",
beats=8.0,
hits=[
# Shell pattern played on timbale shell — the backbone of salsa
_h(TBH, 0.0), _h(TBH, 0.5), _h(TBH, 1.5),
_h(TBH, 2.0), _h(TBH, 3.0), _h(TBH, 3.5),
_h(TBH, 4.5), _h(TBH, 5.0), _h(TBH, 5.5),
_h(TBH, 6.5), _h(TBH, 7.0),
],
)
Pattern._PRESETS["mozambique"] = dict(
name="mozambique",
time_signature="4/4",
beats=4.0,
hits=[
_h(K, 0.0), _h(CB, 0.0), _h(CB, 0.5),
_h(CB, 1.0), _h(S, 1.0, 80), _h(CB, 1.5),
_h(K, 2.0), _h(CB, 2.0), _h(CB, 2.5),
_h(CB, 3.0), _h(S, 3.0, 80), _h(CB, 3.5),
_h(CGA, 0.5, 70), _h(CGB, 1.0, 80),
_h(CGA, 2.5, 70), _h(CGB, 3.0, 80),
],
)
Pattern._PRESETS["nanigo"] = dict(
name="nanigo",
time_signature="6/8",
beats=3.0,
hits=[
# 6/8 Afro-Cuban bell pattern
_h(CB, 0.0), _h(CB, 0.5), _h(CB, 1.0),
_h(CB, 1.5), _h(CB, 2.5),
],
)
Pattern._PRESETS["guaguanco"] = dict(
name="guaguanco",
time_signature="4/4",
beats=8.0,
hits=[
# Rumba guaguanco conga pattern
_h(CGB, 0.0, 90), _h(CGA, 0.5, 60), _h(CGB, 1.0, 70),
_h(CGA, 1.5, 90), _h(CGB, 2.0, 60), _h(CGA, 2.5, 80),
_h(CGA, 3.0, 60), _h(CGB, 3.5, 90),
_h(CGB, 4.0, 90), _h(CGA, 4.5, 60), _h(CGB, 5.0, 70),
_h(CGA, 5.5, 90), _h(CGB, 6.0, 60), _h(CGA, 6.5, 80),
_h(CGA, 7.0, 60), _h(CGB, 7.5, 90),
# Clave underneath
_h(CL, 0.0), _h(CL, 1.5), _h(CL, 3.5),
_h(CL, 5.0), _h(CL, 6.5),
],
)
Pattern._PRESETS["tresillo"] = dict(
name="tresillo",
time_signature="4/4",
beats=4.0,
hits=[
# 3+3+2 — the most fundamental Afro-Latin cell
_h(K, 0.0), _h(K, 1.5), _h(K, 3.0),
],
)
Pattern._PRESETS["habanera"] = dict(
name="habanera",
time_signature="4/4",
beats=4.0,
hits=[
# Habanera / tango rhythm
_h(K, 0.0), _h(K, 1.5), _h(K, 2.0), _h(K, 3.0),
],
)
Pattern._PRESETS["second line"] = dict(
name="second line",
time_signature="4/4",
beats=4.0,
hits=[
# New Orleans second line snare pattern
_h(S, 0.0), _h(S, 0.5, 60), _h(S, 0.75, 50),
_h(S, 1.0), _h(S, 1.5, 60),
_h(S, 2.0), _h(S, 2.5, 60), _h(S, 2.75, 50),
_h(S, 3.0), _h(S, 3.5, 60),
_h(K, 0.0), _h(K, 2.0),
],
)
Pattern._PRESETS["train beat"] = dict(
name="train beat",
time_signature="4/4",
beats=4.0,
hits=[
# Country train beat — cross-stick on every beat, kick on 1 and 3
_h(RS, 0.0), _h(CH, 0.0), _h(CH, 0.25), _h(CH, 0.5), _h(CH, 0.75),
_h(RS, 1.0), _h(CH, 1.0), _h(CH, 1.25), _h(CH, 1.5), _h(CH, 1.75),
_h(RS, 2.0), _h(CH, 2.0), _h(CH, 2.25), _h(CH, 2.5), _h(CH, 2.75),
_h(RS, 3.0), _h(CH, 3.0), _h(CH, 3.25), _h(CH, 3.5), _h(CH, 3.75),
_h(K, 0.0), _h(K, 2.0),
],
)
Pattern._PRESETS["half time"] = dict(
name="half time",
time_signature="4/4",
beats=4.0,
hits=[
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.5),
_h(CH, 1.0), _h(CH, 1.5),
_h(S, 2.0), _h(CH, 2.0), _h(CH, 2.5),
_h(CH, 3.0), _h(CH, 3.5),
],
)
Pattern._PRESETS["double time"] = dict(
name="double time",
time_signature="4/4",
beats=4.0,
hits=[
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.25), _h(CH, 0.5), _h(CH, 0.75),
_h(S, 1.0), _h(CH, 1.0), _h(CH, 1.25), _h(CH, 1.5), _h(CH, 1.75),
_h(K, 2.0), _h(CH, 2.0), _h(CH, 2.25), _h(CH, 2.5), _h(CH, 2.75),
_h(S, 3.0), _h(CH, 3.0), _h(CH, 3.25), _h(CH, 3.5), _h(CH, 3.75),
],
)
Pattern._PRESETS["blast beat"] = dict(
name="blast beat",
time_signature="4/4",
beats=4.0,
hits=[
# Metal blast beat — everything on every 16th
*[_h(K, i * 0.25) for i in range(16)],
*[_h(S, i * 0.25) for i in range(16)],
*[_h(CH, i * 0.25) for i in range(16)],
],
)
Pattern._PRESETS["metal"] = dict(
name="metal",
time_signature="4/4",
beats=4.0,
hits=[
# Double kick metal pattern
_h(K, 0.0), _h(K, 0.25), _h(K, 0.5), _h(K, 0.75),
_h(S, 1.0), _h(K, 1.0), _h(K, 1.25), _h(K, 1.5), _h(K, 1.75),
_h(K, 2.0), _h(K, 2.25), _h(K, 2.5), _h(K, 2.75),
_h(S, 3.0), _h(K, 3.0), _h(K, 3.25), _h(K, 3.5), _h(K, 3.75),
_h(CH, 0.0), _h(CH, 0.5), _h(CH, 1.0), _h(CH, 1.5),
_h(CH, 2.0), _h(CH, 2.5), _h(CH, 3.0), _h(CH, 3.5),
],
)
Pattern._PRESETS["punk"] = dict(
name="punk",
time_signature="4/4",
beats=4.0,
hits=[
# Fast D-beat: ride on 8ths, kick+snare alternating
_h(K, 0.0), _h(S, 0.5), _h(K, 1.0), _h(S, 1.5),
_h(K, 2.0), _h(S, 2.5), _h(K, 3.0), _h(S, 3.5),
_h(RD, 0.0), _h(RD, 0.5), _h(RD, 1.0), _h(RD, 1.5),
_h(RD, 2.0), _h(RD, 2.5), _h(RD, 3.0), _h(RD, 3.5),
],
)
Pattern._PRESETS["disco"] = dict(
name="disco",
time_signature="4/4",
beats=4.0,
hits=[
# Four-on-the-floor kick, open hat on upbeats
_h(K, 0.0), _h(CH, 0.0), _h(OH, 0.5),
_h(K, 1.0), _h(S, 1.0), _h(CH, 1.0), _h(OH, 1.5),
_h(K, 2.0), _h(CH, 2.0), _h(OH, 2.5),
_h(K, 3.0), _h(S, 3.0), _h(CH, 3.0), _h(OH, 3.5),
],
)
Pattern._PRESETS["house"] = dict(
name="house",
time_signature="4/4",
beats=4.0,
hits=[
# Four-on-the-floor, offbeat hats, clap on 2 and 4
_h(K, 0.0), _h(OH, 0.5),
_h(K, 1.0), _h(DrumSound.CLAP, 1.0), _h(OH, 1.5),
_h(K, 2.0), _h(OH, 2.5),
_h(K, 3.0), _h(DrumSound.CLAP, 3.0), _h(OH, 3.5),
],
)
Pattern._PRESETS["hip hop"] = dict(
name="hip hop",
time_signature="4/4",
beats=4.0,
hits=[
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.5),
_h(S, 1.0), _h(CH, 1.0), _h(CH, 1.5),
_h(CH, 2.0), _h(K, 2.25), _h(CH, 2.5),
_h(S, 3.0), _h(CH, 3.0), _h(CH, 3.5),
],
)
Pattern._PRESETS["trap"] = dict(
name="trap",
time_signature="4/4",
beats=4.0,
hits=[
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.25), _h(CH, 0.5), _h(CH, 0.75),
_h(DrumSound.CLAP, 1.0), _h(CH, 1.0), _h(CH, 1.25), _h(CH, 1.5),
_h(CH, 1.75),
_h(CH, 2.0), _h(CH, 2.25), _h(K, 2.5), _h(CH, 2.5), _h(CH, 2.75),
_h(DrumSound.CLAP, 3.0), _h(CH, 3.0), _h(CH, 3.25), _h(CH, 3.5),
_h(OH, 3.75),
],
)
Pattern._PRESETS["breakbeat"] = dict(
name="breakbeat",
time_signature="4/4",
beats=4.0,
hits=[
# Amen break inspired
_h(K, 0.0), _h(RD, 0.0), _h(RD, 0.5),
_h(S, 1.0), _h(RD, 1.0), _h(K, 1.5), _h(RD, 1.5),
_h(K, 1.75), _h(RD, 2.0),
_h(S, 2.5), _h(RD, 2.5), _h(K, 2.75), _h(RD, 3.0),
_h(S, 3.25), _h(RD, 3.5), _h(S, 3.5),
],
)
Pattern._PRESETS["drum and bass"] = dict(
name="drum and bass",
time_signature="4/4",
beats=4.0,
hits=[
# Two-step DnB
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.25), _h(CH, 0.5), _h(CH, 0.75),
_h(S, 1.0), _h(CH, 1.0), _h(CH, 1.25), _h(CH, 1.5), _h(CH, 1.75),
_h(CH, 2.0), _h(K, 2.25), _h(CH, 2.25), _h(CH, 2.5), _h(CH, 2.75),
_h(S, 3.0), _h(CH, 3.0), _h(CH, 3.25), _h(K, 3.5), _h(CH, 3.5),
_h(CH, 3.75),
],
)
Pattern._PRESETS["shuffle"] = dict(
name="shuffle",
time_signature="4/4",
beats=4.0,
hits=[
# Triplet shuffle (Texas blues feel)
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.67),
_h(S, 1.0), _h(CH, 1.0), _h(CH, 1.67),
_h(K, 2.0), _h(CH, 2.0), _h(CH, 2.67),
_h(S, 3.0), _h(CH, 3.0), _h(CH, 3.67),
],
)
Pattern._PRESETS["motown"] = dict(
name="motown",
time_signature="4/4",
beats=4.0,
hits=[
# Snare on every beat (Motown signature)
_h(K, 0.0), _h(S, 0.0), _h(TB, 0.0), _h(TB, 0.5),
_h(S, 1.0), _h(TB, 1.0), _h(TB, 1.5),
_h(K, 2.0), _h(S, 2.0), _h(TB, 2.0), _h(TB, 2.5),
_h(S, 3.0), _h(TB, 3.0), _h(TB, 3.5),
],
)
Pattern._PRESETS["bo diddley"] = dict(
name="bo diddley",
time_signature="4/4",
beats=4.0,
hits=[
# 3+3+3+3+4 shave-and-a-haircut
_h(K, 0.0), _h(DrumSound.MARACAS, 0.0),
_h(K, 0.75), _h(DrumSound.MARACAS, 0.75),
_h(K, 1.5), _h(DrumSound.MARACAS, 1.5),
_h(K, 2.25), _h(DrumSound.MARACAS, 2.25),
_h(K, 3.0), _h(DrumSound.MARACAS, 3.0),
],
)
Pattern._PRESETS["afrobeat"] = dict(
name="afrobeat",
time_signature="4/4",
beats=4.0,
hits=[
# Tony Allen-style afrobeat
_h(K, 0.0), _h(OH, 0.0), _h(CH, 0.5),
_h(CH, 1.0), _h(S, 1.25, 70), _h(CH, 1.5),
_h(K, 2.0), _h(OH, 2.0), _h(CH, 2.5),
_h(S, 3.0), _h(CH, 3.0), _h(K, 3.5, 70), _h(CH, 3.5),
],
)
Pattern._PRESETS["highlife"] = dict(
name="highlife",
time_signature="12/8",
beats=6.0,
hits=[
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.5), _h(CH, 1.0),
_h(S, 1.5), _h(CH, 1.5), _h(CH, 2.0), _h(CH, 2.5),
_h(K, 3.0), _h(CH, 3.0), _h(CH, 3.5), _h(CH, 4.0),
_h(S, 4.5), _h(CH, 4.5), _h(CH, 5.0), _h(CH, 5.5),
],
)
Pattern._PRESETS["cumbia"] = dict(
name="cumbia",
time_signature="4/4",
beats=4.0,
hits=[
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.5),
_h(K, 1.0), _h(CH, 1.0), _h(S, 1.5),
_h(K, 2.0), _h(CH, 2.0), _h(CH, 2.5),
_h(K, 3.0), _h(CH, 3.0), _h(S, 3.5),
_h(DrumSound.GUIRO, 0.0), _h(DrumSound.GUIRO, 0.5),
_h(DrumSound.GUIRO, 1.0), _h(DrumSound.GUIRO, 1.5),
_h(DrumSound.GUIRO, 2.0), _h(DrumSound.GUIRO, 2.5),
_h(DrumSound.GUIRO, 3.0), _h(DrumSound.GUIRO, 3.5),
],
)
Pattern._PRESETS["merengue"] = dict(
name="merengue",
time_signature="4/4",
beats=4.0,
hits=[
_h(K, 0.0), _h(TB, 0.0), _h(TB, 0.25), _h(TB, 0.5), _h(TB, 0.75),
_h(S, 1.0), _h(TB, 1.0), _h(TB, 1.25), _h(TB, 1.5), _h(TB, 1.75),
_h(K, 2.0), _h(TB, 2.0), _h(TB, 2.25), _h(TB, 2.5), _h(TB, 2.75),
_h(S, 3.0), _h(TB, 3.0), _h(TB, 3.25), _h(TB, 3.5), _h(TB, 3.75),
],
)
Pattern._PRESETS["dancehall"] = dict(
name="dancehall",
time_signature="4/4",
beats=4.0,
hits=[
# Steppers riddim
_h(K, 0.0), _h(K, 1.0), _h(K, 2.0), _h(K, 3.0),
_h(S, 1.5), _h(S, 3.5),
_h(CH, 0.5), _h(CH, 1.5), _h(CH, 2.5), _h(CH, 3.5),
],
)
Pattern._PRESETS["new orleans"] = dict(
name="new orleans",
time_signature="4/4",
beats=4.0,
hits=[
# Swung with heavy syncopation
_h(K, 0.0), _h(S, 0.0, 80), _h(CH, 0.0),
_h(CH, 0.67, 70), _h(K, 1.0),
_h(S, 1.67, 60), _h(CH, 2.0),
_h(K, 2.67, 80), _h(S, 3.0), _h(CH, 3.0),
_h(CH, 3.67, 70),
],
)
Pattern._PRESETS["linear"] = dict(
name="linear",
time_signature="4/4",
beats=4.0,
hits=[
# No two limbs hit simultaneously — Gadd/Weckl style
_h(CH, 0.0), _h(K, 0.25), _h(CH, 0.5), _h(S, 0.75),
_h(CH, 1.0), _h(K, 1.25), _h(CH, 1.5), _h(K, 1.75),
_h(CH, 2.0), _h(K, 2.25), _h(CH, 2.5), _h(S, 2.75),
_h(CH, 3.0), _h(K, 3.25), _h(CH, 3.5), _h(K, 3.75),
],
)
Pattern._PRESETS["paradiddle"] = dict(
name="paradiddle",
time_signature="4/4",
beats=4.0,
hits=[
# RLRR LRLL as hi-hat/snare
_h(CH, 0.0), _h(S, 0.25), _h(CH, 0.5), _h(CH, 0.75),
_h(S, 1.0), _h(CH, 1.25), _h(S, 1.5), _h(S, 1.75),
_h(CH, 2.0), _h(S, 2.25), _h(CH, 2.5), _h(CH, 2.75),
_h(S, 3.0), _h(CH, 3.25), _h(S, 3.5), _h(S, 3.75),
_h(K, 0.0), _h(K, 1.0), _h(K, 2.0), _h(K, 3.0),
],
)
Pattern._PRESETS["6/8 afro-cuban"] = dict(
name="6/8 afro-cuban",
time_signature="6/8",
beats=3.0,
hits=[
_h(CB, 0.0), _h(CB, 0.5), _h(CB, 1.0), _h(CB, 1.5), _h(CB, 2.5),
_h(K, 0.0), _h(K, 2.0),
_h(CGA, 0.5, 70), _h(CGB, 1.0, 80),
_h(CGA, 2.0, 70), _h(CGB, 2.5, 80),
],
)
Pattern._PRESETS["bembe"] = dict(
name="bembe",
time_signature="6/8",
beats=3.0,
hits=[
# 6/8 bell pattern — foundation of Afro-Cuban 6/8 feel
_h(CB, 0.0), _h(CB, 0.33), _h(CB, 0.83),
_h(CB, 1.33), _h(CB, 1.67), _h(CB, 2.17), _h(CB, 2.67),
],
)
Pattern._PRESETS["baiao"] = dict(
name="baiao",
time_signature="4/4",
beats=4.0,
hits=[
# Brazilian baiao (Luiz Gonzaga)
_h(K, 0.0), _h(TB, 0.0), _h(TB, 0.5),
_h(TB, 1.0), _h(K, 1.5), _h(TB, 1.5),
_h(TB, 2.0), _h(TB, 2.5),
_h(TB, 3.0), _h(K, 3.0), _h(TB, 3.5),
_h(S, 1.0, 80), _h(S, 3.0, 80),
],
)
Pattern._PRESETS["maracatu"] = dict(
name="maracatu",
time_signature="4/4",
beats=4.0,
hits=[
# Brazilian maracatu (Recife)
_h(K, 0.0), _h(K, 0.5), _h(S, 1.0),
_h(K, 1.5), _h(K, 2.0), _h(S, 2.5),
_h(K, 3.0), _h(S, 3.5),
_h(DrumSound.AGOGO_HIGH, 0.0), _h(DrumSound.AGOGO_LOW, 0.5),
_h(DrumSound.AGOGO_HIGH, 1.0), _h(DrumSound.AGOGO_LOW, 1.5),
_h(DrumSound.AGOGO_HIGH, 2.0), _h(DrumSound.AGOGO_LOW, 2.5),
_h(DrumSound.AGOGO_HIGH, 3.0), _h(DrumSound.AGOGO_LOW, 3.5),
],
)
Pattern._PRESETS["country"] = dict(
name="country",
time_signature="4/4",
beats=4.0,
hits=[
# Train beat variant: kick on 1 and 3, rimshot on 2 and 4, hats on 8ths
_h(K, 0.0), _h(CH, 0.0), _h(CH, 0.5),
_h(RS, 1.0), _h(CH, 1.0), _h(CH, 1.5),
_h(K, 2.0), _h(CH, 2.0), _h(CH, 2.5),
_h(RS, 3.0), _h(CH, 3.0), _h(CH, 3.5),
# Ghost snare on the "and" of 4
_h(S, 3.5, 40),
],
)
Pattern._PRESETS["ska"] = dict(
name="ska",
time_signature="4/4",
beats=4.0,
hits=[
# Offbeat skank: kick on 1 and 3, snare on offbeats, hats on 8ths
_h(K, 0.0), _h(CH, 0.0), _h(S, 0.5), _h(CH, 0.5),
_h(CH, 1.0), _h(S, 1.5), _h(CH, 1.5),
_h(K, 2.0), _h(CH, 2.0), _h(S, 2.5), _h(CH, 2.5),
_h(CH, 3.0), _h(S, 3.5), _h(CH, 3.5),
],
)
Pattern._PRESETS["dub"] = dict(
name="dub",
time_signature="4/4",
beats=4.0,
hits=[
# Sparse and heavy
_h(K, 0.0),
_h(CH, 0.5), _h(CH, 1.5),
_h(S, 2.0, 110),
_h(OH, 2.5),
],
)
Pattern._PRESETS["jungle"] = dict(
name="jungle",
time_signature="4/4",
beats=4.0,
hits=[
# Chopped breakbeat at double-time feel
_h(K, 0.0), _h(K, 1.25), _h(K, 2.5),
_h(S, 1.0), _h(S, 2.25), _h(S, 3.0), _h(S, 3.5),
_h(RD, 0.0), _h(RD, 0.5), _h(RD, 1.0), _h(RD, 1.5),
_h(RD, 2.0), _h(RD, 2.5), _h(RD, 3.0), _h(RD, 3.5),
],
)
Pattern._PRESETS["techno"] = dict(
name="techno",
time_signature="4/4",
beats=4.0,
hits=[
# Minimal four-on-the-floor
_h(K, 0.0), _h(K, 1.0), _h(K, 2.0), _h(K, 3.0),
_h(CH, 0.0, 70), _h(CH, 0.5, 70), _h(CH, 1.0, 70), _h(CH, 1.5, 70),
_h(CH, 2.0, 70), _h(CH, 2.5, 70), _h(CH, 3.0, 70), _h(CH, 3.5, 70),
_h(OH, 0.5, 50), _h(OH, 1.5, 50), _h(OH, 2.5, 50), _h(OH, 3.5, 50),
_h(DrumSound.CLAP, 1.0), _h(DrumSound.CLAP, 3.0),
],
)
Pattern._PRESETS["gospel"] = dict(
name="gospel",
time_signature="4/4",
beats=4.0,
hits=[
# Shuffle feel with triplet hats
_h(K, 0.0), _h(K, 2.67),
_h(S, 1.0), _h(S, 3.0),
_h(CH, 0.0), _h(CH, 0.67), _h(CH, 1.0), _h(CH, 1.67),
_h(CH, 2.0), _h(CH, 2.67), _h(CH, 3.0), _h(CH, 3.67),
# Ghost snares
_h(S, 1.67, 35), _h(S, 3.67, 35),
],
)
Pattern._PRESETS["swing"] = dict(
name="swing",
time_signature="4/4",
beats=4.0,
hits=[
# Big band swing
_h(RD, 0.0), _h(RD, 0.67), _h(RD, 1.0), _h(RD, 1.67),
_h(RD, 2.0), _h(RD, 2.67), _h(RD, 3.0),
# Hi-hat foot on 2 and 4
_h(DrumSound.PEDAL_HAT, 1.0), _h(DrumSound.PEDAL_HAT, 3.0),
# Light kick on 1 and 3
_h(K, 0.0, 60), _h(K, 2.0, 60),
# Snare accent on 4
_h(S, 3.0, 80),
],
)
Pattern._PRESETS["bolero"] = dict(
name="bolero",
time_signature="4/4",
beats=4.0,
hits=[
# Slow romantic bolero
_h(K, 0.0),
_h(RS, 2.5), _h(RS, 3.5),
_h(S, 2.0),
_h(DrumSound.MARACAS, 0.0, 50), _h(DrumSound.MARACAS, 0.5, 50),
_h(DrumSound.MARACAS, 1.0, 50), _h(DrumSound.MARACAS, 1.5, 50),
_h(DrumSound.MARACAS, 2.0, 50), _h(DrumSound.MARACAS, 2.5, 50),
_h(DrumSound.MARACAS, 3.0, 50), _h(DrumSound.MARACAS, 3.5, 50),
],
)
Pattern._PRESETS["tango"] = dict(
name="tango",
time_signature="4/4",
beats=4.0,
hits=[
# Based on habanera rhythm
_h(K, 0.0), _h(K, 1.5), _h(K, 2.0), _h(K, 3.0),
_h(S, 1.0, 90), _h(S, 3.0, 90),
_h(CH, 0.0), _h(CH, 0.5), _h(CH, 1.0), _h(CH, 1.5),
_h(CH, 2.0), _h(CH, 2.5), _h(CH, 3.0), _h(CH, 3.5),
],
)
Pattern._PRESETS["flamenco"] = dict(
name="flamenco",
time_signature="12/8",
beats=3.0, # 6 eighth notes = 3 quarter beats
hits=[
# Palmas (clap) pattern
_h(DrumSound.CLAP, 0.0), _h(DrumSound.CLAP, 0.5),
_h(DrumSound.CLAP, 1.5), _h(DrumSound.CLAP, 2.0), _h(DrumSound.CLAP, 2.5),
# Cajon low (kick) on 0 and 1.5
_h(K, 0.0), _h(K, 1.5),
# Cajon slap (rimshot) on 1.0 and 2.0
_h(RS, 1.0), _h(RS, 2.0),
],
)
# ── Tabla patterns ────────────────────────────────────────────────────────
# Shortcuts for tabla sounds
TNA = DrumSound.TABLA_NA
TTI = DrumSound.TABLA_TIN
TGE = DrumSound.TABLA_GE
TDHA = DrumSound.TABLA_DHA
TTIT = DrumSound.TABLA_TIT
TKE = DrumSound.TABLA_KE
# Teental — the most common taal (16 beats / 4+4+4+4)
Pattern._PRESETS["teental"] = dict(
name="teental",
time_signature="4/4",
beats=16.0,
hits=[
# Vibhag 1: Dha Dhin Dhin Dha
_h(TDHA, 0.0), _h(TNA, 1.0), _h(TNA, 2.0), _h(TDHA, 3.0),
# Vibhag 2: Dha Dhin Dhin Dha
_h(TDHA, 4.0), _h(TNA, 5.0), _h(TNA, 6.0), _h(TDHA, 7.0),
# Vibhag 3 (khali): Dha Tin Tin Ta
_h(TDHA, 8.0), _h(TTI, 9.0), _h(TTI, 10.0), _h(TNA, 11.0),
# Vibhag 4: Dha Dhin Dhin Dha
_h(TDHA, 12.0), _h(TNA, 13.0), _h(TNA, 14.0), _h(TDHA, 15.0),
],
)
# Jhaptaal — 10 beats (2+3+2+3)
Pattern._PRESETS["jhaptaal"] = dict(
name="jhaptaal",
time_signature="4/4",
beats=10.0,
hits=[
# Dhi Na | Dhi Dhi Na | Ti Na | Dhi Dhi Na
_h(TDHA, 0.0), _h(TNA, 1.0),
_h(TDHA, 2.0), _h(TDHA, 3.0), _h(TNA, 4.0),
_h(TTI, 5.0), _h(TNA, 6.0),
_h(TDHA, 7.0), _h(TDHA, 8.0), _h(TNA, 9.0),
],
)
# Rupak taal — 7 beats (3+2+2), starts on khali (unusual)
Pattern._PRESETS["rupak"] = dict(
name="rupak",
time_signature="7/4",
beats=7.0,
hits=[
# Tin Tin Na | Dhi Na | Dhi Na
_h(TTI, 0.0), _h(TTI, 1.0), _h(TNA, 2.0),
_h(TDHA, 3.0), _h(TNA, 4.0),
_h(TDHA, 5.0), _h(TNA, 6.0),
],
)
# Dadra — 6 beats (3+3), light and folk
Pattern._PRESETS["dadra"] = dict(
name="dadra",
time_signature="6/4",
beats=6.0,
hits=[
# Dha Dhi Na | Dha Tin Na
_h(TDHA, 0.0), _h(TNA, 1.0), _h(TNA, 2.0),
_h(TDHA, 3.0), _h(TTI, 4.0), _h(TNA, 5.0),
],
)
# Keherwa — 8 beats (4+4), the most common light taal
Pattern._PRESETS["keherwa"] = dict(
name="keherwa",
time_signature="4/4",
beats=8.0,
hits=[
# Dha Ge Na Ti | Na Ke Dhi Na
_h(TDHA, 0.0), _h(TGE, 1.0), _h(TNA, 2.0), _h(TTIT, 3.0),
_h(TNA, 4.0), _h(TKE, 5.0), _h(TDHA, 6.0), _h(TNA, 7.0),
],
)
# Tabla solo theka — fast 16th note pattern for rhythmic display
Pattern._PRESETS["tabla solo"] = dict(
name="tabla solo",
time_signature="4/4",
beats=4.0,
hits=[
_h(TDHA, 0.0), _h(TTIT, 0.25), _h(TTIT, 0.5), _h(TKE, 0.75),
_h(TNA, 1.0), _h(TTIT, 1.25), _h(TGE, 1.5), _h(TNA, 1.75),
_h(TDHA, 2.0), _h(TNA, 2.25), _h(TTI, 2.5), _h(TNA, 2.75),
_h(TDHA, 3.0), _h(TTIT, 3.5), _h(TGE, 3.75),
],
)
# ── Marching snare patterns ───────────────────────────────────────────────
MS = DrumSound.MARCH_SNARE
MR = DrumSound.MARCH_RIMSHOT
MC = DrumSound.MARCH_CLICK
Q1 = DrumSound.QUAD_1
Q2 = DrumSound.QUAD_2
Q3 = DrumSound.QUAD_3
Q4 = DrumSound.QUAD_4
QS = DrumSound.QUAD_SPOCK
B1 = DrumSound.BASS_1
B2 = DrumSound.BASS_2
B3 = DrumSound.BASS_3
B4 = DrumSound.BASS_4
B5 = DrumSound.BASS_5
# Marching basic — standard 4/4 march with rimshot accents on 2 and 4
Pattern._PRESETS["march"] = dict(
name="march",
time_signature="4/4",
beats=4.0,
hits=[
_h(MS, 0.0, 80), _h(MS, 0.5, 55),
_h(MR, 1.0, 100), _h(MS, 1.5, 55),
_h(MS, 2.0, 80), _h(MS, 2.5, 55),
_h(MR, 3.0, 100), _h(MS, 3.5, 55),
],
)
# Cadence — 8-beat street beat pattern (the classic drumline cadence)
Pattern._PRESETS["cadence"] = dict(
name="cadence",
time_signature="4/4",
beats=8.0,
hits=[
# Bar 1: syncopated groove
_h(MR, 0.0, 105), _h(MS, 0.25, 60), _h(MS, 0.5, 65),
_h(MS, 0.75, 55), _h(MR, 1.0, 100),
_h(MS, 1.5, 60), _h(MS, 1.75, 58),
_h(MR, 2.0, 105), _h(MS, 2.5, 62),
_h(MS, 2.75, 55), _h(MR, 3.0, 100),
_h(MS, 3.25, 58), _h(MS, 3.5, 60), _h(MS, 3.75, 55),
# Bar 2: answer phrase with flams
_h(MR, 4.0, 110), _h(MS, 4.25, 62), _h(MS, 4.5, 65),
_h(MR, 5.0, 105), _h(MS, 5.25, 58),
_h(MS, 5.5, 60), _h(MS, 5.75, 55),
_h(MR, 6.0, 110), _h(MS, 6.25, 62),
_h(MS, 6.5, 65), _h(MS, 6.75, 62),
_h(MR, 7.0, 115), _h(MS, 7.25, 60),
_h(MR, 7.5, 110), _h(MR, 7.75, 115),
],
)
# Paradiddle — RLRR LRLL on marching snare
Pattern._PRESETS["march paradiddle"] = dict(
name="march paradiddle",
time_signature="4/4",
beats=4.0,
hits=[
# RLRR (R=rimshot accent, L=tap)
_h(MR, 0.0, 100), _h(MS, 0.25, 58), _h(MR, 0.5, 65), _h(MR, 0.75, 62),
# LRLL
_h(MS, 1.0, 58), _h(MR, 1.25, 100), _h(MS, 1.5, 58), _h(MS, 1.75, 55),
# RLRR
_h(MR, 2.0, 102), _h(MS, 2.25, 60), _h(MR, 2.5, 68), _h(MR, 2.75, 65),
# LRLL
_h(MS, 3.0, 60), _h(MR, 3.25, 102), _h(MS, 3.5, 60), _h(MS, 3.75, 58),
],
)
# March roll — buzz roll crescendo
Pattern._PRESETS["march roll"] = dict(
name="march roll",
time_signature="4/4",
beats=4.0,
hits=[
# Buzz roll as rapid 32nds, crescendo
*[_h(MS, i * 0.125, 40 + i * 3) for i in range(28)],
# Land on rimshot
_h(MR, 3.5, 115), _h(MR, 3.75, 120),
],
)
# Quad sweep — run across all 4 drums
Pattern._PRESETS["quad sweep"] = dict(
name="quad sweep",
time_signature="4/4",
beats=4.0,
hits=[
# Sweep down
_h(Q1, 0.0, 95), _h(Q2, 0.25, 90), _h(Q3, 0.5, 85), _h(Q4, 0.75, 80),
# Sweep up
_h(Q4, 1.0, 80), _h(Q3, 1.25, 85), _h(Q2, 1.5, 90), _h(Q1, 1.75, 95),
# Double sweep with spocks
_h(Q1, 2.0, 98), _h(Q2, 2.125, 92), _h(Q3, 2.25, 88), _h(Q4, 2.375, 82),
_h(Q4, 2.5, 82), _h(Q3, 2.625, 88), _h(Q2, 2.75, 92), _h(Q1, 2.875, 98),
# Spock accents
_h(QS, 3.0, 105), _h(Q1, 3.25, 90), _h(QS, 3.5, 105), _h(Q4, 3.75, 85),
],
)
# Quad groove — accented pattern with sweeps
Pattern._PRESETS["quad groove"] = dict(
name="quad groove",
time_signature="4/4",
beats=4.0,
hits=[
_h(Q1, 0.0, 100), _h(Q3, 0.25, 55), _h(Q1, 0.5, 60),
_h(Q2, 0.75, 55), _h(Q3, 1.0, 95), _h(Q1, 1.25, 55),
_h(Q4, 1.5, 58), _h(Q2, 1.75, 55),
_h(Q1, 2.0, 100), _h(Q2, 2.25, 55), _h(Q3, 2.5, 58),
_h(Q4, 2.75, 55), _h(QS, 3.0, 105), _h(Q3, 3.25, 55),
_h(Q2, 3.5, 58), _h(Q1, 3.75, 60),
],
)
# Bass split — classic bass drum splits across the line
Pattern._PRESETS["bass split"] = dict(
name="bass split",
time_signature="4/4",
beats=4.0,
hits=[
# Each bass drum takes a 16th, cascading down then up
_h(B1, 0.0, 95), _h(B2, 0.25, 90), _h(B3, 0.5, 85),
_h(B4, 0.75, 80), _h(B5, 1.0, 95),
_h(B5, 1.5, 90), _h(B4, 1.75, 85),
_h(B3, 2.0, 95), _h(B2, 2.25, 90), _h(B1, 2.5, 95),
_h(B1, 2.75, 85), _h(B3, 3.0, 100),
_h(B5, 3.25, 95), _h(B3, 3.5, 90), _h(B1, 3.75, 95),
],
)
# Bass unison — all bass drums hit together on accents
Pattern._PRESETS["bass unison"] = dict(
name="bass unison",
time_signature="4/4",
beats=4.0,
hits=[
# All 5 hit on beat 1
_h(B1, 0.0, 100), _h(B2, 0.0, 100), _h(B3, 0.0, 100),
_h(B4, 0.0, 100), _h(B5, 0.0, 100),
# Split on beat 2
_h(B1, 1.0, 90), _h(B3, 1.25, 85), _h(B5, 1.5, 90),
# All on beat 3
_h(B1, 2.0, 100), _h(B2, 2.0, 100), _h(B3, 2.0, 100),
_h(B4, 2.0, 100), _h(B5, 2.0, 100),
# Cascade into beat 4
_h(B5, 2.75, 80), _h(B4, 3.0, 85), _h(B3, 3.25, 90),
_h(B2, 3.5, 95), _h(B1, 3.75, 100),
],
)
# Full drumline — snare + quads + bass together
Pattern._PRESETS["drumline"] = dict(
name="drumline",
time_signature="4/4",
beats=4.0,
hits=[
# Snare backbone
_h(MR, 0.0, 115), _h(MS, 0.25, 35), _h(MS, 0.5, 38), _h(MS, 0.75, 32),
_h(MR, 1.0, 112), _h(MS, 1.25, 35), _h(MS, 1.5, 32), _h(MS, 1.75, 38),
_h(MR, 2.0, 115), _h(MS, 2.25, 38), _h(MS, 2.5, 32), _h(MS, 2.75, 35),
_h(MR, 3.0, 118), _h(MS, 3.25, 35), _h(MS, 3.5, 32), _h(MS, 3.75, 38),
# Quads on accents
_h(Q1, 0.0, 95), _h(Q3, 0.5, 55), _h(Q2, 1.0, 90),
_h(Q4, 1.5, 55), _h(Q1, 2.0, 95), _h(Q3, 2.5, 55),
_h(QS, 3.0, 100), _h(Q2, 3.5, 55),
# Bass on the big beats
_h(B3, 0.0, 100), _h(B5, 1.0, 95),
_h(B1, 2.0, 100), _h(B3, 3.0, 95),
],
)
# Chakradar — tihai of tihais (16 beats / 4 bars)
# A phrase (Dha Tit Tit Dha Ge Na) is played 3x with increasing intensity,
# and within each repetition the final 3 hits form a mini-tihai landing on sam.
_chakra_phrase_a = [
# Phrase 1 (4 beats): moderate
_h(TDHA, 0.0, 85), _h(TTIT, 0.25, 48), _h(TTIT, 0.5, 50),
_h(TDHA, 0.75, 80), _h(TGE, 1.0, 72),
_h(TNA, 1.5, 68), _h(TTIT, 1.75, 45),
# Mini-tihai: Na Dha, Na Dha, Na Dha
_h(TNA, 2.0, 72), _h(TDHA, 2.25, 82),
_h(TNA, 2.5, 75), _h(TDHA, 2.75, 85),
_h(TNA, 3.0, 78), _h(TDHA, 3.25, 90),
_h(TTIT, 3.5, 42), _h(TTIT, 3.75, 45),
]
_chakra_phrase_b = [
# Phrase 2 (4 beats): louder, busier
_h(TDHA, 4.0, 95), _h(TTIT, 4.125, 50), _h(TTIT, 4.25, 52),
_h(TKE, 4.375, 48), _h(TDHA, 4.5, 90), _h(TGE, 4.75, 78),
_h(TNA, 5.0, 75), _h(TTIT, 5.25, 48), _h(TTIT, 5.5, 50),
_h(TNA, 5.75, 72),
# Mini-tihai: Na Dha Ge, Na Dha Ge, Na Dha Ge
_h(TNA, 6.0, 80), _h(TDHA, 6.25, 92), _h(TGE, 6.5, 78),
_h(TNA, 6.75, 82), _h(TDHA, 7.0, 95), _h(TGE, 7.25, 82),
_h(TNA, 7.5, 85), _h(TDHA, 7.75, 100),
]
_chakra_phrase_c = [
# Phrase 3 (4 beats): peak intensity, fastest
_h(TDHA, 8.0, 105), _h(TTIT, 8.125, 55), _h(TTIT, 8.25, 58),
_h(TKE, 8.375, 52), _h(TNA, 8.5, 85), _h(TTIT, 8.625, 50),
_h(TDHA, 8.75, 100), _h(TGE, 9.0, 85),
_h(TNA, 9.25, 82), _h(TTIT, 9.5, 55), _h(TTIT, 9.625, 58),
_h(TKE, 9.75, 52), _h(TNA, 10.0, 88),
# Final tihai — 3x Dha Tit Na Dha landing on sam
_h(TDHA, 10.5, 105), _h(TTIT, 10.625, 58), _h(TNA, 10.75, 90),
_h(TDHA, 11.0, 108), _h(TTIT, 11.125, 60), _h(TNA, 11.25, 92),
_h(TDHA, 11.5, 112), _h(TTIT, 11.625, 62), _h(TNA, 11.75, 95),
]
_chakra_finale = [
# Bar 4: crescendo triplets into massive sam
*[_h(TTIT, 12.0 + i * (1/6), 50 + i * 5) for i in range(12)],
_h(TDHA, 14.0, 115), _h(TNA, 14.25, 85), _h(TDHA, 14.5, 118),
_h(TNA, 14.75, 88),
_h(TDHA, 15.0, 120), _h(TGE, 15.25, 95),
_h(TDHA, 15.5, 125), _h(DrumSound.TABLA_GE_BEND, 15.75, 110),
]
Pattern._PRESETS["chakradar"] = dict(
name="chakradar",
time_signature="4/4",
beats=16.0,
hits=_chakra_phrase_a + _chakra_phrase_b + _chakra_phrase_c + _chakra_finale,
)
# ── Doumbek patterns ──────────────────────────────────────────────────────
DKD = DrumSound.DOUMBEK_DUM
DKT = DrumSound.DOUMBEK_TEK
DKK = DrumSound.DOUMBEK_KA
# Maqsoum — the most common Arabic rhythm (4/4)
Pattern._PRESETS["maqsoum"] = dict(
name="maqsoum",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKD, 0.0, 85), _h(DKT, 0.5, 65),
_h(DKT, 1.0, 68), _h(DKD, 1.5, 80),
_h(DKT, 2.0, 65), _h(DKT, 2.5, 62),
_h(DKT, 3.0, 68), _h(DKT, 3.5, 62),
],
)
# Baladi — heavy, earthy, belly dance
Pattern._PRESETS["baladi"] = dict(
name="baladi",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKD, 0.0, 88), _h(DKD, 0.5, 78),
_h(DKT, 1.0, 70), _h(DKD, 1.5, 82),
_h(DKT, 2.0, 68), _h(DKT, 2.5, 62),
_h(DKT, 3.0, 68), _h(DKK, 3.25, 45), _h(DKT, 3.5, 65),
],
)
# Saidi — Upper Egyptian, strong and driving
Pattern._PRESETS["saidi"] = dict(
name="saidi",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKD, 0.0, 88), _h(DKT, 0.5, 65),
_h(DKD, 1.0, 82), _h(DKD, 1.5, 78),
_h(DKT, 2.0, 70), _h(DKT, 2.5, 62),
_h(DKT, 3.0, 68), _h(DKT, 3.5, 62),
],
)
# Ayoub — simple 2/4, trance-like repetition
Pattern._PRESETS["ayoub"] = dict(
name="ayoub",
time_signature="2/4",
beats=2.0,
hits=[
_h(DKD, 0.0, 85), _h(DKK, 0.5, 45),
_h(DKT, 1.0, 70), _h(DKT, 1.5, 62),
],
)
# ── Cajón patterns ────────────────────────────────────────────────────────
CB = DrumSound.CAJON_BASS
CSL = DrumSound.CAJON_SLAP
CT = DrumSound.CAJON_TAP
# Cajón flamenco — the classic acoustic percussion groove
Pattern._PRESETS["cajon"] = dict(
name="cajon",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 85), _h(CT, 0.5, 35), _h(CT, 0.75, 38),
_h(CSL, 1.0, 80), _h(CT, 1.5, 32),
_h(CB, 2.0, 82), _h(CT, 2.5, 35), _h(CT, 2.75, 40),
_h(CSL, 3.0, 82), _h(CT, 3.25, 30), _h(CT, 3.5, 35),
],
)
# Cajón rumba — Latin-flavored
Pattern._PRESETS["cajon rumba"] = dict(
name="cajon rumba",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 88), _h(CT, 0.5, 38),
_h(CSL, 1.0, 78), _h(CT, 1.25, 32), _h(CB, 1.5, 72),
_h(CSL, 2.0, 82), _h(CT, 2.5, 35),
_h(CB, 3.0, 75), _h(CSL, 3.5, 80), _h(CT, 3.75, 38),
],
)
# Cajón singer-songwriter — simple, supportive
Pattern._PRESETS["cajon folk"] = dict(
name="cajon folk",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 80),
_h(CSL, 1.0, 72), _h(CT, 1.5, 30),
_h(CB, 2.0, 78),
_h(CSL, 3.0, 75),
],
)
# ── Metal kit patterns ────────────────────────────────────────────────────
MK = DrumSound.METAL_KICK
MS = DrumSound.METAL_SNARE
MH = DrumSound.METAL_HAT
# Metal double kick — the classic thrash/death metal beat
Pattern._PRESETS["double kick"] = dict(
name="double kick",
time_signature="4/4",
beats=4.0,
hits=[
# Double kick 16ths, snare on 2 and 4, tight hats
*[_h(MK, i * 0.25) for i in range(16)],
_h(MS, 1.0), _h(MS, 3.0),
*[_h(MH, i * 0.5) for i in range(8)],
],
)
# Metal blast — blast beat with metal kit sounds
Pattern._PRESETS["metal blast"] = dict(
name="metal blast",
time_signature="4/4",
beats=4.0,
hits=[
*[_h(MK, i * 0.25) for i in range(16)],
*[_h(MS, i * 0.25) for i in range(16)],
*[_h(MH, i * 0.25) for i in range(16)],
],
)
# Metal groove — half time with double kick fills
Pattern._PRESETS["metal groove"] = dict(
name="metal groove",
time_signature="4/4",
beats=4.0,
hits=[
_h(MK, 0.0), _h(MH, 0.0),
_h(MH, 0.5),
_h(MS, 1.0), _h(MH, 1.0),
_h(MK, 1.5), _h(MH, 1.5),
_h(MK, 2.0), _h(MH, 2.0),
_h(MK, 2.25),
_h(MK, 2.5), _h(MH, 2.5),
_h(MK, 2.75),
_h(MS, 3.0), _h(MH, 3.0),
_h(MH, 3.5),
],
)
# Metal gallop — the classic Iron Maiden triplet feel
Pattern._PRESETS["metal gallop"] = dict(
name="metal gallop",
time_signature="4/4",
beats=4.0,
hits=[
_h(MK, 0.0), _h(MH, 0.0),
_h(MK, 0.33), _h(MK, 0.67),
_h(MS, 1.0), _h(MH, 1.0),
_h(MK, 1.33), _h(MK, 1.67),
_h(MK, 2.0), _h(MH, 2.0),
_h(MK, 2.33), _h(MK, 2.67),
_h(MS, 3.0), _h(MH, 3.0),
_h(MK, 3.33), _h(MK, 3.67),
],
)
# Tabla tiri-kita — rapid 16th-note dayan patter
Pattern._PRESETS["tiri kita"] = dict(
name="tiri kita",
time_signature="4/4",
beats=4.0,
hits=[
# Ti ri ki ta | dha ti ri ki | ta ka dhi na | dha — ti dha
_h(TTIT, 0.0), _h(TTIT, 0.25), _h(TKE, 0.5), _h(TNA, 0.75),
_h(TDHA, 1.0), _h(TTIT, 1.25), _h(TTIT, 1.5), _h(TKE, 1.75),
_h(TNA, 2.0), _h(TKE, 2.25), _h(TDHA, 2.5), _h(TNA, 2.75),
_h(TDHA, 3.0), _h(TTIT, 3.5), _h(TDHA, 3.75),
],
)
# ── Dhol patterns ────────────────────────────────────────────────────────
DD = DrumSound.DHOL_DAGGA
DT = DrumSound.DHOL_TILLI
DB = DrumSound.DHOL_BOTH
# Bhangra — the classic punjabi groove
Pattern._PRESETS["bhangra"] = dict(
name="bhangra",
time_signature="4/4",
beats=4.0,
hits=[
# Dagga on 1, tilli fills, both on 3
_h(DD, 0.0), _h(DT, 0.5), _h(DT, 0.75),
_h(DT, 1.0), _h(DT, 1.5),
_h(DB, 2.0), _h(DT, 2.5), _h(DT, 2.75),
_h(DD, 3.0), _h(DT, 3.25), _h(DT, 3.5), _h(DT, 3.75),
],
)
# Dhol chaal — driving folk pattern
Pattern._PRESETS["dhol chaal"] = dict(
name="dhol chaal",
time_signature="4/4",
beats=4.0,
hits=[
_h(DB, 0.0), _h(DT, 0.25), _h(DD, 0.5),
_h(DT, 1.0), _h(DT, 1.25), _h(DT, 1.5), _h(DD, 1.75),
_h(DB, 2.0), _h(DT, 2.25), _h(DD, 2.5),
_h(DT, 3.0), _h(DT, 3.25), _h(DT, 3.5), _h(DT, 3.75),
],
)
# ── Dholak patterns ─────────────────────────────────────────────────────
DKG = DrumSound.DHOLAK_GE
DKN = DrumSound.DHOLAK_NA
DKT = DrumSound.DHOLAK_TIT
# Qawwali — driving devotional pattern
Pattern._PRESETS["qawwali"] = dict(
name="qawwali",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKG, 0.0), _h(DKN, 0.5), _h(DKT, 0.75),
_h(DKN, 1.0), _h(DKG, 1.5),
_h(DKG, 2.0), _h(DKN, 2.5), _h(DKT, 2.75),
_h(DKN, 3.0), _h(DKT, 3.25), _h(DKN, 3.5), _h(DKG, 3.75),
],
)
# Dholak folk — light folk music pattern
Pattern._PRESETS["dholak folk"] = dict(
name="dholak folk",
time_signature="4/4",
beats=4.0,
hits=[
_h(DKG, 0.0), _h(DKN, 1.0), _h(DKT, 1.5),
_h(DKG, 2.0), _h(DKN, 3.0), _h(DKT, 3.5),
],
)
# ── Mridangam patterns ──────────────────────────────────────────────────
MTH = DrumSound.MRIDANGAM_THAM
MN = DrumSound.MRIDANGAM_NAM
MD = DrumSound.MRIDANGAM_DIN
MTA = DrumSound.MRIDANGAM_THA
# Adi talam — the fundamental Carnatic rhythm (8 beats: 4+2+2)
Pattern._PRESETS["adi talam"] = dict(
name="adi talam",
time_signature="4/4",
beats=8.0,
hits=[
# Tha Din | Tha ka | Dhi na | Tha ka
_h(MD, 0.0), _h(MN, 1.0),
_h(MTH, 2.0), _h(MTA, 3.0),
_h(MD, 4.0), _h(MN, 5.0),
_h(MTH, 6.0), _h(MTA, 7.0),
],
)
# Mridangam korvai — rhythmic cadence pattern
Pattern._PRESETS["mridangam korvai"] = dict(
name="mridangam korvai",
time_signature="4/4",
beats=4.0,
hits=[
_h(MD, 0.0), _h(MN, 0.25), _h(MTA, 0.5), _h(MN, 0.75),
_h(MTH, 1.0), _h(MN, 1.25), _h(MN, 1.5), _h(MTH, 1.75),
_h(MD, 2.0), _h(MTA, 2.25), _h(MN, 2.5), _h(MTA, 2.75),
_h(MD, 3.0), _h(MN, 3.5), _h(MD, 3.75),
],
)
# ── Djembe patterns ─────────────────────────────────────────────────────
JB = DrumSound.DJEMBE_BASS
JT = DrumSound.DJEMBE_TONE
JS = DrumSound.DJEMBE_SLAP
# Djembe — standard West African pattern
Pattern._PRESETS["djembe"] = dict(
name="djembe",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0), _h(JT, 0.5), _h(JT, 0.75),
_h(JS, 1.0), _h(JT, 1.5),
_h(JB, 2.0), _h(JT, 2.5), _h(JT, 2.75),
_h(JS, 3.0), _h(JT, 3.25), _h(JS, 3.5),
],
)
# Kuku — traditional Guinean harvest dance rhythm
Pattern._PRESETS["kuku"] = dict(
name="kuku",
time_signature="4/4",
beats=4.0,
hits=[
_h(JS, 0.0), _h(JS, 0.5),
_h(JT, 1.0), _h(JB, 1.5),
_h(JS, 2.0), _h(JS, 2.5),
_h(JT, 3.0), _h(JT, 3.25), _h(JB, 3.5),
],
)
# Soli — powerful Mandinka rhythm
Pattern._PRESETS["soli"] = dict(
name="soli",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0), _h(JT, 0.25), _h(JS, 0.5), _h(JT, 0.75),
_h(JB, 1.0), _h(JS, 1.5),
_h(JB, 2.0), _h(JT, 2.25), _h(JS, 2.5), _h(JT, 2.75),
_h(JB, 3.0), _h(JT, 3.5), _h(JS, 3.75),
],
)
# Dununba — heavy bass-driven rhythm (accompaniment djembe part)
Pattern._PRESETS["dununba"] = dict(
name="dununba",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0, 110), _h(JB, 0.5, 95),
_h(JT, 1.0, 75), _h(JB, 1.5, 100),
_h(JB, 2.0, 108), _h(JT, 2.5, 70),
_h(JB, 3.0, 105), _h(JB, 3.5, 90), _h(JT, 3.75, 65),
],
)
# Tiriba — joyful Susu rhythm from Guinea
Pattern._PRESETS["tiriba"] = dict(
name="tiriba",
time_signature="4/4",
beats=4.0,
hits=[
_h(JT, 0.0, 85), _h(JS, 0.25, 95), _h(JT, 0.5, 80),
_h(JB, 1.0, 100), _h(JT, 1.5, 75),
_h(JS, 2.0, 92), _h(JT, 2.25, 78), _h(JT, 2.5, 80),
_h(JB, 3.0, 105), _h(JS, 3.5, 88), _h(JT, 3.75, 72),
],
)
# Yankadi — gentle greeting/welcome rhythm from Guinea
Pattern._PRESETS["yankadi"] = dict(
name="yankadi",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0, 90), _h(JT, 0.5, 70),
_h(JT, 1.0, 72), _h(JS, 1.5, 85),
_h(JB, 2.0, 88), _h(JT, 2.5, 68),
_h(JS, 3.0, 82), _h(JT, 3.5, 65),
],
)
# Djansa — fast Malinke dance rhythm
Pattern._PRESETS["djansa"] = dict(
name="djansa",
time_signature="4/4",
beats=4.0,
hits=[
_h(JS, 0.0, 100), _h(JT, 0.25, 72), _h(JT, 0.5, 70),
_h(JB, 0.75, 95),
_h(JS, 1.0, 98), _h(JT, 1.25, 68), _h(JB, 1.5, 92),
_h(JS, 2.0, 102), _h(JT, 2.25, 75), _h(JT, 2.5, 72),
_h(JB, 2.75, 90),
_h(JS, 3.0, 105), _h(JT, 3.25, 70), _h(JB, 3.5, 95),
_h(JS, 3.75, 88),
],
)
# Mendiani — women's dance rhythm, celebratory
Pattern._PRESETS["mendiani"] = dict(
name="mendiani",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0, 100), _h(JT, 0.25, 65), _h(JS, 0.5, 90),
_h(JT, 1.0, 70), _h(JB, 1.5, 95), _h(JT, 1.75, 68),
_h(JS, 2.0, 92), _h(JT, 2.5, 72), _h(JS, 2.75, 85),
_h(JB, 3.0, 105), _h(JT, 3.25, 65), _h(JS, 3.5, 95),
],
)
# ── Fill presets ──────────────────────────────────────────────────────────
Pattern._FILLS["rock"] = dict(
name="rock fill",
time_signature="4/4",
beats=4.0,
hits=[
# Classic descending toms: high tom → mid tom → low tom → crash
_h(HT, 0.0), _h(HT, 0.5),
_h(MT, 1.0), _h(MT, 1.5),
_h(LT, 2.0), _h(LT, 2.5),
_h(CR, 3.0), _h(K, 3.0),
],
)
Pattern._FILLS["rock crash"] = dict(
name="rock crash fill",
time_signature="4/4",
beats=4.0,
hits=[
# Snare buildup into crash on beat 4
_h(S, 0.0), _h(S, 0.5),
_h(S, 1.0), _h(S, 1.25), _h(S, 1.5), _h(S, 1.75),
_h(S, 2.0), _h(S, 2.25), _h(S, 2.5), _h(S, 2.75),
_h(CR, 3.0), _h(K, 3.0),
],
)
Pattern._FILLS["jazz"] = dict(
name="jazz fill",
time_signature="4/4",
beats=4.0,
hits=[
# Snare press roll crescendo with ride accent
_h(S, 0.0, 40), _h(S, 0.25, 45), _h(S, 0.5, 50), _h(S, 0.75, 55),
_h(S, 1.0, 60), _h(S, 1.25, 65), _h(S, 1.5, 70), _h(S, 1.75, 75),
_h(S, 2.0, 80), _h(S, 2.25, 85), _h(S, 2.5, 90), _h(S, 2.75, 95),
_h(RD, 3.0, 110), _h(S, 3.0, 100),
],
)
Pattern._FILLS["jazz brush"] = dict(
name="jazz brush fill",
time_signature="4/4",
beats=4.0,
hits=[
# Subtle snare ghost notes leading to ride bell
_h(S, 0.0, 30), _h(S, 0.67, 35),
_h(S, 1.0, 40), _h(S, 1.67, 45),
_h(S, 2.0, 50), _h(S, 2.67, 60),
_h(RB, 3.0, 100), _h(S, 3.0, 70),
],
)
Pattern._FILLS["salsa"] = dict(
name="salsa fill",
time_signature="4/4",
beats=4.0,
hits=[
# Timbale cascade (high to low) with cowbell accent
_h(TBH, 0.0), _h(TBH, 0.25), _h(TBH, 0.5), _h(TBH, 0.75),
_h(TBH, 1.0), _h(TBL, 1.25), _h(TBL, 1.5), _h(TBL, 1.75),
_h(TBL, 2.0), _h(TBL, 2.25), _h(TBL, 2.5), _h(TBL, 2.75),
_h(CB, 3.0, 120), _h(CR, 3.5),
],
)
Pattern._FILLS["samba"] = dict(
name="samba fill",
time_signature="4/4",
beats=4.0,
hits=[
# Snare rolls with kick accents
_h(K, 0.0, 100), _h(S, 0.0, 80), _h(S, 0.25, 60), _h(S, 0.5, 70),
_h(K, 1.0, 90), _h(S, 1.0, 80), _h(S, 1.25, 60), _h(S, 1.5, 70), _h(S, 1.75, 80),
_h(K, 2.0, 100), _h(S, 2.0, 90), _h(S, 2.25, 70), _h(S, 2.5, 80), _h(S, 2.75, 90),
_h(CR, 3.0), _h(K, 3.0),
],
)
Pattern._FILLS["funk"] = dict(
name="funk fill",
time_signature="4/4",
beats=4.0,
hits=[
# Syncopated 16th-note snare/kick pattern ending on crash
_h(S, 0.0), _h(K, 0.25), _h(S, 0.5), _h(S, 0.75),
_h(K, 1.0), _h(S, 1.25), _h(K, 1.5), _h(S, 1.75),
_h(S, 2.0), _h(K, 2.25), _h(S, 2.5), _h(K, 2.75),
_h(S, 3.0), _h(S, 3.25), _h(K, 3.5), _h(CR, 3.75),
],
)
Pattern._FILLS["metal"] = dict(
name="metal fill",
time_signature="4/4",
beats=4.0,
hits=[
# Double kick 16ths with descending tom pattern
*[_h(K, i * 0.25) for i in range(16)],
_h(HT, 0.0), _h(HT, 0.5),
_h(MT, 1.0), _h(MT, 1.5),
_h(LT, 2.0), _h(LT, 2.5),
_h(CR, 3.0), _h(LT, 3.0), _h(LT, 3.5),
],
)
Pattern._FILLS["blast"] = dict(
name="blast fill",
time_signature="4/4",
beats=4.0,
hits=[
# All drums 16th notes building to crash
*[_h(K, i * 0.25, 80 + i) for i in range(14)],
*[_h(S, i * 0.25, 80 + i) for i in range(14)],
*[_h(CH, i * 0.25, 70 + i) for i in range(14)],
_h(CR, 3.5), _h(K, 3.5), _h(S, 3.5),
_h(CR, 3.75), _h(K, 3.75), _h(S, 3.75),
],
)
Pattern._FILLS["buildup"] = dict(
name="buildup fill",
time_signature="4/4",
beats=4.0,
hits=[
# Snare hits accelerating: quarter → eighth → 16th → crash
# Quarter notes (beat 0)
_h(S, 0.0),
# Eighth notes (beat 1)
_h(S, 1.0), _h(S, 1.5),
# 16th notes (beats 2-3)
_h(S, 2.0), _h(S, 2.25), _h(S, 2.5), _h(S, 2.75),
_h(S, 3.0), _h(S, 3.25), _h(S, 3.5),
_h(CR, 3.75), _h(K, 3.75),
],
)
Pattern._FILLS["breakdown"] = dict(
name="breakdown fill",
time_signature="4/4",
beats=4.0,
hits=[
# Sparse: just kick on 1, silence, crash on 4+
_h(K, 0.0, 110),
_h(CR, 3.5), _h(K, 3.5),
],
)
Pattern._FILLS["reggae"] = dict(
name="reggae fill",
time_signature="4/4",
beats=4.0,
hits=[
# Rimshot flams into kick+snare crash
_h(RS, 0.0, 70), _h(RS, 0.5, 70), _h(RS, 1.0, 70), _h(RS, 1.5, 70),
_h(K, 2.0), _h(S, 2.0),
_h(CR, 3.5),
],
)
Pattern._FILLS["afrobeat"] = dict(
name="afrobeat fill",
time_signature="4/4",
beats=4.0,
hits=[
# Tony Allen style: open hats, descending toms, kick, snare, crash
_h(OH, 0.0), _h(OH, 0.5),
_h(HT, 1.0), _h(MT, 1.5), _h(LT, 2.0),
_h(K, 2.5), _h(S, 3.0), _h(CR, 3.75),
],
)
Pattern._FILLS["bossa nova"] = dict(
name="bossa nova fill",
time_signature="4/4",
beats=4.0,
hits=[
# Subtle cross-stick fill
_h(RS, 0.5, 60), _h(RS, 1.5, 60), _h(RS, 2.5, 60),
_h(K, 3.0), _h(RB, 3.5),
],
)
Pattern._FILLS["house"] = dict(
name="house fill",
time_signature="4/4",
beats=4.0,
hits=[
# Snare roll into clap with ascending velocity
_h(S, 0.0, 40), _h(S, 0.25, 47), _h(S, 0.5, 54), _h(S, 0.75, 61),
_h(S, 1.0, 68), _h(S, 1.25, 75), _h(S, 1.5, 82), _h(S, 1.75, 90),
_h(DrumSound.CLAP, 2.0), _h(DrumSound.CLAP, 3.0),
_h(K, 2.5), _h(K, 3.5),
],
)
Pattern._FILLS["trap"] = dict(
name="trap fill",
time_signature="4/4",
beats=4.0,
hits=[
# Hi-hat roll accelerating then open hat, kick, clap
*[_h(CH, i * 0.25, 50 + i * 4) for i in range(8)],
_h(OH, 2.5), _h(K, 3.0), _h(DrumSound.CLAP, 3.5),
],
)
Pattern._FILLS["hip hop"] = dict(
name="hip hop fill",
time_signature="4/4",
beats=4.0,
hits=[
# Snare + open hat stutter
_h(S, 0.0, 80), _h(S, 0.5, 80),
_h(OH, 1.0), _h(OH, 1.5),
_h(K, 2.0), _h(S, 2.5),
_h(OH, 3.0), _h(CR, 3.75),
],
)
Pattern._FILLS["disco"] = dict(
name="disco fill",
time_signature="4/4",
beats=4.0,
hits=[
# Tom cascade with open hat
_h(OH, 0.0), _h(HT, 0.5), _h(MT, 1.0), _h(LT, 1.5),
_h(K, 2.0), _h(K, 2.5),
_h(OH, 3.0), _h(CR, 3.75),
],
)
Pattern._FILLS["cumbia"] = dict(
name="cumbia fill",
time_signature="4/4",
beats=4.0,
hits=[
# Guiro scrape accent
_h(DrumSound.GUIRO, 0.0, 70), _h(DrumSound.GUIRO, 0.5, 70),
_h(DrumSound.GUIRO, 1.0, 70), _h(DrumSound.GUIRO, 1.5, 70),
_h(K, 2.0), _h(K, 2.5),
_h(S, 3.0), _h(CR, 3.75),
],
)
Pattern._FILLS["highlife"] = dict(
name="highlife fill",
time_signature="4/4",
beats=4.0,
hits=[
# Bell pattern variation
_h(CB, 0.0), _h(CB, 0.5), _h(CB, 1.5), _h(CB, 2.0),
_h(RB, 2.5), _h(RB, 3.0), _h(CR, 3.75),
],
)
Pattern._FILLS["second line"] = dict(
name="second line fill",
time_signature="4/4",
beats=4.0,
hits=[
# Press roll buzz: snare at 16ths with ascending velocity
*[_h(S, i * 0.25, 30 + int(i * 60 / 10)) for i in range(11)],
_h(K, 3.0), _h(CR, 3.75),
],
)
# ── Doumbek fills ────────────────────────────────────────────────────────
_DKD = DrumSound.DOUMBEK_DUM
_DKT = DrumSound.DOUMBEK_TEK
_DKK = DrumSound.DOUMBEK_KA
# Doumbek roll — rapid teks building to dum
Pattern._FILLS["doumbek roll"] = dict(
name="doumbek roll fill",
time_signature="4/4",
beats=4.0,
hits=[
*[_h(_DKT, i * 0.125, 40 + i * 4) for i in range(16)],
_h(_DKD, 2.0, 100), _h(_DKT, 2.25, 65), _h(_DKT, 2.5, 68),
_h(_DKD, 3.0, 110), _h(_DKD, 3.25, 105),
_h(_DKD, 3.5, 115), _h(_DKT, 3.75, 80),
],
)
# Doumbek accent — syncopated dum-tek-ka pattern
Pattern._FILLS["doumbek accent"] = dict(
name="doumbek accent fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(_DKD, 0.0, 95), _h(_DKT, 0.25, 65), _h(_DKK, 0.5, 50),
_h(_DKT, 0.75, 68), _h(_DKD, 1.0, 90),
_h(_DKT, 1.5, 72), _h(_DKK, 1.75, 52), _h(_DKD, 2.0, 100),
_h(_DKT, 2.25, 68), _h(_DKT, 2.5, 70), _h(_DKT, 2.75, 72),
_h(_DKD, 3.0, 110), _h(_DKD, 3.5, 115),
],
)
# ── Tabla fills ──────────────────────────────────────────────────────────
_TNA = DrumSound.TABLA_NA
_TDH = DrumSound.TABLA_DHA
_TTT = DrumSound.TABLA_TIT
_TKE = DrumSound.TABLA_KE
_TGB = DrumSound.TABLA_GE_BEND
_TGE = DrumSound.TABLA_GE
_TTI = DrumSound.TABLA_TIN
_T3 = 1.0 / 12.0
# Tihai — the classic 3x pattern landing on sam
Pattern._FILLS["tihai"] = dict(
name="tihai fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(_TDH, 0.0, 105), _h(_TNA, 0.25, 72), _h(_TTT, 0.5, 48),
_h(_TKE, 0.75, 52), _h(_TDH, 1.0, 100),
_h(_TDH, 1.25, 110), _h(_TNA, 1.5, 78), _h(_TTT, 1.75, 52),
_h(_TKE, 2.0, 55), _h(_TDH, 2.25, 105),
_h(_TDH, 2.5, 118), _h(_TNA, 2.75, 82), _h(_TTT, 3.0, 58),
_h(_TKE, 3.25, 60), _h(_TDH, 3.5, 127),
],
)
# Chakkardar — 32nd triplet cascade into slam
Pattern._FILLS["chakkardar"] = dict(
name="chakkardar fill",
time_signature="4/4",
beats=4.0,
hits=[
*[_h(_TTT, i * _T3, 32 + i * 3) for i in range(12)],
_h(_TDH, 1.0, 115), _h(_TGB, 1.5, 108),
*[_h(_TTT, 2.0 + i * _T3, 35 + i * 3) for i in range(12)],
_h(_TDH, 3.0, 120), _h(_TDH, 3.25, 115),
_h(_TGB, 3.5, 120), _h(_TDH, 3.75, 127),
],
)
# Tiri kita fill — rapid 16th note dayan burst
Pattern._FILLS["tiri kita"] = dict(
name="tiri kita fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(_TTT, 0.0, 50), _h(_TTT, 0.125, 38), _h(_TKE, 0.25, 48),
_h(_TNA, 0.5, 72), _h(_TTT, 0.75, 42),
_h(_TDH, 1.0, 95), _h(_TTT, 1.25, 38), _h(_TTT, 1.5, 42),
_h(_TKE, 1.75, 48), _h(_TNA, 2.0, 75),
_h(_TTT, 2.25, 40), _h(_TTT, 2.5, 45), _h(_TKE, 2.75, 50),
_h(_TDH, 3.0, 100), _h(_TNA, 3.25, 70),
_h(_TDH, 3.5, 110), _h(_TGB, 3.75, 105),
],
)
# Bayan showcase — deep bass bends
Pattern._FILLS["bayan"] = dict(
name="bayan fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(_TGB, 0.0, 100), _h(_TNA, 0.5, 65),
_h(_TGE, 1.0, 85), _h(_TGB, 1.5, 105),
_h(_TNA, 2.0, 70), _h(_TKE, 2.25, 48),
_h(_TGB, 2.5, 110), _h(_TDH, 3.0, 115),
_h(_TGB, 3.5, 120),
],
)
# Call and response — dayan speaks, bayan answers
Pattern._FILLS["tabla call"] = dict(
name="tabla call fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(_TNA, 0.0, 105), _h(_TNA, 0.25, 55), _h(_TTT, 0.5, 38),
_h(_TNA, 0.75, 100),
_h(_TGE, 1.0, 95), _h(_TGE, 1.25, 48), _h(_TGB, 1.5, 90),
_h(_TNA, 2.0, 108), _h(_TTT, 2.125, 30), _h(_TTT, 2.25, 35),
_h(_TNA, 2.5, 100),
_h(_TGB, 3.0, 112), _h(_TKE, 3.25, 48),
_h(_TDH, 3.5, 120),
],
)
# ── Djembe fills ─────────────────────────────────────────────────────────
# Djembe call — bass-tone-slap conversation building to climax
Pattern._FILLS["djembe call"] = dict(
name="djembe call fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0, 100), _h(JT, 0.25, 70), _h(JT, 0.5, 72),
_h(JS, 0.75, 90),
_h(JB, 1.0, 95), _h(JT, 1.25, 68), _h(JS, 1.5, 88),
_h(JT, 1.75, 75),
_h(JS, 2.0, 100), _h(JS, 2.25, 95), _h(JT, 2.5, 78),
_h(JB, 2.75, 105),
_h(JS, 3.0, 110), _h(JT, 3.25, 80), _h(JS, 3.5, 112),
_h(JB, 3.75, 120),
],
)
# Djembe roll — rapid slaps accelerating into bass
Pattern._FILLS["djembe roll"] = dict(
name="djembe roll fill",
time_signature="4/4",
beats=4.0,
hits=[
# Accelerating slap roll
*[_h(JS, i * 0.125, 50 + i * 4) for i in range(16)],
# Bass accents punching through
_h(JB, 2.0, 105), _h(JB, 2.5, 108),
_h(JB, 3.0, 112), _h(JT, 3.25, 85),
_h(JB, 3.5, 115), _h(JS, 3.75, 100),
],
)
# Djembe break — syncopated West African-style break
Pattern._FILLS["djembe break"] = dict(
name="djembe break fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(JB, 0.0, 105), _h(JT, 0.25, 65), _h(JS, 0.5, 90),
_h(JT, 0.75, 70), _h(JB, 1.0, 100),
_h(JS, 1.25, 85), _h(JS, 1.5, 88),
_h(JB, 1.75, 95), _h(JT, 2.0, 72),
_h(JS, 2.25, 92), _h(JB, 2.5, 108),
_h(JT, 2.75, 68), _h(JS, 2.875, 55),
_h(JB, 3.0, 115), _h(JS, 3.25, 100),
_h(JB, 3.5, 118), _h(JB, 3.75, 120),
],
)
# ── Cajón fills ──────────────────────────────────────────────────────────
# Cajón flam run — slaps accelerating into bass hit
Pattern._FILLS["cajon flam"] = dict(
name="cajon flam fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(CSL, 0.0, 95), _h(CT, 0.125, 45), _h(CSL, 0.25, 90),
_h(CB, 0.5, 100), _h(CT, 0.75, 50),
_h(CSL, 1.0, 88), _h(CT, 1.125, 42), _h(CSL, 1.25, 92),
_h(CT, 1.5, 55), _h(CSL, 1.75, 85),
_h(CB, 2.0, 105), _h(CSL, 2.25, 75), _h(CT, 2.5, 48),
_h(CSL, 2.75, 80), _h(CT, 2.875, 40),
_h(CB, 3.0, 110), _h(CSL, 3.25, 90), _h(CSL, 3.5, 95),
_h(CB, 3.75, 120),
],
)
# Cajón rumble — fast taps building to slap accents
Pattern._FILLS["cajon rumble"] = dict(
name="cajon rumble fill",
time_signature="4/4",
beats=4.0,
hits=[
*[_h(CT, i * 0.125, 35 + i * 3) for i in range(16)],
_h(CSL, 2.0, 95), _h(CSL, 2.5, 100),
_h(CB, 3.0, 108), _h(CSL, 3.25, 88),
_h(CB, 3.5, 112), _h(CSL, 3.75, 95),
],
)
# Cajón breakdown — syncopated bass-slap groove
Pattern._FILLS["cajon breakdown"] = dict(
name="cajon breakdown fill",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 100), _h(CT, 0.25, 45), _h(CSL, 0.5, 85),
_h(CB, 1.0, 95), _h(CSL, 1.25, 78), _h(CT, 1.5, 50),
_h(CSL, 1.75, 82),
_h(CB, 2.0, 105), _h(CT, 2.125, 40), _h(CT, 2.25, 42),
_h(CSL, 2.5, 90), _h(CT, 2.75, 48),
_h(CB, 3.0, 115), _h(CSL, 3.25, 95),
_h(CB, 3.5, 110), _h(CSL, 3.75, 100),
],
)
# ── Metal fills (using metal kit) ────────────────────────────────────────
# Metal triplet — double kick triplets with snare accents
Pattern._FILLS["metal triplet"] = dict(
name="metal triplet fill",
time_signature="4/4",
beats=4.0,
hits=[
# Triplet kick pattern (12 kicks across 4 beats = triplet 8ths)
*[_h(MK, i * (1/3), 95 + (i % 3 == 0) * 15) for i in range(12)],
# Snare accents on downbeats
_h(MS, 0.0, 110), _h(MS, 1.0, 105),
_h(MS, 2.0, 110), _h(MS, 3.0, 115),
# Hat on upbeats
_h(MH, 0.5, 60), _h(MH, 1.5, 60),
_h(MH, 2.5, 65), _h(MH, 3.5, 70),
],
)
# Metal blastbeat variant — alternating snare/kick 32nds
Pattern._FILLS["metal blast"] = dict(
name="metal blast fill",
time_signature="4/4",
beats=4.0,
hits=[
# Alternating kick-snare at 32nd note speed for 2 beats
*[_h(MK if i % 2 == 0 else MS, i * 0.125, 100 + i) for i in range(16)],
# Then crash into half-time for 2 beats
_h(MK, 2.0, 120), _h(MS, 2.5, 115),
_h(MK, 3.0, 120), _h(MH, 3.25, 80),
_h(MS, 3.5, 120),
],
)
# Metal cascade — descending snare/kick rolls
Pattern._FILLS["metal cascade"] = dict(
name="metal cascade fill",
time_signature="4/4",
beats=4.0,
hits=[
# Fast snare roll beat 1
*[_h(MS, i * 0.125, 80 + i * 5) for i in range(8)],
# Double kick beat 2
*[_h(MK, 1.0 + i * 0.125, 90 + i * 3) for i in range(8)],
# Alternating beat 3
_h(MS, 2.0, 105), _h(MK, 2.125, 95),
_h(MS, 2.25, 108), _h(MK, 2.375, 98),
_h(MS, 2.5, 110), _h(MK, 2.625, 100),
_h(MS, 2.75, 112), _h(MK, 2.875, 102),
# Crash ending
_h(MK, 3.0, 120), _h(MS, 3.0, 120),
_h(MK, 3.5, 120), _h(MS, 3.5, 120),
],
)
class Part:
"""A named voice within a Score, with its own synth, envelope, and effects.
Parts allow layering multiple instruments — lead, bass, pads, etc. —
each with independent synth settings and effects, mixed together on playback.
Don't instantiate directly — use ``Score.part()`` instead.
Example::
score = Score("4/4", bpm=140)
lead = score.part("lead", synth="saw", envelope="pluck",
reverb=0.3, delay=0.25)
lead.add("E5", Duration.QUARTER).add("D5", Duration.EIGHTH)
bass = score.part("bass", synth="triangle", envelope="pluck")
bass.add("A2", Duration.HALF)
"""
def __init__(self, name: str, *, synth: str = "sine",
envelope: str = "piano", volume: float = 0.5,
reverb: float = 0.0, reverb_decay: float = 1.0,
reverb_type: str = "algorithmic",
delay: float = 0.0, delay_time: float = 0.375,
delay_feedback: float = 0.4,
highpass: float = 0.0, highpass_q: float = 0.707,
lowpass: float = 0.0, lowpass_q: float = 0.707,
distortion: float = 0.0, distortion_drive: float = 3.0,
legato: bool = False, glide: float = 0.0,
chorus: float = 0.0, chorus_rate: float = 1.5,
chorus_depth: float = 0.003,
swing: Optional[float] = None,
humanize: float = 0.0,
sidechain: float = 0.0,
sidechain_release: float = 0.1,
detune: float = 0.0,
pan: float = 0.0,
spread: float = 0.0,
# ── New synth engine params ──
sub_osc: float = 0.0,
noise_mix: float = 0.0,
filter_attack: float = 0.01,
filter_decay: float = 0.3,
filter_sustain: float = 0.0,
filter_amount: float = 0.0,
vel_to_filter: float = 0.0,
saturation: float = 0.0,
tremolo_depth: float = 0.0,
tremolo_rate: float = 5.0,
phaser: float = 0.0,
phaser_rate: float = 0.5,
cabinet: float = 0.0,
cabinet_brightness: float = 0.5,
analog: float = 0.0,
ensemble: int = 1,
fm_ratio: float = 2.0,
fm_index: float = 3.0,
synth_kw: dict = None):
self.name = name
self.synth = synth
self.envelope = envelope
self.volume = volume
self.swing = swing
self.humanize = humanize
self.sidechain = sidechain
self.sidechain_release = sidechain_release
self.reverb_mix = reverb
self.reverb_decay = reverb_decay
self.reverb_type = reverb_type
self.delay_mix = delay
self.delay_time = delay_time
self.delay_feedback = delay_feedback
self.highpass = highpass
self.highpass_q = highpass_q
self.lowpass = lowpass
self.lowpass_q = lowpass_q
self.distortion_mix = distortion
self.distortion_drive = distortion_drive
self.legato = legato
self.glide = glide
self.chorus_mix = chorus
self.chorus_rate = chorus_rate
self.chorus_depth = chorus_depth
self.detune = detune
self.pan = pan
self.spread = spread
# New synth engine params
self.sub_osc = sub_osc
self.noise_mix = noise_mix
self.filter_attack = filter_attack
self.filter_decay = filter_decay
self.filter_sustain = filter_sustain
self.filter_amount = filter_amount
self.vel_to_filter = vel_to_filter
self.saturation = saturation
self.tremolo_depth = tremolo_depth
self.tremolo_rate = tremolo_rate
self.phaser_mix = phaser
self.phaser_rate = phaser_rate
self.cabinet = cabinet
self.cabinet_brightness = cabinet_brightness
self.analog = analog
self.ensemble = ensemble
self.fm_ratio = fm_ratio
self.fm_index = fm_index
self.synth_kw = synth_kw or {}
self._system = "western" # default, overridden by Score.part()
self._fretboard = None # set by Score.part(fretboard=...)
self.notes: list[Note] = []
self._drum_hits: list[_Hit] = []
self._drum_pattern_beats: float = 0.0
self._automation: list[tuple[float, dict]] = [] # (beat, {param: value})
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "",
articulation: str = "") -> "Part":
"""Add a note. Accepts Tone/Chord objects or note strings like ``"E5"``.
Duration can be a ``Duration`` enum or a raw float (beats).
Velocity controls loudness (1-127, default 100).
Bend specifies a pitch bend in semitones over the note duration
(e.g. ``bend=2`` bends up a whole step, ``bend=-1`` bends down
a half step). Used for guitar bends, sitar meends, slides.
Articulation changes how the note is played: ``"staccato"`` (short,
~40% duration), ``"legato"`` (overlaps next note), ``"marcato"``
(heavy accent), ``"tenuto"`` (full duration, soft attack),
``"accent"`` (velocity bump), ``"fermata"`` (held ~50% longer).
Returns self for chaining.
"""
if isinstance(tone_or_string, str):
from .tones import Tone
tone_or_string = Tone.from_string(tone_or_string, system=self._system)
if isinstance(duration, (int, float)):
duration = _RawDuration(duration)
self.notes.append(Note(tone=tone_or_string, duration=duration,
velocity=velocity, bend=bend,
bend_type=bend_type, lyric=lyric,
articulation=articulation))
return self
def hold(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "",
articulation: str = "") -> "Part":
"""Add a note without advancing the beat position.
The note plays at the current position but the next note
starts at the *same* time — enabling polyphonic overlap
on a single part.
Use this for: piano sustain pedal (bass note rings while
melody plays above), guitar strumming with individual
string timing, held drone notes under a melody.
Example::
>>> piano = score.part("piano", instrument="piano")
>>> piano.hold("C3", Duration.WHOLE) # bass rings for 4 beats
>>> piano.add("E4", Duration.HALF) # starts at same time as C3
>>> piano.add("G4", Duration.HALF) # starts at beat 2
"""
if isinstance(tone_or_string, str):
from .tones import Tone
tone_or_string = Tone.from_string(tone_or_string, system=self._system)
if isinstance(duration, (int, float)):
duration = _RawDuration(duration)
self.notes.append(Note(tone=tone_or_string, duration=duration,
velocity=velocity, bend=bend,
bend_type=bend_type, lyric=lyric,
articulation=articulation, _hold=True))
return self
def hit(self, sound, duration=Duration.EIGHTH, *, velocity: int = 100,
articulation: str = "") -> "Part":
"""Add a drum hit to this part.
Places a drum sound into the note stream so it goes through the
normal renderer — meaning articulations, humanize, and effects
all work on individual hits.
Args:
sound: A :class:`DrumSound` enum member (e.g. ``DrumSound.KICK``).
duration: How long the hit occupies in the timeline (default 8th note).
velocity: Hit loudness 1-127.
articulation: ``"accent"``, ``"staccato"``, ``"marcato"``, etc.
Example::
>>> drums = score.part("kit", synth="sine")
>>> drums.hit(DrumSound.KICK, Duration.QUARTER, articulation="accent")
>>> drums.hit(DrumSound.CLOSED_HAT, Duration.EIGHTH)
"""
if isinstance(duration, (int, float)):
duration = _RawDuration(duration)
self.notes.append(Note(tone=_DrumTone(sound), duration=duration,
velocity=velocity, articulation=articulation))
return self
def flam(self, sound, duration=Duration.QUARTER, *, velocity: int = 110,
gap: float = 0.015, grace_vel: float = 0.3,
articulation: str = "") -> "Part":
"""Add a flam — a grace note immediately before the main hit.
The grace note is nearly simultaneous with the main hit,
thickening the attack. Tighter gap = more like one fat hit,
wider gap = audible double.
Args:
sound: A :class:`DrumSound` enum member.
duration: Total duration the flam occupies.
velocity: Main hit velocity.
gap: Beats between grace and main (default 0.008 ≈ 4ms at 120).
grace_vel: Grace note velocity as fraction of main (default 0.3).
articulation: Optional articulation for the main hit.
Example::
>>> p.flam(DrumSound.MARCH_SNARE, Duration.QUARTER, velocity=120)
"""
if isinstance(duration, (int, float)):
dur_val = duration
else:
dur_val = duration.value if hasattr(duration, 'value') else float(duration)
self.hit(sound, gap, velocity=int(velocity * grace_vel))
self.hit(sound, dur_val - gap, velocity=velocity, articulation=articulation)
return self
def diddle(self, sound, duration=Duration.EIGHTH, *,
velocity: int = 70) -> "Part":
"""Add a diddle — two equal strokes in the space of one note.
A double-stroke roll building block. Two hits split evenly
across the duration.
Args:
sound: A :class:`DrumSound` enum member.
duration: Total duration (default 8th note). Each stroke
gets half.
velocity: Velocity for both strokes.
Example::
>>> p.diddle(DrumSound.MARCH_SNARE, Duration.EIGHTH, velocity=60)
"""
if isinstance(duration, (int, float)):
dur_val = duration
else:
dur_val = duration.value if hasattr(duration, 'value') else float(duration)
half = dur_val / 2
self.hit(sound, half, velocity=velocity)
self.hit(sound, half, velocity=int(velocity * 0.9))
return self
def cheese(self, sound, duration=Duration.QUARTER, *, velocity: int = 110,
gap: float = 0.008, grace_vel: float = 0.3) -> "Part":
"""Add a cheese — a flam followed by a diddle.
Common marching rudiment: grace-MAIN-tap-tap.
Args:
sound: A :class:`DrumSound` enum member.
duration: Total duration.
velocity: Main hit velocity.
"""
if isinstance(duration, (int, float)):
dur_val = duration
else:
dur_val = duration.value if hasattr(duration, 'value') else float(duration)
# Flam takes first half, diddle takes second half
flam_dur = dur_val * 0.5
diddle_dur = dur_val * 0.5
self.hit(sound, gap, velocity=int(velocity * grace_vel))
self.hit(sound, flam_dur - gap, velocity=velocity)
self.hit(sound, diddle_dur / 2, velocity=int(velocity * 0.5))
self.hit(sound, diddle_dur / 2, velocity=int(velocity * 0.45))
return self
def crescendo(self, notes, duration=Duration.QUARTER, *,
start_vel: int = 40, end_vel: int = 110,
articulation: str = "") -> "Part":
"""Add notes with velocity ramping up (getting louder).
Args:
notes: List of note strings (e.g. ``["C4", "D4", "E4"]``).
duration: Duration for each note.
start_vel: Starting velocity (quiet).
end_vel: Ending velocity (loud).
articulation: Optional articulation for all notes.
Example::
>>> piano.crescendo(["C4","D4","E4","F4","G4"], Duration.QUARTER,
... start_vel=40, end_vel=110)
"""
return self.dynamics(notes, duration, velocities=(start_vel, end_vel),
articulation=articulation)
def decrescendo(self, notes, duration=Duration.QUARTER, *,
start_vel: int = 110, end_vel: int = 40,
articulation: str = "") -> "Part":
"""Add notes with velocity ramping down (getting quieter).
Args:
notes: List of note strings.
duration: Duration for each note.
start_vel: Starting velocity (loud).
end_vel: Ending velocity (quiet).
articulation: Optional articulation for all notes.
Example::
>>> piano.decrescendo(["G4","F4","E4","D4","C4"], Duration.QUARTER,
... start_vel=110, end_vel=40)
"""
return self.dynamics(notes, duration, velocities=(start_vel, end_vel),
articulation=articulation)
def dynamics(self, notes, duration=Duration.QUARTER, *,
velocities=None, articulation: str = "") -> "Part":
"""Add notes with a velocity curve.
Args:
notes: List of note strings or Tone/Chord objects.
duration: Duration for each note (or list of durations).
velocities: Velocity curve — either a ``(start, end)`` tuple
for a linear ramp, or a list of ints (one per note).
articulation: Optional articulation for all notes (or list).
Example::
>>> # Linear ramp
>>> piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
... velocities=(50, 120))
>>> # Custom curve (swell and fade)
>>> piano.dynamics(["C4","D4","E4","F4","G4","F4","E4","D4"],
... Duration.EIGHTH,
... velocities=[50, 70, 90, 110, 110, 90, 70, 50])
"""
n = len(notes)
if n == 0:
return self
# Resolve velocities
if velocities is None:
vels = [100] * n
elif isinstance(velocities, (tuple, list)) and len(velocities) == 2 and isinstance(velocities[0], (int, float)):
# (start, end) tuple — linear ramp
start_v, end_v = velocities
if n == 1:
vels = [int(start_v)]
else:
vels = [int(start_v + (end_v - start_v) * i / (n - 1))
for i in range(n)]
else:
vels = list(velocities)
# Resolve durations
if isinstance(duration, (list, tuple)):
durs = list(duration)
else:
durs = [duration] * n
# Resolve articulations
if isinstance(articulation, (list, tuple)):
arts = list(articulation)
else:
arts = [articulation] * n
for note, vel, dur, art in zip(notes, vels, durs, arts):
vel = max(1, min(127, vel))
self.add(note, dur, velocity=vel, articulation=art)
return self
def swell(self, notes, duration=Duration.QUARTER, *,
low_vel: int = 40, peak_vel: int = 110,
articulation: str = "") -> "Part":
"""Add notes that swell up then fade back down (< > shape).
The velocity ramps up to the midpoint then back down,
creating the classic orchestral swell.
Args:
notes: List of note strings.
duration: Duration for each note.
low_vel: Velocity at start and end.
peak_vel: Velocity at the peak (midpoint).
articulation: Optional articulation.
Example::
>>> strings.swell(["C4","D4","E4","F4","G4","F4","E4","D4"],
... Duration.QUARTER, low_vel=40, peak_vel=110)
"""
n = len(notes)
if n <= 2:
return self.dynamics(notes, duration, velocities=[peak_vel] * n,
articulation=articulation)
mid = n // 2
vels = []
for i in range(n):
if i <= mid:
v = low_vel + (peak_vel - low_vel) * i / mid
else:
v = peak_vel - (peak_vel - low_vel) * (i - mid) / (n - 1 - mid)
vels.append(int(v))
return self.dynamics(notes, duration, velocities=vels,
articulation=articulation)
def set(self, **params) -> "Part":
"""Change effect parameters at the current beat position.
Inserts an automation marker — from this point forward, the
specified parameters take new values. Use this to open filters,
add reverb, kick in distortion, or change volume mid-song.
Args:
**params: Any Part parameter — ``lowpass``, ``lowpass_q``,
``reverb``, ``reverb_decay``, ``delay``, ``delay_time``,
``delay_feedback``, ``distortion``, ``distortion_drive``,
``volume``, ``chorus``, ``chorus_rate``, ``chorus_depth``.
Returns:
Self for chaining.
Example::
>>> lead = score.part("lead", synth="saw", lowpass=800)
>>> lead.add("C5", Duration.WHOLE) # filtered
>>> lead.set(lowpass=3000, reverb=0.4) # filter opens
>>> lead.add("E5", Duration.WHOLE) # bright + reverb
>>> lead.set(distortion=0.6, lowpass=1500) # grit
>>> lead.add("G5", Duration.WHOLE)
"""
beat_pos = sum(n.beats for n in self.notes)
# Map shorthand param names to internal attribute names
param_map = {
"reverb": "reverb_mix", "delay": "delay",
"distortion": "distortion", "chorus": "chorus_mix",
}
mapped = {}
for k, v in params.items():
attr = param_map.get(k, k)
# Handle the special naming conventions
if k == "reverb":
mapped["reverb_mix"] = v
elif k == "delay":
mapped["delay_mix"] = v
elif k == "distortion":
mapped["distortion_mix"] = v
elif k == "chorus":
mapped["chorus_mix"] = v
elif k == "phaser":
mapped["phaser_mix"] = v
else:
mapped[k] = v
self._automation.append((beat_pos, mapped))
return self
def _get_params_at(self, beat: float) -> dict:
"""Get the effective parameters at a given beat position."""
# Start with initial values
params = {
"volume": self.volume,
"saturation": self.saturation,
"tremolo_depth": self.tremolo_depth, "tremolo_rate": self.tremolo_rate,
"reverb_mix": self.reverb_mix, "reverb_decay": self.reverb_decay,
"reverb_type": self.reverb_type,
"delay_mix": self.delay_mix, "delay_time": self.delay_time,
"delay_feedback": self.delay_feedback,
"phaser_mix": self.phaser_mix, "phaser_rate": self.phaser_rate,
"cabinet": self.cabinet, "cabinet_brightness": self.cabinet_brightness,
"highpass": self.highpass, "highpass_q": self.highpass_q,
"lowpass": self.lowpass, "lowpass_q": self.lowpass_q,
"distortion_mix": self.distortion_mix,
"distortion_drive": self.distortion_drive,
"chorus_mix": self.chorus_mix, "chorus_rate": self.chorus_rate,
"chorus_depth": self.chorus_depth,
}
# Apply automation up to the given beat
for auto_beat, changes in sorted(self._automation, key=lambda a: a[0]):
if auto_beat <= beat:
params.update(changes)
else:
break
return params
def _get_automation_points(self) -> list[float]:
"""Return sorted list of beat positions where parameters change."""
points = sorted(set(beat for beat, _ in self._automation))
return points
def ramp(self, over: float = 4.0, resolution: float = 0.25,
curve: str = "linear", **params) -> "Part":
"""Smoothly ramp parameters from their current values to new targets.
Generates interpolated automation points — like turning a knob
gradually instead of jumping to a new position. Works for any
parameter that ``.set()`` accepts.
Args:
over: Duration of the ramp in beats (default 4.0 = 1 bar).
Use ``Duration.WHOLE * 4`` for a 4-bar ramp, etc.
resolution: How often to insert points, in beats (default 0.25).
Lower = smoother but more points.
curve: Interpolation shape — ``"linear"`` (default),
``"ease_in"`` (slow start, fast end),
``"ease_out"`` (fast start, slow end),
``"ease_in_out"`` (slow start and end).
**params: Target values for any parameter. The ramp starts
from the parameter's current value at this beat position.
Returns:
Self for chaining.
Example::
>>> lead = score.part("lead", synth="saw", lowpass=200)
>>> # Open the filter over 4 bars
>>> lead.ramp(over=Duration.WHOLE * 4, lowpass=8000)
>>> # Fade reverb in over 2 bars
>>> pad.ramp(over=Duration.WHOLE * 2, reverb=0.5)
>>> # Multiple params at once with easing
>>> lead.ramp(over=8.0, curve="ease_in", lowpass=6000, distortion=0.4)
"""
current_beat = sum(n.beats for n in self.notes)
# Map param names to internal names
param_map = {
"reverb": "reverb_mix", "delay": "delay_mix",
"distortion": "distortion_mix", "chorus": "chorus_mix",
"phaser": "phaser_mix",
}
# Get current values for each param
current_params = self._get_params_at(current_beat)
ramps = {}
for param, target in params.items():
internal = param_map.get(param, param)
start = current_params.get(internal, getattr(self, internal, 0.0))
ramps[internal] = (float(start), float(target))
# Generate interpolated points
beat = 0.0
while beat <= over:
t = beat / over if over > 0 else 1.0
t = max(0.0, min(1.0, t))
# Apply curve
if curve == "ease_in":
t = t * t
elif curve == "ease_out":
t = 1.0 - (1.0 - t) ** 2
elif curve == "ease_in_out":
t = 3 * t * t - 2 * t * t * t
point = {}
for internal, (start, end) in ramps.items():
point[internal] = start + (end - start) * t
self._automation.append((current_beat + beat, point))
beat += resolution
return self
def lfo(self, param: str, *, rate: float = 0.5, min: float = 0.0,
max: float = 1.0, bars: float = 4, shape: str = "sine",
resolution: float = 0.25) -> "Part":
"""Automate a parameter with an LFO (low-frequency oscillator).
Generates automation points at regular intervals, sweeping a
parameter smoothly between min and max values. This is how
filter sweeps, tremolo, and auto-wah effects work.
Args:
param: Parameter name to modulate (e.g. ``"lowpass"``,
``"reverb"``, ``"distortion"``, ``"volume"``,
``"chorus"``, ``"delay"``).
rate: LFO speed in cycles per bar (default 0.5 = one sweep
every 2 bars). 0.25 = very slow, 1 = once per bar,
4 = four times per bar.
min: Minimum parameter value.
max: Maximum parameter value.
bars: Number of bars to run the LFO over (default 4).
shape: Waveform shape — ``"sine"`` (smooth), ``"triangle"``
(linear), ``"saw"`` (ramp up), ``"square"`` (on/off).
resolution: How often to insert automation points, in beats
(default 0.25 = every 16th note). Lower = smoother.
Returns:
Self for chaining.
Example::
>>> lead = score.part("lead", synth="saw", lowpass=400)
>>> # Slow filter sweep: 400→3000 Hz over 8 bars
>>> lead.lfo("lowpass", rate=0.125, min=400, max=3000, bars=8)
>>> lead.arpeggio("Cm", bars=8, pattern="up", octaves=2)
"""
import math
current_beat = sum(n.beats for n in self.notes)
beats_per_bar = 4.0 # assume 4/4
total_beats = bars * beats_per_bar
cycles_per_beat = rate / beats_per_bar
beat = 0.0
while beat < total_beats:
# Normalized position in the LFO cycle (0-1)
phase = (beat * cycles_per_beat) % 1.0
# Shape the LFO
if shape == "sine":
# Sine: 0→1→0→-1→0 mapped to min→max→min
value = 0.5 + 0.5 * math.sin(2 * math.pi * phase)
elif shape == "triangle":
# Triangle: linear up then down
value = 2 * phase if phase < 0.5 else 2 * (1 - phase)
elif shape == "saw":
# Sawtooth: ramp up
value = phase
elif shape == "square":
# Square: on/off
value = 1.0 if phase < 0.5 else 0.0
else:
value = 0.5 + 0.5 * math.sin(2 * math.pi * phase)
# Map 0-1 to min-max
param_value = min + value * (max - min)
# Insert automation point at the absolute beat position
abs_beat = current_beat + beat
# Map param name to internal name
param_map = {
"reverb": "reverb_mix", "delay": "delay_mix",
"distortion": "distortion_mix", "chorus": "chorus_mix",
}
internal_name = param_map.get(param, param)
self._automation.append((abs_beat, {internal_name: param_value}))
beat += resolution
return self
def rest(self, duration=Duration.QUARTER) -> "Part":
"""Add a rest. Returns self for chaining."""
if isinstance(duration, (int, float)):
duration = _RawDuration(duration)
self.notes.append(Note(tone=None, duration=duration, velocity=0))
return self
def fade_in(self, bars: float = 4) -> "Part":
"""Fade volume from 0 to current level over N bars."""
beats = bars * 4.0 # assume 4/4
current_beat = sum(n.beats for n in self.notes)
steps = int(beats / 0.5) # automate every half beat
for i in range(steps + 1):
frac = i / steps
beat = current_beat + i * 0.5
vol = self.volume * frac
self._automation.append((beat, {"volume": vol}))
return self
def fade_out(self, bars: float = 4) -> "Part":
"""Fade volume from current level to 0 over N bars."""
beats = bars * 4.0
current_beat = sum(n.beats for n in self.notes)
steps = int(beats / 0.5)
for i in range(steps + 1):
frac = 1.0 - (i / steps)
beat = current_beat + i * 0.5
vol = self.volume * frac
self._automation.append((beat, {"volume": vol}))
return self
def arpeggio(self, chord, *, bars: float = 1, pattern: str = "up",
division=Duration.SIXTEENTH, octaves: int = 1,
velocity: int = 100) -> "Part":
"""Arpeggiate a chord into a rhythmic pattern.
Takes a chord and sequences through its notes automatically,
like a hardware arpeggiator on a synth. Combined with
``legato=True`` and ``glide``, this produces classic acid
and trance arpeggiated lines.
Args:
chord: A Chord object (or string like ``"Am"``).
bars: Number of bars to fill (default 1).
pattern: Arpeggio pattern:
- ``"up"`` — low to high, repeat
- ``"down"`` — high to low, repeat
- ``"updown"`` — up then down (bounce)
- ``"downup"`` — down then up
- ``"random"`` — random note order
division: Note length for each step (default ``Duration.SIXTEENTH``).
octaves: Number of octaves to span (default 1). With 2,
the pattern repeats one octave higher before cycling.
Returns:
Self for chaining.
Example::
>>> lead = score.part("lead", synth="saw", legato=True, glide=0.03)
>>> lead.arpeggio(Chord.from_symbol("Am"), bars=2, pattern="updown")
"""
from .tones import Tone
# Parse chord if string
if isinstance(chord, str):
from .chords import Chord as ChordClass
chord = ChordClass.from_symbol(chord)
# Get the pitches from the chord, sorted low to high
tones = sorted(chord.tones, key=lambda t: t.pitch())
# Expand across octaves
all_tones = []
for oct in range(octaves):
for t in tones:
if oct == 0:
all_tones.append(t)
else:
all_tones.append(t.add(12 * oct))
# Build the sequence based on pattern
if pattern == "up":
seq = list(all_tones)
elif pattern == "down":
seq = list(reversed(all_tones))
elif pattern == "updown":
seq = list(all_tones) + list(reversed(all_tones[1:-1]))
elif pattern == "downup":
seq = list(reversed(all_tones)) + list(all_tones[1:-1])
elif pattern == "random":
import random
seq = list(all_tones)
random.shuffle(seq)
else:
seq = list(all_tones)
if not seq:
return self
# Calculate how many steps fit in the given bars
if hasattr(division, 'value'):
step_beats = division.value
else:
step_beats = float(division)
# Get beats per bar from score's time signature if available
total_beats = bars * 4.0 # default 4/4
total_steps = int(total_beats / step_beats)
# Fill the bars by cycling through the sequence
for i in range(total_steps):
tone = seq[i % len(seq)]
self.add(tone, step_beats, velocity=velocity)
return self
def strum(self, chord_name: str, duration=Duration.QUARTER, *,
direction: str = "down", velocity: int = 100,
strum_time: float = 0.05) -> "Part":
"""Strum a chord using the part's fretboard fingering.
Looks up the chord on the fretboard, gets the fingering, and
adds each string as a rapid sequence with tiny time offsets —
like a real guitar strum. Muted strings are skipped.
Args:
chord_name: Chord name (e.g. ``"Am"``, ``"G"``, ``"D"``).
duration: Total duration of the strum (default QUARTER).
direction: ``"down"`` (low→high, default) or ``"up"`` (high→low).
velocity: Base velocity (each string gets slight variation).
strum_time: Time in beats for the full strum sweep
(default 0.03 = very fast). Larger values = slower,
more audible strum. Try 0.1 for a lazy strum.
Returns:
Self for chaining.
Example::
>>> guitar = score.part("guitar", instrument="acoustic_guitar",
... fretboard=Fretboard.guitar())
>>> guitar.strum("Am", Duration.HALF)
>>> guitar.strum("G", Duration.HALF, direction="up")
"""
if self._fretboard is None:
raise ValueError(
"Cannot strum without a fretboard. "
"Set fretboard= when creating the part."
)
from .charts import CHARTS
# Get the fingering
system_name = self._system if isinstance(self._system, str) else "western"
if system_name in CHARTS:
chart = CHARTS[system_name]
else:
chart = CHARTS["western"]
if chord_name in chart:
fingering = chart[chord_name].fingering(fretboard=self._fretboard)
else:
# Try fretboard.chord() as fallback
fingering = self._fretboard.chord(chord_name)
# Get the sounding tones (skips muted strings)
tones = fingering.tones # list of Tone objects, high to low
if not tones:
self.rest(duration)
return self
# Order: down strum = low to high (reverse since tones are high-to-low)
if direction == "down":
strum_tones = list(reversed(tones))
else:
strum_tones = list(tones)
if hasattr(duration, 'value'):
total_beats = duration.value
else:
total_beats = float(duration)
# Build a Chord — all strings ring together through the
# shared body resonance, like a real guitar
from .chords import Chord as ChordClass
chord_obj = ChordClass(tones=strum_tones)
# Strum: hold a quiet leading string simultaneously with the
# full chord using hold(). No timing gap — both start at the
# same beat position. The leading string adds strum texture.
n_strings = len(strum_tones)
if strum_time > 0 and n_strings >= 3:
grace_vel = max(1, int(velocity * 0.15))
self.hold(strum_tones[0], total_beats, velocity=grace_vel)
self.add(chord_obj, total_beats, velocity=velocity)
return self
def roll(self, tone_or_string, duration=Duration.WHOLE, *,
velocity_start: int = 40, velocity_end: int = 100,
speed=Duration.SIXTEENTH) -> "Part":
"""Play a roll — rapid repeated notes with velocity ramp.
Perfect for timpani rolls, snare rolls, tremolo on any
instrument. The velocity ramps from ``velocity_start`` to
``velocity_end`` over the duration for crescendo/decrescendo.
Args:
tone_or_string: The note to repeat.
duration: Total duration of the roll.
velocity_start: Velocity of the first hit (default 40).
velocity_end: Velocity of the last hit (default 100).
speed: How fast to repeat (default SIXTEENTH notes).
Returns:
Self for chaining.
Example::
>>> timp = score.part("timp", instrument="timpani")
>>> timp.roll("C3", Duration.WHOLE, velocity_start=30, velocity_end=110)
"""
if hasattr(duration, 'value'):
total = duration.value
else:
total = float(duration)
if hasattr(speed, 'value'):
step = speed.value
else:
step = float(speed)
n_hits = max(1, int(total / step))
for i in range(n_hits):
frac = i / max(1, n_hits - 1)
vel = int(velocity_start + (velocity_end - velocity_start) * frac)
vel = max(1, min(127, vel))
remaining = total - i * step
note_dur = min(step, remaining)
if note_dur > 0:
self.add(tone_or_string, note_dur, velocity=vel)
return self
@property
def is_drums(self) -> bool:
"""True if this part contains drum hits."""
return len(self._drum_hits) > 0
@property
def total_beats(self) -> float:
note_beats = sum(n.beats for n in self.notes)
if self._drum_hits:
drum_beats = self._drum_pattern_beats
return max(note_beats, drum_beats)
return note_beats
# ── ASCII tablature export ──────────────────────────────────────────
_TAB_TUNINGS = {
"guitar": [40, 45, 50, 55, 59, 64],
"bass": [28, 33, 38, 43],
"drop_d": [38, 45, 50, 55, 59, 64],
}
_TAB_LABELS = {
"guitar": ["E", "A", "D", "G", "B", "e"],
"bass": ["E", "A", "D", "G"],
"drop_d": ["D", "A", "D", "G", "B", "e"],
}
def to_tab(self, *, tuning="guitar", frets=24, time_signature=None):
"""Generate ASCII guitar/bass tablature from this part's notes.
Args:
tuning: ``"guitar"`` (6-string standard), ``"bass"`` (4-string),
``"drop_d"`` (guitar drop D), or a list of MIDI note numbers
for custom tuning (low string first).
frets: Maximum fret number (default 24).
time_signature: A ``TimeSignature`` or ``None`` for 4/4.
Returns:
A multi-line ASCII tablature string.
"""
if isinstance(tuning, str):
open_midis = list(self._TAB_TUNINGS[tuning])
labels = list(self._TAB_LABELS[tuning])
else:
open_midis = list(tuning)
_note_names = ["C", "C#", "D", "D#", "E", "F",
"F#", "G", "G#", "A", "A#", "B"]
labels = [_note_names[m % 12] for m in open_midis]
n_strings = len(open_midis)
beats_per_measure = 4.0
if time_signature is not None:
beats_per_measure = time_signature.beats_per_measure
# Build columns: each column is a list[str] of length n_strings
columns: list[list[str]] = []
beat_acc = 0.0
for note in self.notes:
dur_beats = note.duration.value
# Insert barline if we've crossed a measure boundary
while beat_acc >= beats_per_measure - 0.001:
columns.append(["|"] * n_strings)
beat_acc -= beats_per_measure
col = ["---"] * n_strings
tone = note.tone
if tone is None or isinstance(tone, _DrumTone):
pass
elif hasattr(tone, "tones"):
# Chord — assign each chord tone to a different string
used: set[int] = set()
for ct in tone.tones:
midi_val = getattr(ct, "midi", None)
if midi_val is None:
continue
best_s, best_f = self._find_best_string(
midi_val, open_midis, frets, used)
if best_s is not None:
fret_str = str(best_f)
col[best_s] = fret_str.center(3, "-")
used.add(best_s)
else:
midi_val = getattr(tone, "midi", None)
if midi_val is not None:
best_s, best_f = self._find_best_string(
midi_val, open_midis, frets, set())
if best_s is not None:
fret_str = str(best_f)
col[best_s] = fret_str.center(3, "-")
columns.append(col)
if not note._hold:
beat_acc += dur_beats
# Trailing barline
if columns and columns[-1] != ["|"] * n_strings:
while beat_acc >= beats_per_measure - 0.001:
columns.append(["|"] * n_strings)
beat_acc -= beats_per_measure
columns.append(["|"] * n_strings)
# Build output lines (highest-pitched string first in display)
lines: list[str] = []
for s_idx in range(n_strings - 1, -1, -1):
label = labels[s_idx]
parts_str = "".join(c[s_idx] for c in columns)
lines.append(f"{label}|{parts_str}")
return "\n".join(lines)
@staticmethod
def _find_best_string(midi_val, open_midis, max_fret, used):
"""Find the best string/fret for a MIDI note.
Returns (string_index, fret) or (None, None) if unplayable.
"""
best_s = None
best_f = None
for s_idx, open_m in enumerate(open_midis):
if s_idx in used:
continue
f = midi_val - open_m
if 0 <= f <= max_fret:
if best_f is None or f < best_f:
best_s = s_idx
best_f = f
return best_s, best_f
def __len__(self):
return len(self.notes) + len(self._drum_hits)
def __iter__(self):
return iter(self.notes)
def __repr__(self):
return (f"<Part {self.name!r} synth={self.synth} "
f"{len(self.notes)} notes {self.total_beats:.1f} beats>")
class Section:
"""A named section of a Score (verse, chorus, bridge, etc.)."""
def __init__(self, name: str, score: "Score"):
self.name = name
self._score = score
self._start_beat = score.total_beats
# Snapshot current state
self._part_starts: dict[str, int] = {
n: len(p.notes) for n, p in score.parts.items()
}
self._default_start = len(score.notes)
self._drum_start = len(score._drum_hits)
self._drum_beat_start = score._drum_pattern_beats
self._finalized = False
self._part_notes: dict[str, list[Note]] = {}
self._default_notes: list[Note] = []
self._drum_hits: list[_Hit] = []
self._drum_beat_duration: float = 0
self._duration: float = 0
@property
def beats(self) -> float:
if self._finalized:
return self._duration
return self._score.total_beats - self._start_beat
def _finalize(self):
if self._finalized:
return
s = self._score
# Capture notes added since snapshot
for pname, start_idx in self._part_starts.items():
if pname in s.parts:
self._part_notes[pname] = list(s.parts[pname].notes[start_idx:])
self._default_notes = list(s.notes[self._default_start:])
# Capture drum hits added since snapshot
self._drum_hits = list(s._drum_hits[self._drum_start:])
self._drum_beat_duration = s._drum_pattern_beats - self._drum_beat_start
self._duration = s.total_beats - self._start_beat
self._finalized = True
class Score:
"""A multi-part arrangement with drums, chords, and instrument voices.
A Score combines:
- **Drum patterns** via ``add_pattern()``
- **Chord/tone notes** via ``add()`` (backwards-compatible default part)
- **Named parts** via ``part()`` — each with its own synth and envelope
Example::
score = Score("4/4", bpm=140)
score.add_pattern(Pattern.preset("bossa nova"), repeats=4)
chords = score.part("chords", synth="sine", envelope="pad")
lead = score.part("lead", synth="saw", envelope="pluck")
bass = score.part("bass", synth="triangle", envelope="pluck")
for chord in key.progression("i", "iv", "V", "i"):
chords.add(chord, Duration.WHOLE)
lead.add("E5", Duration.QUARTER).add("D5", Duration.EIGHTH)
bass.add("A2", Duration.HALF).add("D2", Duration.HALF)
play_score(score)
"""
def __init__(self, time_signature="4/4", bpm=120, swing: float = 0.0,
drum_humanize: float = 0.15, system: str = "western",
temperament: str = "equal", reference_pitch: float = 440.0):
if isinstance(time_signature, str):
self.time_signature = TimeSignature.from_string(time_signature)
else:
self.time_signature = time_signature
self.bpm = bpm
self.swing = swing
self.system = system
self.temperament = temperament
self.reference_pitch = reference_pitch
self._drum_humanize = drum_humanize
self.notes: list[Note] = []
self.parts: dict[str, Part] = {}
self._tempo_changes: list[tuple[float, int]] = []
self._sections: dict[str, Section] = {}
self._current_section: Optional[Section] = None
def _ensure_drums_part(self) -> Part:
"""Get or create the drums Part."""
if "drums" not in self.parts:
self.parts["drums"] = Part("drums", synth="sine", volume=0.7)
return self.parts["drums"]
@property
def _drum_hits(self) -> list:
"""Proxy: drum hits live on the drums Part."""
return self._ensure_drums_part()._drum_hits
@property
def _drum_pattern_beats(self) -> float:
"""Proxy: drum pattern beats live on the drums Part."""
return self._ensure_drums_part()._drum_pattern_beats
@_drum_pattern_beats.setter
def _drum_pattern_beats(self, value: float):
self._ensure_drums_part()._drum_pattern_beats = value
@property
def drum_effects(self) -> dict:
"""Proxy: drum effects are just the drums Part's effect settings."""
p = self._ensure_drums_part()
return {
"reverb_mix": p.reverb_mix, "reverb_decay": p.reverb_decay,
"reverb_type": p.reverb_type,
"delay_mix": p.delay_mix, "delay_time": p.delay_time,
"delay_feedback": p.delay_feedback,
"lowpass": p.lowpass, "lowpass_q": p.lowpass_q,
"distortion_mix": p.distortion_mix,
"distortion_drive": p.distortion_drive,
"chorus_mix": p.chorus_mix,
}
def set_drum_effects(self, **kwargs) -> "Score":
"""Set effects on all drum parts.
When drums are split, applies to every drum Part (kick, snare,
hats, etc.). When not split, applies to the single drums Part.
Example::
score.set_drum_effects(reverb=0.2, reverb_type="plate")
"""
param_map = {"reverb": "reverb_mix", "delay": "delay_mix",
"distortion": "distortion_mix", "chorus": "chorus_mix"}
drum_parts = [p for p in self.parts.values() if p.is_drums]
if not drum_parts:
drum_parts = [self._ensure_drums_part()]
for p in drum_parts:
for k, v in kwargs.items():
attr = param_map.get(k, k)
setattr(p, attr, v)
return self
def part(self, name: str, *, instrument: str = None,
synth: str = None, envelope: str = None,
volume: float = None,
reverb: float = None, reverb_decay: float = None,
reverb_type: str = None,
delay: float = None, delay_time: float = None,
delay_feedback: float = None,
highpass: float = None, highpass_q: float = None,
lowpass: float = None, lowpass_q: float = None,
distortion: float = None, distortion_drive: float = None,
legato: bool = None, glide: float = None,
chorus: float = None, chorus_rate: float = None,
chorus_depth: float = None,
swing: Optional[float] = None,
humanize: float = None,
sidechain: float = None,
sidechain_release: float = None,
detune: float = None,
pan: float = None,
spread: float = None,
# New synth engine params
sub_osc: float = None,
noise_mix: float = None,
filter_attack: float = None,
filter_decay: float = None,
filter_sustain: float = None,
filter_amount: float = None,
vel_to_filter: float = None,
saturation: float = None,
tremolo_depth: float = None,
tremolo_rate: float = None,
phaser: float = None,
phaser_rate: float = None,
cabinet: float = None,
cabinet_brightness: float = None,
analog: float = None,
ensemble: int = None,
fm_ratio: float = None,
fm_index: float = None,
fretboard=None) -> Part:
"""Create a named part with its own synth voice and effects.
Args:
name: Part name (e.g. ``"lead"``, ``"bass"``, ``"pads"``).
instrument: Instrument preset name (e.g. ``"piano"``,
``"violin"``, ``"808_bass"``). See :data:`INSTRUMENTS`
for the full list. When set, the preset's synth, envelope,
and effects are used as defaults; any explicit keyword
argument still overrides the preset value.
synth: Waveform — ``"sine"``, ``"saw"``, ``"triangle"``,
``"square"``, ``"pulse"``, ``"fm"``, ``"noise"``,
``"supersaw"``, ``"pwm_slow"``, ``"pwm_fast"``.
envelope: ADSR preset name — ``"piano"``, ``"pluck"``,
``"pad"``, ``"organ"``, ``"bell"``, ``"strings"``,
``"staccato"``, or ``"none"``.
volume: Mix level from 0.0 to 1.0 (default 0.5).
reverb: Reverb wet/dry mix, 0.01.0 (default 0, off).
reverb_decay: Reverb tail length in seconds (default 1.0).
reverb_type: Reverb algorithm — ``"algorithmic"`` (Schroeder, default)
or a convolution IR preset: ``"taj_mahal"``, ``"cathedral"``,
``"plate"``, ``"spring"``, ``"cave"``, ``"parking_garage"``,
``"canyon"``.
delay: Delay wet/dry mix, 0.01.0 (default 0, off).
delay_time: Delay time in seconds (default 0.375, dotted 8th).
delay_feedback: Delay feedback 0.01.0 (default 0.4).
lowpass: Lowpass filter cutoff in Hz (default 0, off).
Try 800 for muffled bass, 2000 for warm lead,
5000 for subtle brightness rolloff.
lowpass_q: Filter resonance/Q factor (default 0.707, flat).
Higher values add a resonant peak at the cutoff —
1.0 = slight peak, 2.0 = pronounced, 5.0+ = aggressive.
distortion: Distortion wet/dry mix, 0.01.0 (default 0, off).
distortion_drive: Gain before soft clipping (default 3.0).
0.52 = subtle warmth, 38 = overdrive, 10+ = fuzz.
legato: If True, notes share a continuous waveform instead
of retriggering the envelope on each note (default False).
glide: Portamento time in seconds between consecutive pitches
(default 0, instant). 0.030.05 = quick 303 slide,
0.10.2 = slow glide.
humanize: Random timing and velocity variation, 0.01.0
(default 0, off). Adds micro-imperfections that make
programmed parts feel like a real player.
0.1 = subtle, 0.3 = natural, 0.5+ = loose/drunk.
sidechain: Sidechain compression amount, 0.01.0 (default 0, off).
How much the drum hits duck this part's volume.
0.8 = typical EDM pumping effect.
sidechain_release: How fast the volume comes back after ducking,
in seconds (default 0.1).
Returns:
A :class:`Part` object. Add notes with ``.add()`` and ``.rest()``.
Example::
lead = score.part("lead", synth="saw", envelope="pluck",
reverb=0.3, delay=0.25, lowpass=3000)
# Or use an instrument preset:
piano = score.part("keys", instrument="piano")
"""
# Default values for all Part parameters.
_defaults = {
"synth": "sine", "envelope": "piano", "volume": 0.5,
"reverb": 0.0, "reverb_decay": 1.0, "reverb_type": "algorithmic",
"delay": 0.0, "delay_time": 0.375, "delay_feedback": 0.4,
"lowpass": 0.0, "lowpass_q": 0.707,
"distortion": 0.0, "distortion_drive": 3.0,
"legato": False, "glide": 0.0,
"chorus": 0.0, "chorus_rate": 1.5, "chorus_depth": 0.003,
"swing": None, "humanize": 0.0,
"sidechain": 0.0, "sidechain_release": 0.1,
"detune": 0.0, "pan": 0.0, "spread": 0.0,
}
# If an instrument preset is specified, layer it on top of defaults.
if instrument is not None:
preset = INSTRUMENTS.get(instrument)
if preset is None:
raise ValueError(
f"Unknown instrument: {instrument!r}. "
f"Use Score.list_instruments() to see available presets."
)
_defaults.update(preset)
# Collect explicitly-provided kwargs (non-None) and override defaults.
explicit = {}
_locals = {
"synth": synth, "envelope": envelope, "volume": volume,
"reverb": reverb, "reverb_decay": reverb_decay,
"reverb_type": reverb_type,
"delay": delay, "delay_time": delay_time,
"delay_feedback": delay_feedback,
"highpass": highpass, "highpass_q": highpass_q,
"lowpass": lowpass, "lowpass_q": lowpass_q,
"distortion": distortion, "distortion_drive": distortion_drive,
"legato": legato, "glide": glide,
"chorus": chorus, "chorus_rate": chorus_rate,
"chorus_depth": chorus_depth,
"swing": swing, "humanize": humanize,
"sidechain": sidechain, "sidechain_release": sidechain_release,
"detune": detune, "pan": pan, "spread": spread,
"sub_osc": sub_osc, "noise_mix": noise_mix,
"filter_attack": filter_attack, "filter_decay": filter_decay,
"filter_sustain": filter_sustain, "filter_amount": filter_amount,
"vel_to_filter": vel_to_filter,
"saturation": saturation,
"tremolo_depth": tremolo_depth, "tremolo_rate": tremolo_rate,
"phaser": phaser, "phaser_rate": phaser_rate,
"cabinet": cabinet, "cabinet_brightness": cabinet_brightness,
"analog": analog, "ensemble": ensemble,
"fm_ratio": fm_ratio, "fm_index": fm_index,
}
for k, v in _locals.items():
if v is not None:
explicit[k] = v
merged = {**_defaults, **explicit}
p = Part(name, **merged)
p._system = self.system
p._fretboard = fretboard
self.parts[name] = p
return p
@classmethod
def list_instruments(cls) -> list:
"""Return a sorted list of available instrument preset names.
Example::
Score.list_instruments()
# ['808_bass', 'acid_bass', 'acoustic_guitar', ...]
"""
return sorted(INSTRUMENTS.keys())
def add_pattern(self, pattern, repeats: int = 1) -> "Score":
"""Add a drum pattern to this score.
Args:
pattern: A :class:`Pattern` object.
repeats: Number of times to repeat.
Returns:
Self for chaining.
"""
for r in range(repeats):
offset = self._drum_pattern_beats + r * pattern.beats
for hit in pattern.hits:
self._drum_hits.append(
_Hit(hit.sound, hit.position + offset, hit.velocity))
self._drum_pattern_beats += repeats * pattern.beats
return self
def fill(self, name: str = "rock") -> "Score":
"""Insert a 1-bar drum fill at the current position.
Replaces what would be the next bar of drums with a genre-appropriate fill.
"""
fill_pattern = Pattern.fill(name)
return self.add_pattern(fill_pattern, repeats=1)
# Drum sound groups for split mode
_DRUM_GROUPS = {
"kick": {DrumSound.KICK.value},
"snare": {DrumSound.SNARE.value, DrumSound.RIMSHOT.value, DrumSound.CLAP.value},
"hats": {DrumSound.CLOSED_HAT.value, DrumSound.OPEN_HAT.value, DrumSound.PEDAL_HAT.value},
"toms": {DrumSound.LOW_TOM.value, DrumSound.MID_TOM.value, DrumSound.HIGH_TOM.value},
"cymbals": {DrumSound.CRASH.value, DrumSound.RIDE.value, DrumSound.RIDE_BELL.value},
"percussion": {DrumSound.COWBELL.value, DrumSound.CLAVE.value, DrumSound.SHAKER.value,
DrumSound.TAMBOURINE.value, DrumSound.CONGA_HIGH.value, DrumSound.CONGA_LOW.value,
DrumSound.BONGO_HIGH.value, DrumSound.BONGO_LOW.value, DrumSound.TIMBALE_HIGH.value,
DrumSound.TIMBALE_LOW.value, DrumSound.AGOGO_HIGH.value, DrumSound.AGOGO_LOW.value,
DrumSound.GUIRO.value, DrumSound.MARACAS.value},
}
def drums(self, preset: str, repeats: int = 4, fill: str = None,
fill_every: int = None, split: bool = False) -> "Score":
"""Add a drum pattern by preset name, with optional auto-fills.
Args:
preset: Pattern preset name (e.g. ``"bossa nova"``, ``"rock"``).
repeats: Number of times to repeat (default 4).
fill: Optional fill name.
fill_every: Replace every Nth bar with a fill.
split: If True, create separate Parts for kick, snare, hats,
toms, cymbals, and percussion — each with independent
effects. Access via ``score.parts["kick"]``, etc.
Returns:
Self for chaining.
Example::
>>> score.drums("rock", repeats=4, split=True)
>>> score.parts["snare"].reverb_mix = 0.3
>>> score.parts["hats"].lowpass = 6000
"""
if fill is None:
self.add_pattern(Pattern.preset(preset), repeats=repeats)
else:
groove = Pattern.preset(preset)
fill_pattern = Pattern.fill(fill)
if fill_every is None:
fill_every = repeats
for bar in range(1, repeats + 1):
if bar % fill_every == 0:
self.add_pattern(fill_pattern, repeats=1)
else:
self.add_pattern(groove, repeats=1)
if split:
self._split_drums()
return self
def _split_drums(self):
"""Move drum hits from the 'drums' Part into separate group Parts."""
drums_part = self.parts.get("drums")
if not drums_part:
return
all_hits = list(drums_part._drum_hits)
pattern_beats = drums_part._drum_pattern_beats
drums_part._drum_hits.clear()
drums_part._drum_pattern_beats = 0.0
for group_name, sound_values in self._DRUM_GROUPS.items():
group_hits = [h for h in all_hits if h.sound.value in sound_values]
if group_hits:
if group_name not in self.parts:
self.parts[group_name] = Part(group_name, synth="sine", volume=0.7)
p = self.parts[group_name]
p._drum_hits.extend(group_hits)
p._drum_pattern_beats = max(p._drum_pattern_beats, pattern_beats)
# Remove empty drums Part
if not drums_part._drum_hits and "drums" in self.parts:
del self.parts["drums"]
def add(self, tone_or_chord, duration=Duration.QUARTER) -> "Score":
"""Add a note to the default (unnamed) part.
For simple scores without named parts. Returns self for chaining.
"""
self.notes.append(Note(tone=tone_or_chord, duration=duration))
return self
def rest(self, duration=Duration.QUARTER) -> "Score":
"""Add a rest to the default part. Returns self for chaining."""
self.notes.append(Note(tone=None, duration=duration))
return self
def set_tempo(self, bpm: int) -> "Score":
"""Insert a tempo change at the current beat position.
The new tempo takes effect from the current total_beats position
and remains until the next tempo change.
Args:
bpm: New tempo in beats per minute.
Returns:
Self for chaining.
"""
self._tempo_changes.append((self.total_beats, bpm))
return self
def section(self, name: str) -> "Section":
"""Begin a named section. Everything added after this call until
the next section() or end_section() belongs to this section.
Example::
score.section("verse")
chords.add(chord, Duration.WHOLE)
lead.add("C5", Duration.QUARTER)
score.section("chorus")
chords.add(chord, Duration.WHOLE)
score.repeat("verse")
score.repeat("chorus", times=2)
"""
# Finalize the previous section if any
if self._current_section is not None:
self._current_section._finalize()
sec = Section(name, self)
self._sections[name] = sec
self._current_section = sec
return sec
def end_section(self) -> "Score":
"""Close the current section explicitly.
Returns:
Self for chaining.
"""
if self._current_section is not None:
self._current_section._finalize()
self._current_section = None
return self
def repeat(self, name: str, times: int = 1) -> "Score":
"""Repeat a previously defined section.
Copies all notes, drum hits, and automation from the named section
and appends them at the current position.
Args:
name: Name of a section defined with ``section()``.
times: Number of times to repeat (default 1).
Returns:
Self for chaining.
"""
if name not in self._sections:
raise ValueError(f"Unknown section: {name!r}")
sec = self._sections[name]
# Ensure section is finalized
if not sec._finalized:
sec._finalize()
for _ in range(times):
# Copy notes to each part
for pname, notes in sec._part_notes.items():
if pname in self.parts:
for note in notes:
self.parts[pname].notes.append(
Note(tone=note.tone, duration=note.duration,
velocity=note.velocity))
# Copy default notes
for note in sec._default_notes:
self.notes.append(
Note(tone=note.tone, duration=note.duration,
velocity=note.velocity))
# Copy drum hits with offset
if sec._drum_hits:
offset = self._drum_pattern_beats - sec._drum_beat_start
for hit in sec._drum_hits:
self._drum_hits.append(
_Hit(hit.sound, hit.position + offset, hit.velocity))
self._drum_pattern_beats += sec._drum_beat_duration
return self
@property
def total_beats(self) -> float:
beats = [sum(n.beats for n in self.notes), self._drum_pattern_beats]
for p in self.parts.values():
beats.append(p.total_beats)
return max(beats) if beats else 0.0
@property
def measures(self) -> float:
"""Number of measures (may be fractional if incomplete)."""
return self.total_beats / self.time_signature.beats_per_measure
@property
def duration_ms(self) -> float:
"""Total duration in milliseconds."""
ms_per_beat = 60_000 / self.bpm
return self.total_beats * ms_per_beat
def __len__(self):
return len(self.notes) + sum(len(p) for p in self.parts.values())
def __iter__(self):
return iter(self.notes)
def __repr__(self):
part_info = ""
if self.parts:
part_info = f" {len(self.parts)} parts"
return (
f"<Score {self.time_signature} {self.bpm}bpm"
f"{part_info} {self.measures:.1f} measures>"
)
# ── ABC notation export ────────────────────────────────────────────
def to_abc(self, *, title="Untitled", key="C", html=False):
"""Export the score as ABC notation.
Args:
title: Tune title for the ``T:`` field.
key: Key signature (e.g. ``"C"``, ``"Gm"``, ``"D"``) for the
``K:`` field.
html: If *True*, wrap the ABC string in a self-contained HTML
page that renders sheet music via abcjs.
Returns:
An ABC notation string, or a full HTML document string when
*html* is True.
"""
ts = self.time_signature
default_unit = 8 # L:1/8
lines = [
"X:1",
f"T:{title}",
f"M:{ts.beats}/{ts.unit}",
f"Q:1/4={self.bpm}",
f"L:1/{default_unit}",
]
# Collect voices: default notes first, then named parts
# Skip drum parts and parts with no pitched notes
voices: list[tuple[str, list]] = []
if self.notes:
voices.append(("default", self.notes))
for name, part in self.parts.items():
if part.is_drums:
continue
if not part.notes:
continue
# Skip parts that have no pitched tones (only drum tones / rests)
has_pitched = any(
n.tone is not None
and (hasattr(n.tone, "name") or hasattr(n.tone, "tones"))
for n in part.notes
)
if not has_pitched:
continue
voices.append((name, part.notes))
multi = len(voices) > 1
if multi:
for i, (vname, notes) in enumerate(voices, 1):
clef = self._guess_clef(notes)
clef_str = f" clef={clef}" if clef != "treble" else ""
lines.append(f"V:{i} name=\"{vname}\"{clef_str}")
lines.append(f"K:{key}")
for i, (_, notes) in enumerate(voices, 1):
lines.append(f"V:{i}")
lines.append(self._notes_to_abc(notes, default_unit, ts))
else:
lines.append(f"K:{key}")
if voices:
lines.append(self._notes_to_abc(voices[0][1], default_unit, ts))
abc = "\n".join(lines) + "\n"
if not html:
return abc
return (
"<!DOCTYPE html>\n<html><head><meta charset=\"utf-8\">\n"
"<title>" + title + "</title>\n"
"<script src=\"https://cdn.jsdelivr.net/npm/abcjs@6/dist"
"/abcjs-basic-min.js\"></script>\n"
"</head><body>\n<div id=\"score\"></div>\n<script>\n"
"ABCJS.renderAbc(\"score\", "
+ repr(abc)
+ ");\n</script>\n</body></html>\n"
)
@staticmethod
def _guess_clef(notes):
"""Return 'bass' if most pitched notes are below C4, else 'treble'."""
octaves = []
for note in notes:
tone = note.tone
if tone is None or not hasattr(tone, "octave"):
continue
if hasattr(tone, "tones"):
# Chord — use average of chord tones
for t in tone.tones:
if hasattr(t, "octave") and t.octave is not None:
octaves.append(t.octave)
elif tone.octave is not None:
octaves.append(tone.octave)
if not octaves:
return "treble"
avg = sum(octaves) / len(octaves)
return "bass" if avg < 4 else "treble"
@staticmethod
def _tone_to_abc(tone, default_unit):
"""Convert a single Tone to an ABC note string."""
if tone is None:
return "z"
# Skip drum tones — they don't have pitched names
if not hasattr(tone, "name") or not hasattr(tone, "octave"):
return "z"
name = tone.name # e.g. "C", "C#", "Bb"
octave = tone.octave if tone.octave is not None else 4
# ABC accidentals: ^ = sharp, _ = flat, ^^ = double sharp, __ = double flat
letter = name[0].upper()
acc = name[1:] if len(name) > 1 else ""
abc_acc = acc.replace("##", "^^").replace("#", "^").replace("bb", "__").replace("b", "_")
# ABC octave: C-B = octave 4, c-b = octave 5,
# c' = 6, c'' = 7, C, = 3, C,, = 2
if octave >= 5:
note_char = letter.lower()
ticks = octave - 5
oct_str = "'" * ticks
else:
note_char = letter.upper()
commas = 4 - octave
oct_str = "," * commas
return f"{abc_acc}{note_char}{oct_str}"
@staticmethod
def _format_dur(multiplier):
"""Format an ABC duration multiplier string."""
if abs(multiplier - 1) < 0.001:
return ""
elif abs(multiplier - int(multiplier)) < 0.001:
return str(int(multiplier))
elif abs(multiplier - 0.5) < 0.001:
return "/2"
elif abs(multiplier - 0.25) < 0.001:
return "/4"
elif abs(multiplier - 1.5) < 0.001:
return "3/2"
else:
from fractions import Fraction
frac = Fraction(multiplier).limit_denominator(16)
return f"{frac.numerator}/{frac.denominator}"
def _notes_to_abc(self, notes, default_unit, ts,
bars_per_line=4):
"""Convert a list of Note objects to an ABC body string."""
beats_per_measure = ts.beats_per_measure
tokens = []
beat_in_measure = 0.0
measure_count = 0
for note in notes:
total_beats = note.duration.value
unit_beats = 4.0 / default_unit # beats per L unit
if note.tone is None:
abc_note = "z"
elif hasattr(note.tone, "tones"):
chord_notes = [
self._tone_to_abc(t, default_unit)
for t in note.tone.tones
]
abc_note = "[" + "".join(chord_notes) + "]"
else:
abc_note = self._tone_to_abc(note.tone, default_unit)
# Split notes longer than one measure into tied pieces
remaining = total_beats
first_chunk = True
while remaining > 0.001:
# How much room left in this measure?
room = beats_per_measure - beat_in_measure
chunk = min(remaining, room) if remaining > room + 0.001 else remaining
needs_tie = remaining - chunk > 0.001
multiplier = chunk / unit_beats
dur_str = self._format_dur(multiplier)
tie_str = "-" if needs_tie and abc_note != "z" else ""
tokens.append(f"{abc_note}{dur_str}{tie_str}")
remaining -= chunk
beat_in_measure += chunk
first_chunk = False
if beat_in_measure >= beats_per_measure - 0.001:
measure_count += 1
if measure_count % bars_per_line == 0:
tokens.append("|\n")
else:
tokens.append("|")
beat_in_measure -= beats_per_measure
body = " ".join(tokens)
# Clean up trailing/double barlines
body = body.replace("| |", "|").rstrip("| \n").rstrip()
if not body.endswith("|"):
body += " |"
return body
# ── LilyPond notation export ─────────────────────────────────────
def to_lilypond(self, *, title="Untitled", key="C", mode="major"):
"""Export the score as a LilyPond source string.
Args:
title: Title for the ``\\header`` block.
key: Key signature root (e.g. ``"C"``, ``"D"``, ``"Bb"``).
mode: LilyPond mode string (``"major"``, ``"minor"``, etc.).
Returns:
A complete LilyPond source string.
"""
ts = self.time_signature
# Collect voices (same filter as to_abc)
voices: list[tuple[str, list]] = []
if self.notes:
voices.append(("default", self.notes))
for name, part in self.parts.items():
if part.is_drums:
continue
if not part.notes:
continue
has_pitched = any(
n.tone is not None
and (hasattr(n.tone, "name") or hasattr(n.tone, "tones"))
for n in part.notes
)
if not has_pitched:
continue
voices.append((name, part.notes))
ly_key = self._tone_name_to_lilypond(key)
staves = []
for vname, notes in voices:
clef = self._guess_clef(notes)
body = self._notes_to_lilypond(notes, ts)
staff = (
f' \\new Staff \\with {{ instrumentName = "{vname}" }} {{\n'
f" \\clef {clef}\n"
f" \\key {ly_key} \\{mode}\n"
f" \\time {ts.beats}/{ts.unit}\n"
f" \\tempo 4 = {self.bpm}\n"
f" {body}\n"
f" }}"
)
staves.append(staff)
staves_block = "\n".join(staves)
return (
f'\\version "2.24.0"\n'
f"\\header {{\n"
f' title = "{title}"\n'
f"}}\n\n"
f"\\score {{\n"
f" \\new StaffGroup <<\n"
f"{staves_block}\n"
f" >>\n"
f" \\layout {{ }}\n"
f"}}\n"
)
@staticmethod
def _tone_name_to_lilypond(name):
"""Convert a note name like 'C#', 'Bb', 'F' to LilyPond pitch."""
if not name:
return "c"
letter = name[0].lower()
acc = name[1:] if len(name) > 1 else ""
ly_acc = (
acc.replace("##", "isis")
.replace("#", "is")
.replace("bb", "eses")
.replace("b", "es")
)
return f"{letter}{ly_acc}"
@staticmethod
def _tone_to_lilypond(tone):
"""Convert a single Tone to a LilyPond pitch string (no duration)."""
if tone is None:
return None
if not hasattr(tone, "name") or not hasattr(tone, "octave"):
return None
name = tone.name
octave = tone.octave if tone.octave is not None else 4
letter = name[0].lower()
acc = name[1:] if len(name) > 1 else ""
ly_acc = (
acc.replace("##", "isis")
.replace("#", "is")
.replace("bb", "eses")
.replace("b", "es")
)
# LilyPond: c = C3, c' = C4, c'' = C5, c, = C2, c,, = C1
if octave >= 4:
oct_str = "'" * (octave - 3)
else:
oct_str = "," * (3 - octave)
return f"{letter}{ly_acc}{oct_str}"
@staticmethod
def _beats_to_lilypond_dur(beats):
"""Convert a beat count to a LilyPond duration string."""
_MAP = {
4.0: "1",
2.0: "2",
1.0: "4",
0.5: "8",
0.25: "16",
3.0: "2.",
1.5: "4.",
}
for ref, ly in _MAP.items():
if abs(beats - ref) < 0.001:
return ly
if abs(beats - 2 / 3) < 0.05:
return "4"
closest = min(_MAP, key=lambda k: abs(k - beats))
return _MAP[closest]
def _notes_to_lilypond(self, notes, ts, bars_per_line=4):
"""Convert a list of Note objects to a LilyPond music body string."""
beats_per_measure = ts.beats_per_measure
tokens: list[str] = []
beat_in_measure = 0.0
measure_count = 0
for note in notes:
total_beats = note.duration.value
if note.tone is None:
pitch = None
is_rest = True
elif hasattr(note.tone, "tones"):
chord_pitches = []
for t in note.tone.tones:
p = self._tone_to_lilypond(t)
if p is not None:
chord_pitches.append(p)
if chord_pitches:
pitch = "<" + " ".join(chord_pitches) + ">"
is_rest = False
else:
pitch = None
is_rest = True
else:
p = self._tone_to_lilypond(note.tone)
if p is not None:
pitch = p
is_rest = False
else:
pitch = None
is_rest = True
remaining = total_beats
while remaining > 0.001:
room = beats_per_measure - beat_in_measure
chunk = min(remaining, room) if remaining > room + 0.001 else remaining
needs_tie = remaining - chunk > 0.001
dur_str = self._beats_to_lilypond_dur(chunk)
if is_rest or pitch is None:
tokens.append(f"r{dur_str}")
else:
tie_str = "~" if needs_tie else ""
tokens.append(f"{pitch}{dur_str}{tie_str}")
remaining -= chunk
beat_in_measure += chunk
if beat_in_measure >= beats_per_measure - 0.001:
measure_count += 1
if measure_count % bars_per_line == 0:
tokens.append("|\n ")
else:
tokens.append("|")
beat_in_measure -= beats_per_measure
body = " ".join(tokens)
body = body.replace("| |", "|").rstrip("| \n").rstrip()
if not body.endswith("|"):
body += " |"
return body
# ── MusicXML export ───────────────────────────────────────────────
def to_musicxml(self, *, title="Untitled"):
"""Export the score as a MusicXML string.
Args:
title: Work title embedded in the ``<work-title>`` element.
Returns:
A MusicXML 4.0 partwise document as a pretty-printed XML string.
"""
import xml.etree.ElementTree as ET
import xml.dom.minidom
DIVISIONS = 4 # divisions per quarter note
_DUR_MAP = {
4.0: ("whole", False),
3.0: ("half", True),
2.0: ("half", False),
1.5: ("quarter", True),
1.0: ("quarter", False),
0.5: ("eighth", False),
0.25: ("16th", False),
}
def _beats_to_divisions(beats):
return int(round(beats * DIVISIONS))
def _best_dur_type(beats):
for val, info in _DUR_MAP.items():
if abs(beats - val) < 0.001:
return info
return None
def _split_into_measures(notes, beats_per_measure):
beat_in_measure = 0.0
for note in notes:
tone = note.tone
if tone is not None and not hasattr(tone, "name") and not hasattr(tone, "tones"):
tone = None
remaining = note.duration.value
is_first = True
while remaining > 0.001:
room = beats_per_measure - beat_in_measure
if room < 0.001:
room = beats_per_measure
beat_in_measure = 0.0
chunk = min(remaining, room)
needs_tie_start = (remaining - chunk) > 0.001
needs_tie_stop = not is_first
yield (tone, chunk, needs_tie_start, needs_tie_stop,
note.velocity, note.articulation)
remaining -= chunk
beat_in_measure += chunk
is_first = False
if beat_in_measure >= beats_per_measure - 0.001:
beat_in_measure = 0.0
def _tone_to_pitch_el(tone):
pitch = ET.Element("pitch")
name = tone.name
letter = name[0].upper()
acc_str = name[1:] if len(name) > 1 else ""
step = ET.SubElement(pitch, "step")
step.text = letter
alter_val = 0
if acc_str == "#":
alter_val = 1
elif acc_str == "##":
alter_val = 2
elif acc_str == "b":
alter_val = -1
elif acc_str == "bb":
alter_val = -2
if alter_val != 0:
alter = ET.SubElement(pitch, "alter")
alter.text = str(alter_val)
octave_el = ET.SubElement(pitch, "octave")
octave_el.text = str(tone.octave if tone.octave is not None else 4)
return pitch
def _add_note_el(measure, tone, dur_beats, is_chord_continuation,
tie_start, tie_stop, velocity):
note_el = ET.SubElement(measure, "note")
if is_chord_continuation:
ET.SubElement(note_el, "chord")
if tone is None:
ET.SubElement(note_el, "rest")
elif hasattr(tone, "tones"):
ET.SubElement(note_el, "rest")
else:
note_el.append(_tone_to_pitch_el(tone))
dur_el = ET.SubElement(note_el, "duration")
dur_el.text = str(_beats_to_divisions(dur_beats))
if tie_stop:
tie_s = ET.SubElement(note_el, "tie")
tie_s.set("type", "stop")
if tie_start:
tie_s = ET.SubElement(note_el, "tie")
tie_s.set("type", "start")
dur_info = _best_dur_type(dur_beats)
if dur_info:
type_el = ET.SubElement(note_el, "type")
type_el.text = dur_info[0]
if dur_info[1]:
ET.SubElement(note_el, "dot")
if tie_start or tie_stop:
notations = ET.SubElement(note_el, "notations")
if tie_stop:
tied = ET.SubElement(notations, "tied")
tied.set("type", "stop")
if tie_start:
tied = ET.SubElement(notations, "tied")
tied.set("type", "start")
# ── Collect voices ──────────────────────────────────────────
voices = []
if self.notes:
voices.append(("default", self.notes))
for name, part in self.parts.items():
if part.is_drums:
continue
if not part.notes:
continue
has_pitched = any(
n.tone is not None
and (hasattr(n.tone, "name") or hasattr(n.tone, "tones"))
for n in part.notes
)
if not has_pitched:
continue
voices.append((name, part.notes))
if not voices:
voices.append(("default", []))
# ── Build XML tree ──────────────────────────────────────────
root = ET.Element("score-partwise")
root.set("version", "4.0")
work = ET.SubElement(root, "work")
work_title = ET.SubElement(work, "work-title")
work_title.text = title
part_list = ET.SubElement(root, "part-list")
ts = self.time_signature
beats_per_measure = ts.beats_per_measure
for idx, (vname, notes) in enumerate(voices, 1):
pid = f"P{idx}"
sp = ET.SubElement(part_list, "score-part")
sp.set("id", pid)
pn = ET.SubElement(sp, "part-name")
pn.text = vname
for idx, (vname, notes) in enumerate(voices, 1):
pid = f"P{idx}"
part_el = ET.SubElement(root, "part")
part_el.set("id", pid)
clef_type = self._guess_clef(notes)
chunks = list(_split_into_measures(notes, beats_per_measure))
beat_in_measure = 0.0
measure_num = 1
measure_el = ET.SubElement(part_el, "measure")
measure_el.set("number", str(measure_num))
attrs = ET.SubElement(measure_el, "attributes")
div_el = ET.SubElement(attrs, "divisions")
div_el.text = str(DIVISIONS)
time_el = ET.SubElement(attrs, "time")
beats_el = ET.SubElement(time_el, "beats")
beats_el.text = str(ts.beats)
bt_el = ET.SubElement(time_el, "beat-type")
bt_el.text = str(ts.unit)
clef_el = ET.SubElement(attrs, "clef")
sign_el = ET.SubElement(clef_el, "sign")
line_el = ET.SubElement(clef_el, "line")
if clef_type == "bass":
sign_el.text = "F"
line_el.text = "4"
else:
sign_el.text = "G"
line_el.text = "2"
direction = ET.SubElement(measure_el, "direction")
dir_type = ET.SubElement(direction, "direction-type")
metronome = ET.SubElement(dir_type, "metronome")
bu = ET.SubElement(metronome, "beat-unit")
bu.text = "quarter"
pm = ET.SubElement(metronome, "per-minute")
pm.text = str(self.bpm)
for (tone, dur_beats, tie_start, tie_stop,
vel, artic) in chunks:
if beat_in_measure >= beats_per_measure - 0.001:
measure_num += 1
measure_el = ET.SubElement(part_el, "measure")
measure_el.set("number", str(measure_num))
beat_in_measure = 0.0
if tone is not None and hasattr(tone, "tones"):
chord_tones = [
t for t in tone.tones
if hasattr(t, "name") and hasattr(t, "octave")
]
if not chord_tones:
_add_note_el(measure_el, None, dur_beats, False,
tie_start, tie_stop, vel)
else:
for ci, ct in enumerate(chord_tones):
_add_note_el(measure_el, ct, dur_beats,
ci > 0, tie_start, tie_stop, vel)
else:
_add_note_el(measure_el, tone, dur_beats, False,
tie_start, tie_stop, vel)
beat_in_measure += dur_beats
# ── Serialize ───────────────────────────────────────────────
raw = ET.tostring(root, encoding="unicode")
doctype = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<!DOCTYPE score-partwise PUBLIC '
'"-//Recordare//DTD MusicXML 4.0 Partwise//EN" '
'"http://www.musicxml.org/dtds/partwise.dtd">\n'
)
pretty = xml.dom.minidom.parseString(raw).toprettyxml(indent=" ")
lines = pretty.split("\n")
if lines and lines[0].startswith("<?xml"):
lines = lines[1:]
return doctype + "\n".join(lines)
# ── ASCII tablature export ──────────────────────────────────────────
def to_tab(self, part_name=None, **kwargs):
"""Generate ASCII tablature for a part in this score.
Args:
part_name: Name of the part to tab. If *None*, tabs the first
non-drum part that has notes.
**kwargs: Passed through to :meth:`Part.to_tab` (e.g.
``tuning``, ``frets``, ``time_signature``).
Returns:
An ASCII tablature string.
Raises:
ValueError: If no suitable part is found.
"""
if "time_signature" not in kwargs:
kwargs["time_signature"] = self.time_signature
if part_name is not None:
if part_name not in self.parts:
raise ValueError(f"No part named {part_name!r}")
return self.parts[part_name].to_tab(**kwargs)
for name, part in self.parts.items():
if part.is_drums:
continue
if not part.notes:
continue
has_pitched = any(
n.tone is not None and not isinstance(n.tone, _DrumTone)
for n in part.notes
)
if has_pitched:
return part.to_tab(**kwargs)
if self.notes:
tmp = Part("_default")
tmp.notes = list(self.notes)
return tmp.to_tab(**kwargs)
raise ValueError("No pitched parts with notes found in score")
def save_midi(self, path, velocity=100):
"""Export to Standard MIDI File, measure-aware."""
ticks_per_beat = 480
us_per_beat = int(60_000_000 / self.bpm)
events = bytearray()
# Tempo meta event
events += _vlq(0)
events += b"\xFF\x51\x03"
events += struct.pack(">I", us_per_beat)[1:]
# Time signature meta event: FF 58 04 nn dd cc bb
ts = self.time_signature
dd = int(math.log2(ts.unit))
events += _vlq(0)
events += b"\xFF\x58\x04"
events += bytes([ts.beats, dd, 24, 8])
accumulated_delta = 0
for note in self.notes:
duration_ticks = int(note.beats * ticks_per_beat)
if note.tone is None:
accumulated_delta += duration_ticks
continue
# Resolve MIDI note numbers
if hasattr(note.tone, "tones"):
# Chord-like object
midi_notes = [
t.midi for t in note.tone.tones if t.midi is not None
]
else:
midi_val = note.tone.midi
midi_notes = [midi_val] if midi_val is not None else []
if not midi_notes:
accumulated_delta += duration_ticks
continue
# Note On events
for i, mn in enumerate(midi_notes):
delta = accumulated_delta if i == 0 else 0
events += _vlq(delta)
events += bytes([0x90, mn & 0x7F, velocity & 0x7F])
accumulated_delta = 0
# Note Off events
for i, mn in enumerate(midi_notes):
delta = duration_ticks if i == 0 else 0
events += _vlq(delta)
events += bytes([0x80, mn & 0x7F, 0])
# ── Drum hits (channel 10 = 0x99/0x89) ────────────────────────
if self._drum_hits:
# Sort by position, render as absolute-time events
sorted_hits = sorted(self._drum_hits, key=lambda h: h.position)
current_tick = 0
for hit in sorted_hits:
hit_tick = int(hit.position * ticks_per_beat)
delta = max(0, hit_tick - current_tick)
events += _vlq(delta)
events += bytes([0x99, hit.sound.value & 0x7F,
hit.velocity & 0x7F])
# Immediate note-off (very short duration for percussion)
events += _vlq(int(0.1 * ticks_per_beat))
events += bytes([0x89, hit.sound.value & 0x7F, 0])
current_tick = hit_tick + int(0.1 * ticks_per_beat)
# End of track (flush any trailing rest delta)
events += _vlq(accumulated_delta)
events += b"\xFF\x2F\x00"
with open(path, "wb") as f:
f.write(b"MThd")
f.write(struct.pack(">I", 6))
f.write(struct.pack(">HHH", 0, 1, ticks_per_beat))
f.write(b"MTrk")
f.write(struct.pack(">I", len(events)))
f.write(events)
# ── MIDI Import ──────────────────────────────────────────────────────
@classmethod
def from_midi(cls, path, synth="sine", envelope="pluck") -> "Score":
"""Import a Standard MIDI File into a Score.
Reads notes, tempo, and time signature from any Type 0 or Type 1
MIDI file. Each MIDI channel becomes a named Part. Channel 10
(drums) becomes drum hits.
Args:
path: Path to a .mid file.
synth: Default synth for all parts (default "sine").
envelope: Default envelope for all parts (default "pluck").
Returns:
A Score with Parts populated from the MIDI data.
Example::
>>> score = Score.from_midi("song.mid")
>>> score.parts["ch1"].synth = "saw"
>>> score.parts["ch1"].reverb_mix = 0.3
"""
midi = _parse_midi(path)
# Compute BPM from tempo (microseconds per beat)
bpm = round(60_000_000 / midi["tempo"])
# Build time signature string
ts_num, ts_den = midi["time_sig"]
ts_str = f"{ts_num}/{ts_den}"
score = cls(time_signature=ts_str, bpm=bpm)
tpb = midi["ticks_per_beat"]
# Build reverse DrumSound lookup: MIDI note number -> DrumSound
_drum_by_note = {}
for ds in DrumSound:
# First one wins (SHAKER and MARACAS both map to 70)
if ds.value not in _drum_by_note:
_drum_by_note[ds.value] = ds
# Collect note events per channel from all tracks
# Each entry: (abs_tick, 'on'/'off', pitch, velocity)
channel_events: dict[int, list] = {}
for track_events in midi["tracks"]:
for ev in track_events:
abs_tick, etype, channel, data = ev
if etype in ("note_on", "note_off"):
if channel not in channel_events:
channel_events[channel] = []
channel_events[channel].append(ev)
for ch in sorted(channel_events.keys()):
events = sorted(channel_events[ch], key=lambda e: e[0])
is_drum = (ch == 9) # channel 10 in 0-indexed
if is_drum:
# Convert to _Hit objects
for ev in events:
abs_tick, etype, channel, data = ev
if etype == "note_on" and data["velocity"] > 0:
pitch = data["pitch"]
beat_pos = abs_tick / tpb
velocity = data["velocity"]
drum_sound = _drum_by_note.get(pitch)
if drum_sound is not None:
score._drum_hits.append(
_Hit(drum_sound, beat_pos, velocity))
else:
# Melodic channel: pair note_on/note_off to get durations
active: dict[int, tuple] = {} # pitch -> (on_tick, velocity)
completed = [] # (beat_pos, pitch, velocity, duration_beats)
for ev in events:
abs_tick, etype, channel_num, data = ev
pitch = data["pitch"]
vel = data["velocity"]
if etype == "note_on" and vel > 0:
active[pitch] = (abs_tick, vel)
else:
# note_off or note_on with vel=0
if pitch in active:
on_tick, on_vel = active.pop(pitch)
dur_ticks = abs_tick - on_tick
if dur_ticks > 0:
beat_pos = on_tick / tpb
dur_beats = dur_ticks / tpb
completed.append(
(beat_pos, pitch, on_vel, dur_beats))
if not completed:
continue
completed.sort(key=lambda x: (x[0], x[1]))
part_name = f"ch{ch + 1}"
part = score.part(part_name, synth=synth, envelope=envelope)
# Walk through notes, inserting rests for gaps
cursor = 0.0 # current beat position
for beat_pos, pitch, velocity, dur_beats in completed:
gap = beat_pos - cursor
if gap > 0.001: # tolerance for floating point
part.notes.append(Rest(_RawDuration(gap)))
from .tones import Tone
tone = Tone.from_midi(pitch)
part.notes.append(
Note(tone=tone, duration=_RawDuration(dur_beats),
velocity=velocity))
cursor = beat_pos + dur_beats
return score
# ── MIDI File Parser ─────────────────────────────────────────────────────
def _read_vlq(data, pos):
"""Read a MIDI variable-length quantity.
Returns:
(value, new_pos) tuple.
"""
value = 0
while True:
byte = data[pos]
value = (value << 7) | (byte & 0x7F)
pos += 1
if not (byte & 0x80):
break
return value, pos
def _parse_midi(path):
"""Parse a Standard MIDI File (Type 0 or Type 1).
Returns a dict with:
- ticks_per_beat: int
- tempo: int (microseconds per beat, default 500000 = 120 bpm)
- time_sig: (numerator, denominator)
- tracks: list of lists of events
Each event is a tuple: (abs_tick, type_str, channel, data_dict)
where type_str is 'note_on' or 'note_off' and data_dict has
'pitch' and 'velocity' keys.
"""
with open(path, "rb") as f:
raw = f.read()
pos = 0
# ── Header chunk ──
if raw[pos:pos + 4] != b"MThd":
raise ValueError("Not a MIDI file (missing MThd header)")
pos += 4
header_len = struct.unpack(">I", raw[pos:pos + 4])[0]
pos += 4
fmt, num_tracks, ticks_per_beat = struct.unpack(">HHH", raw[pos:pos + 6])
pos += header_len # usually 6
if fmt > 1:
raise ValueError(f"MIDI format {fmt} not supported (only 0 and 1)")
tempo = 500000 # default 120 BPM
time_sig = (4, 4) # default
tracks = []
# ── Track chunks ──
for _ in range(num_tracks):
if raw[pos:pos + 4] != b"MTrk":
raise ValueError("Expected MTrk chunk")
pos += 4
track_len = struct.unpack(">I", raw[pos:pos + 4])[0]
pos += 4
track_end = pos + track_len
track_events = []
abs_tick = 0
running_status = 0
while pos < track_end:
# Read delta time
delta, pos = _read_vlq(raw, pos)
abs_tick += delta
# Read event
byte = raw[pos]
if byte == 0xFF:
# Meta event
pos += 1
meta_type = raw[pos]
pos += 1
meta_len, pos = _read_vlq(raw, pos)
meta_data = raw[pos:pos + meta_len]
pos += meta_len
if meta_type == 0x51 and meta_len == 3:
# Tempo: 3 bytes, microseconds per beat
tempo = (meta_data[0] << 16) | (meta_data[1] << 8) | meta_data[2]
elif meta_type == 0x58 and meta_len >= 2:
# Time signature: nn dd cc bb
ts_num = meta_data[0]
ts_den = 2 ** meta_data[1]
time_sig = (ts_num, ts_den)
# End of track (0x2F) and others: just skip
elif byte == 0xF0 or byte == 0xF7:
# SysEx event
pos += 1
sysex_len, pos = _read_vlq(raw, pos)
pos += sysex_len
elif byte & 0x80:
# Channel message with status byte
status = byte
running_status = status
pos += 1
msg_type = status & 0xF0
channel = status & 0x0F
if msg_type == 0x90:
# Note On
pitch = raw[pos]; pos += 1
vel = raw[pos]; pos += 1
if vel == 0:
track_events.append(
(abs_tick, "note_off", channel,
{"pitch": pitch, "velocity": 0}))
else:
track_events.append(
(abs_tick, "note_on", channel,
{"pitch": pitch, "velocity": vel}))
elif msg_type == 0x80:
# Note Off
pitch = raw[pos]; pos += 1
vel = raw[pos]; pos += 1
track_events.append(
(abs_tick, "note_off", channel,
{"pitch": pitch, "velocity": vel}))
elif msg_type in (0xA0, 0xB0, 0xE0):
# Aftertouch, Control Change, Pitch Bend: 2 data bytes
pos += 2
elif msg_type in (0xC0, 0xD0):
# Program Change, Channel Pressure: 1 data byte
pos += 1
else:
# Unknown channel message, skip 2 bytes as safe default
pos += 2
else:
# Running status (no status byte, reuse previous)
if running_status == 0:
# No previous status, skip byte
pos += 1
continue
msg_type = running_status & 0xF0
channel = running_status & 0x0F
if msg_type == 0x90:
pitch = raw[pos]; pos += 1
vel = raw[pos]; pos += 1
if vel == 0:
track_events.append(
(abs_tick, "note_off", channel,
{"pitch": pitch, "velocity": 0}))
else:
track_events.append(
(abs_tick, "note_on", channel,
{"pitch": pitch, "velocity": vel}))
elif msg_type == 0x80:
pitch = raw[pos]; pos += 1
vel = raw[pos]; pos += 1
track_events.append(
(abs_tick, "note_off", channel,
{"pitch": pitch, "velocity": vel}))
elif msg_type in (0xA0, 0xB0, 0xE0):
pos += 2
elif msg_type in (0xC0, 0xD0):
pos += 1
else:
pos += 2
tracks.append(track_events)
return {
"ticks_per_beat": ticks_per_beat,
"tempo": tempo,
"time_sig": time_sig,
"tracks": tracks,
}