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