"""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,
},
}
[docs]
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
[docs]
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
[docs]
@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
[docs]
@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"
[docs]
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 ─────────────────────────────────────────────────────────
[docs]
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):
if isinstance(sound, str):
key = sound.strip().replace(" ", "_").replace("-", "_").upper()
try:
sound = DrumSound[key]
except KeyError:
raise ValueError(
f"Unknown drum sound: {sound!r}. "
f"Use a DrumSound member name like 'kick', 'snare', "
f"or 'closed_hat'."
) from None
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})"
[docs]
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>")
[docs]
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] = {}
[docs]
@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)
[docs]
@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] = {}
[docs]
@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)
[docs]
@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),
],
)
[docs]
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})
[docs]
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
[docs]
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
[docs]
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``)
or its name as a string (``"kick"``, ``"snare"``,
``"closed_hat"`` — case-insensitive).
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("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
[docs]
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
[docs]
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
[docs]
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
[docs]
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)
[docs]
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)
[docs]
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
[docs]
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)
[docs]
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
[docs]
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
[docs]
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
[docs]
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
[docs]
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
[docs]
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
[docs]
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
[docs]
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
if not tones:
self.rest(duration)
return self
# Sort by pitch so strum direction is correct regardless of the
# fingering's display orientation: down = low to high, up = high to low.
low_to_high = sorted(tones, key=lambda t: t.midi)
if direction == "down":
strum_tones = low_to_high
else:
strum_tones = list(reversed(low_to_high))
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
[docs]
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"],
}
[docs]
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), a ``Fretboard`` object, 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])
elif hasattr(tuning, "tones"):
# Fretboard object — sort by pitch so we get low-to-high
# regardless of the board's display orientation.
fb_tones = sorted(tuning.tones, key=lambda t: t.midi)
open_midis = [t.midi for t in fb_tones]
labels = [t.name if len(t.name) <= 2 else t.name[0] for t in fb_tones]
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>")
[docs]
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
[docs]
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,
}
[docs]
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
[docs]
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.0–1.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.0–1.0 (default 0, off).
delay_time: Delay time in seconds (default 0.375, dotted 8th).
delay_feedback: Delay feedback 0.0–1.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.0–1.0 (default 0, off).
distortion_drive: Gain before soft clipping (default 3.0).
0.5–2 = subtle warmth, 3–8 = 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.03–0.05 = quick 303 slide,
0.1–0.2 = slow glide.
humanize: Random timing and velocity variation, 0.0–1.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.0–1.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
[docs]
@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())
[docs]
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
[docs]
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},
}
[docs]
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"]
[docs]
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
[docs]
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
[docs]
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
[docs]
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
[docs]
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
[docs]
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 ────────────────────────────────────────────
[docs]
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 ─────────────────────────────────────
[docs]
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 ───────────────────────────────────────────────
[docs]
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 ──────────────────────────────────────────
[docs]
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")
[docs]
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)
# ── Audio Import ─────────────────────────────────────────────────────
[docs]
@classmethod
def from_wav(cls, path, *, bpm=None, quantize=None, split=False,
part_name="melody", synth="piano_synth",
fmin=50.0, fmax=1500.0) -> "Score":
"""Transcribe an audio recording into a Score.
Hum a melody, whistle a hook, record a bass line — load the
recording and get editable notes back. Pitch tracking is the
YIN algorithm; transcription is monophonic per pass (voice,
whistle, a single instrument line), not chords.
Reads WAV directly; .m4a voice memos, .mp3, and anything else
convert automatically through afconvert (macOS) or ffmpeg.
Args:
path: Path to an audio file.
bpm: Tempo to interpret the timing against. Default
``None`` estimates it from the recording's onset
pattern (falling back to 120 for pulse-free rubato).
quantize: Optional grid in beats — ``0.25`` snaps starts
and durations to sixteenths. Default keeps the timing
as performed.
split: If True, separate harmonics from drums first and
transcribe a ``"bass"`` part and a ``"melody"`` part.
For full mixes — the bassline comes out well; the
melody only as well as it dominates the mix.
part_name: Name of the created part (default "melody").
synth: Playback synth for the transcription.
fmin/fmax: Pitch search range in Hz. Tighten for better
results (e.g. ``fmin=60, fmax=350`` for bass).
Returns:
A Score of detected notes, rests, and velocities, with
``score.bpm`` set to the estimated (or given) tempo.
Example::
>>> score = Score.from_wav("hum.m4a", quantize=0.25)
>>> print(score.to_abc(title="My Hum"))
>>> score.save_midi("hum.mid")
"""
from .audio import transcribe
return transcribe(path, bpm=bpm, quantize=quantize, split=split,
part_name=part_name, synth=synth,
fmin=fmin, fmax=fmax)
# ── MIDI Import ──────────────────────────────────────────────────────
[docs]
@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,
}