Source code for pytheory.rhythm

"""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, }