From b9dcad045465da1b08ff537bd682a05a137c2afd Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 28 Mar 2026 22:48:37 -0400 Subject: [PATCH] Marching snare, ensemble, flam/diddle/cheese, resonance buildup - 3 marching percussion sounds: snare, rimshot, stick click - 4 marching patterns: march, cadence, paradiddle, roll - Part.flam(), Part.diddle(), Part.cheese() rudiment methods - Part ensemble= parameter: duplicate voices with per-player timing tendencies and micro pitch drift (works on any Part) - Sympathetic resonance: marching snare buzz builds up with repeated hits - Song #32: Snare Cadence (16 bars with triplets, 32nds, flams, cheese) Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/songs.py | 222 ++++++++++++++++++++++++++++++++++++++++++++- pytheory/play.py | 213 +++++++++++++++++++++++++++++++++++++------ pytheory/rhythm.py | 159 +++++++++++++++++++++++++++++++- 3 files changed, 563 insertions(+), 31 deletions(-) diff --git a/examples/songs.py b/examples/songs.py index 2c585bf..821a98e 100644 --- a/examples/songs.py +++ b/examples/songs.py @@ -2440,6 +2440,225 @@ def acid_tabla(): play_song(score, "Acid Tabla — 303 filter automation + tabla (ramp, articulations, Part.hit)") +def snare_cadence(): + """Snare Cadence — marching snare with click count-off, flams, diddles, cheese.""" + score = Score("4/4", bpm=120) + p = score.part("snare", synth="sine", volume=0.8, reverb=0.2) + + S = DrumSound.MARCH_SNARE + R = DrumSound.MARCH_RIMSHOT + C = DrumSound.MARCH_CLICK + + # ── Click count-off ── + for _ in range(4): + p.hit(C, Duration.QUARTER, velocity=95) + + _trip = 1.0 / 3 # triplet 8th + + # ── Section 1: 16th note groove (4 bars) ── + # 16ths are the baseline — accents give it shape + for _ in range(2): + p.hit(R, Duration.SIXTEENTH, velocity=115) + p.hit(S, Duration.SIXTEENTH, velocity=32) + p.hit(S, Duration.SIXTEENTH, velocity=35) + p.hit(S, Duration.SIXTEENTH, velocity=30) + p.hit(R, Duration.SIXTEENTH, velocity=112) + p.hit(S, Duration.SIXTEENTH, velocity=30) + p.hit(S, Duration.SIXTEENTH, velocity=32) + p.hit(S, Duration.SIXTEENTH, velocity=28) + p.hit(R, Duration.SIXTEENTH, velocity=115) + p.hit(S, Duration.SIXTEENTH, velocity=35) + p.hit(S, Duration.SIXTEENTH, velocity=30) + p.hit(S, Duration.SIXTEENTH, velocity=32) + p.hit(R, Duration.SIXTEENTH, velocity=118) + p.hit(S, Duration.SIXTEENTH, velocity=30) + p.hit(S, Duration.SIXTEENTH, velocity=28) + p.hit(S, Duration.SIXTEENTH, velocity=32) + + # 2 bars with triplets mixed in + for _ in range(2): + p.hit(R, _trip, velocity=115) + p.hit(S, _trip, velocity=32) + p.hit(S, _trip, velocity=30) + p.hit(R, _trip, velocity=112) + p.hit(S, _trip, velocity=28) + p.hit(S, _trip, velocity=32) + p.hit(R, Duration.SIXTEENTH, velocity=118) + p.hit(S, Duration.SIXTEENTH, velocity=30) + p.hit(S, Duration.SIXTEENTH, velocity=32) + p.hit(S, Duration.SIXTEENTH, velocity=28) + p.hit(R, _trip, velocity=115) + p.hit(S, _trip, velocity=30) + p.hit(S, _trip, velocity=35) + + # ── Section 2: Add flams + triplets (4 bars) ── + for _ in range(2): + p.flam(S, Duration.QUARTER, velocity=118) + p.hit(S, Duration.SIXTEENTH, velocity=30) + p.hit(S, Duration.SIXTEENTH, velocity=32) + p.hit(R, _trip, velocity=115) + p.hit(S, _trip, velocity=28) + p.hit(S, _trip, velocity=30) + p.flam(S, Duration.QUARTER, velocity=115) + + for _ in range(2): + p.hit(S, _trip, velocity=35) + p.flam(S, _trip * 2, velocity=118) + p.hit(S, Duration.SIXTEENTH, velocity=30) + p.hit(S, Duration.SIXTEENTH, velocity=32) + p.flam(S, Duration.QUARTER, velocity=115) + p.hit(S, _trip, velocity=28) + p.hit(R, _trip, velocity=120) + p.hit(S, _trip, velocity=35) + + # ── Section 3: Flams + diddles + triplets (4 bars) ── + for _ in range(2): + p.flam(S, Duration.QUARTER, velocity=118) + p.diddle(S, Duration.EIGHTH, velocity=45) + p.hit(S, _trip, velocity=30) + p.hit(S, _trip, velocity=32) + p.hit(S, _trip, velocity=28) + p.hit(R, Duration.EIGHTH, velocity=120) + p.diddle(S, Duration.EIGHTH, velocity=42) + + for _ in range(2): + p.diddle(S, Duration.EIGHTH, velocity=45) + p.hit(R, _trip, velocity=118) + p.hit(S, _trip, velocity=30) + p.hit(S, _trip, velocity=32) + p.diddle(S, Duration.EIGHTH, velocity=48) + p.hit(R, _trip, velocity=115) + p.hit(S, _trip, velocity=28) + p.hit(S, _trip, velocity=30) + p.flam(S, Duration.EIGHTH, velocity=120) + p.hit(S, Duration.EIGHTH, velocity=35) + + # ── Section 4: Syncopation + triplet accents (4 bars) ── + for _ in range(2): + p.hit(R, Duration.SIXTEENTH, velocity=118) + p.hit(S, Duration.SIXTEENTH, velocity=30) + p.rest(Duration.SIXTEENTH) + p.hit(S, Duration.SIXTEENTH, velocity=32) + p.flam(S, Duration.EIGHTH, velocity=118) + p.hit(S, _trip, velocity=28) + p.hit(R, _trip, velocity=115) + p.hit(S, _trip, velocity=30) + p.hit(R, Duration.SIXTEENTH, velocity=118) + p.hit(S, Duration.SIXTEENTH, velocity=32) + p.diddle(S, Duration.EIGHTH, velocity=45) + p.hit(R, Duration.EIGHTH, velocity=122) + + # Accent pattern with space + p.hit(R, Duration.EIGHTH, velocity=122) + p.rest(Duration.EIGHTH) + p.hit(R, _trip, velocity=118) + p.hit(R, _trip, velocity=115) + p.hit(R, _trip, velocity=120) + p.rest(Duration.QUARTER) + p.hit(R, Duration.EIGHTH, velocity=122) + p.rest(Duration.EIGHTH) + p.hit(R, Duration.SIXTEENTH, velocity=118) + p.hit(R, Duration.SIXTEENTH, velocity=125) + + p.hit(R, Duration.QUARTER, velocity=125) + p.rest(Duration.QUARTER) + p.hit(R, Duration.QUARTER, velocity=125) + p.rest(Duration.EIGHTH) + p.diddle(S, Duration.EIGHTH, velocity=52) + + # ── Section 5: Cheese + 32nd bursts (4 bars) ── + for _ in range(2): + p.cheese(S, Duration.QUARTER, velocity=118) + p.hit(S, 0.0625, velocity=30) + p.hit(S, 0.0625, velocity=32) + p.hit(S, 0.0625, velocity=35) + p.hit(S, 0.0625, velocity=30) + p.cheese(S, Duration.QUARTER, velocity=115) + p.diddle(S, Duration.EIGHTH, velocity=48) + p.hit(R, Duration.EIGHTH, velocity=122) + + p.cheese(S, Duration.QUARTER, velocity=120) + p.cheese(S, Duration.QUARTER, velocity=118) + p.cheese(S, Duration.QUARTER, velocity=122) + p.cheese(S, Duration.QUARTER, velocity=125) + + p.flam(S, Duration.EIGHTH, velocity=118) + p.diddle(S, Duration.EIGHTH, velocity=50) + p.flam(S, Duration.EIGHTH, velocity=120) + p.diddle(S, Duration.EIGHTH, velocity=52) + p.flam(S, Duration.EIGHTH, velocity=122) + p.diddle(S, Duration.EIGHTH, velocity=55) + p.hit(R, Duration.EIGHTH, velocity=125) + p.hit(S, Duration.EIGHTH, velocity=38) + + # ── Section 6: 16ths with 32nd bursts (4 bars) ── + # 16ths with accents, 32nd doubles sprinkled in + for _ in range(2): + p.hit(R, Duration.SIXTEENTH, velocity=118) + p.hit(S, 0.0625, velocity=32) # 32nd + p.hit(S, 0.0625, velocity=35) # 32nd + p.hit(S, Duration.SIXTEENTH, velocity=30) + p.hit(S, Duration.SIXTEENTH, velocity=38) + p.hit(R, Duration.SIXTEENTH, velocity=115) + p.hit(S, 0.0625, velocity=32) + p.hit(S, 0.0625, velocity=30) + p.hit(S, 0.0625, velocity=35) + p.hit(S, 0.0625, velocity=32) + p.hit(R, Duration.SIXTEENTH, velocity=118) + p.hit(S, Duration.SIXTEENTH, velocity=35) + p.hit(S, 0.0625, velocity=28) + p.hit(S, 0.0625, velocity=32) + p.hit(S, 0.0625, velocity=35) + p.hit(S, 0.0625, velocity=38) + p.hit(R, Duration.SIXTEENTH, velocity=112) + p.hit(S, Duration.SIXTEENTH, velocity=30) + + # Triplet bars — 12 hits per beat, accent every 3 + _trip = 1.0 / 3 # triplet 8th + for _ in range(2): + for beat in range(4): + p.hit(R, _trip, velocity=115) + p.hit(S, _trip, velocity=35) + p.hit(S, _trip, velocity=32) + + # 32nd note run crescendo into rimshot + for i in range(32): + p.hit(S, 0.0625, velocity=min(22 + i * 3, 92)) + p.hit(R, Duration.EIGHTH, velocity=122) + p.hit(R, Duration.EIGHTH, velocity=125) + + # Triplet 16ths — 6 per beat, insane + _trip16 = 1.0 / 6 + for _ in range(2): + for beat in range(4): + p.hit(R, _trip16, velocity=112) + p.hit(S, _trip16, velocity=30) + p.hit(S, _trip16, velocity=32) + p.hit(R, _trip16, velocity=108) + p.hit(S, _trip16, velocity=28) + p.hit(S, _trip16, velocity=30) + + # ── Section 7: Full send (2 bars) ── + # 32nd notes building into the tightest buzz roll + for i in range(64): + p.hit(S, 0.0625, velocity=min(20 + i * 1.5, 100)) + p.hit(R, Duration.EIGHTH, velocity=125) + p.hit(R, Duration.EIGHTH, velocity=127) + + # ── Ending: big unison hits ── + p.hit(R, Duration.EIGHTH, velocity=125) + p.rest(Duration.QUARTER + Duration.EIGHTH) + p.hit(R, Duration.EIGHTH, velocity=125) + p.rest(Duration.QUARTER + Duration.EIGHTH) + # Flam into final CRACK + p.flam(S, Duration.EIGHTH, velocity=127) + p.rest(Duration.QUARTER + Duration.EIGHTH) + p.hit(R, Duration.QUARTER, velocity=127) + p.rest(Duration.HALF) + + play_song(score, "Snare Cadence — marching snare (flams, diddles, cheese, resonance)") + + SONGS = { "1": ("Bossa Nova in A minor", bossa_nova_girl), "2": ("Bebop in Bb major", bebop_in_bb), @@ -2472,6 +2691,7 @@ SONGS = { "29": ("Pop Rock (I-V-vi-IV)", pop_rock), "30": ("Sitar Drone (Bhairav, hold() polyphony)", sitar_drone), "31": ("Acid Tabla (303 + tabla, ramp, articulations)", acid_tabla), + "32": ("Snare Cadence (marching snare, flams, diddles)", snare_cadence), } if __name__ == "__main__": @@ -2485,7 +2705,7 @@ if __name__ == "__main__": print(f" {key:>2}. {name}") print() - choice = input(" Pick a song (1-31, or 'all'): ").strip() + choice = input(" Pick a song (1-32, or 'all'): ").strip() print() if choice == "all": diff --git a/pytheory/play.py b/pytheory/play.py index 39cb751..8e85cf9 100644 --- a/pytheory/play.py +++ b/pytheory/play.py @@ -2796,6 +2796,83 @@ def _synth_metal_hat(n_samples): return out +def _synth_march_snare(n_samples): + """Marching snare — ultra-tight kevlar head, high and crisp. + + Higher pitched than a kit snare. Very short decay — all attack, + no sustain. Tight snare wires give a brief sizzle. + """ + t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE + # Higher-pitched body — tight kevlar pops high + body = numpy.sin(2 * numpy.pi * 450 * t) * _exp_decay(n_samples, 60) * 0.4 + body2 = numpy.sin(2 * numpy.pi * 700 * t) * _exp_decay(n_samples, 75) * 0.2 + # Sharp stick pop + click_len = min(int(SAMPLE_RATE * 0.001), n_samples) + click = _noise(click_len) * _exp_decay(click_len, 400) * 1.2 + # Very tight snare sizzle — higher band, shorter + buzz_len = min(int(SAMPLE_RATE * 0.025), n_samples) + buzz_raw = _noise(buzz_len) + if buzz_len > 20: + bl, al = scipy.signal.butter(2, [3500, 8000], btype='band', fs=SAMPLE_RATE) + buzz = scipy.signal.lfilter(bl, al, numpy.pad(buzz_raw, (0, max(0, n_samples - buzz_len))))[:buzz_len] + else: + buzz = buzz_raw + buzz *= _exp_decay(buzz_len, 50) * 0.35 + result = body + body2 + result[:click_len] += click + result[:buzz_len] += buzz + return numpy.tanh(result * 2.8) + + +def _synth_march_rimshot(n_samples): + """Marching rimshot — woody metallic crack. + + The stick catches the rim — you get the full snare hit plus + a bright, woody-metallic crack from the aluminum rim. Short + ring that dies fast but gives it that cutting edge. + """ + wave = _synth_march_snare(n_samples) + t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE + # Rim crack — bright but short, woody-metallic character + rim = numpy.sin(2 * numpy.pi * 1100 * t) * _exp_decay(n_samples, 45) * 0.35 + rim2 = numpy.sin(2 * numpy.pi * 2200 * t) * _exp_decay(n_samples, 55) * 0.2 + # Hard transient pop + pop_len = min(int(SAMPLE_RATE * 0.002), n_samples) + pop = _noise(pop_len) * _exp_decay(pop_len, 350) * 1.5 + # Extra body punch + punch = numpy.sin(2 * numpy.pi * 500 * t) * _exp_decay(n_samples, 65) * 0.3 + result = wave * 1.4 + rim + rim2 + punch + result[:pop_len] += pop + return numpy.tanh(result * 2.0) + + +def _synth_march_click(n_samples): + """Stick click — taped hickory sticks clocked together. + + Bright wood-on-wood with a slightly dampened attack from the + electrical tape. Not as ringy as a clave — the tape absorbs + some of the high overtones — but still bright and snappy. + """ + t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE + # Wood resonance — brighter than before, but tape dampens ring + body = numpy.sin(2 * numpy.pi * 1100 * t) * _exp_decay(n_samples, 65) * 0.45 + body2 = numpy.sin(2 * numpy.pi * 1800 * t) * _exp_decay(n_samples, 80) * 0.25 + # Woody overtone — gives it that hickory character + body3 = numpy.sin(2 * numpy.pi * 2600 * t) * _exp_decay(n_samples, 95) * 0.12 + # Bright but slightly muffled transient (tape on wood) + click_len = min(int(SAMPLE_RATE * 0.001), n_samples) + click_raw = _noise(click_len) + if click_len > 10: + bl, al = scipy.signal.butter(2, [800, 7000], btype='band', fs=SAMPLE_RATE) + click = scipy.signal.lfilter(bl, al, numpy.pad(click_raw, (0, max(0, n_samples - click_len))))[:click_len] + else: + click = click_raw + click *= _exp_decay(click_len, 350) * 0.9 + result = body + body2 + body3 + result[:click_len] += click + return numpy.tanh(result * 2.8) + + def _synth_tabla_ge_bend(n_samples): """Tabla Ge with upward pitch bend — palm pressing into bayan head. @@ -3012,6 +3089,10 @@ def _render_drum_hit(sound_value, n_samples): DrumSound.METAL_KICK.value: lambda n: _synth_metal_kick(n), DrumSound.METAL_SNARE.value: lambda n: _synth_metal_snare(n), DrumSound.METAL_HAT.value: lambda n: _synth_metal_hat(n), + # Marching + DrumSound.MARCH_SNARE.value: lambda n: _synth_march_snare(n), + DrumSound.MARCH_RIMSHOT.value: lambda n: _synth_march_rimshot(n), + DrumSound.MARCH_CLICK.value: lambda n: _synth_march_click(n), } renderer = _dispatch.get(sound_value, lambda n: _synth_clave(n)) @@ -4496,35 +4577,71 @@ def render_score(score): synth_kwargs["mod_index"] = part.fm_index _temperament = getattr(score, 'temperament', 'equal') _ref_pitch = getattr(score, 'reference_pitch', 440.0) - if part.legato: - _render_legato_to_buf( - part.notes, part_buf, samples_per_beat, total_samples, - synth_fn, env_tuple, part.volume, score.bpm, - glide_time=part.glide, swing=effective_swing, - tempo_map=tempo_map if has_tempo_changes else None, - temperament=_temperament, reference_pitch=_ref_pitch) - else: - _render_notes_to_buf( - part.notes, part_buf, samples_per_beat, total_samples, - synth_fn, env_tuple, part.volume, score.bpm, - swing=effective_swing, - tempo_map=tempo_map if has_tempo_changes else None, - humanize=part.humanize, - detune=part.detune, - spread=part.spread, - stereo_buf=stereo_buf, - sub_osc=part.sub_osc, - noise_mix=part.noise_mix, - filter_attack=part.filter_attack, - filter_decay=part.filter_decay, - filter_sustain=part.filter_sustain, - filter_amount=part.filter_amount, - vel_to_filter=part.vel_to_filter, - filter_q=part.lowpass_q, - synth_kwargs=synth_kwargs, - temperament=_temperament, - reference_pitch=_ref_pitch, - analog=part.analog) + + n_ensemble = max(1, getattr(part, 'ensemble', 1)) + + for _ens_i in range(n_ensemble): + # Each ensemble voice gets its own buffer + ens_buf = part_buf if n_ensemble == 1 else numpy.zeros(total_samples, dtype=numpy.float32) + # Ensemble voices get micro-variations + ens_humanize = part.humanize + ens_analog = part.analog + if n_ensemble > 1: + import random as _ens_rnd + _ens_rnd.seed(42 + _ens_i * 7) + # Hybrid approach: + # 1. Consistent player tendency (rush/drag) — seeded per player + _player_tendency = _ens_rnd.gauss(0, 0.018) + # 2. Tiny per-note wobble on top + ens_humanize = max(part.humanize, 0.012) + # Each player's drum tuned slightly different + ens_analog = max(part.analog, 0.06 + _ens_rnd.uniform(0, 0.08)) + + if part.legato: + _render_legato_to_buf( + part.notes, ens_buf, samples_per_beat, total_samples, + synth_fn, env_tuple, part.volume, score.bpm, + glide_time=part.glide, swing=effective_swing, + tempo_map=tempo_map if has_tempo_changes else None, + temperament=_temperament, reference_pitch=_ref_pitch) + else: + _render_notes_to_buf( + part.notes, ens_buf, samples_per_beat, total_samples, + synth_fn, env_tuple, part.volume, score.bpm, + swing=effective_swing, + tempo_map=tempo_map if has_tempo_changes else None, + humanize=ens_humanize, + detune=part.detune, + spread=part.spread, + stereo_buf=stereo_buf, + sub_osc=part.sub_osc, + noise_mix=part.noise_mix, + filter_attack=part.filter_attack, + filter_decay=part.filter_decay, + filter_sustain=part.filter_sustain, + filter_amount=part.filter_amount, + vel_to_filter=part.vel_to_filter, + filter_q=part.lowpass_q, + synth_kwargs=synth_kwargs, + temperament=_temperament, + reference_pitch=_ref_pitch, + analog=ens_analog) + + if n_ensemble > 1: + # Shift the whole voice by the player's consistent tendency + # (some players rush, some drag — this is fixed per player) + shift_samples = int(_player_tendency * samples_per_beat) + if shift_samples > 0 and shift_samples < total_samples: + # Player drags — shift right + shifted = numpy.zeros_like(ens_buf) + shifted[shift_samples:] = ens_buf[:-shift_samples] + ens_buf = shifted + elif shift_samples < 0 and abs(shift_samples) < total_samples: + # Player rushes — shift left + shifted = numpy.zeros_like(ens_buf) + shifted[:shift_samples] = ens_buf[-shift_samples:] + ens_buf = shifted + part_buf += ens_buf / n_ensemble # Apply effects — segmented if automation exists auto_points = part._get_automation_points() @@ -4657,6 +4774,10 @@ def render_score(score): DrumSound.METAL_KICK.value: 0.0, DrumSound.METAL_SNARE.value: 0.0, DrumSound.METAL_HAT.value: 0.3, + # Marching — centered + DrumSound.MARCH_SNARE.value: 0.0, + DrumSound.MARCH_RIMSHOT.value: 0.0, + DrumSound.MARCH_CLICK.value: 0.0, } # Render all drum Parts (may be one "drums" or split into kick/snare/hats/etc.) @@ -4672,6 +4793,7 @@ def render_score(score): # Track last hit position per sound for choke (new hit dampens # the previous ring on the same drum) _last_hit_start = {} + _resonance = {} # sound_id → resonance level (0.0–1.0) for hit in drum_part._drum_hits: pos = hit.position @@ -4741,6 +4863,39 @@ def render_score(score): vel_jitter = int(drum_humanize * 10) vel = max(1, min(127, vel + _drum_rnd.randint(-vel_jitter, vel_jitter))) vel_scale = vel / 127.0 + + # Sympathetic resonance: marching snare builds up buzz + # as hits accumulate. Each hit adds to a resonance counter + # that scales extra snare wire buzz into the sound. + _RESONANCE_SOUNDS = { + DrumSound.MARCH_SNARE.value, DrumSound.MARCH_RIMSHOT.value, + } + if sound_id in _RESONANCE_SOUNDS: + reso = _resonance.get(sound_id, 0.0) + # Decay based on gap since last hit + if sound_id in _last_hit_start: + gap_samples = start - _last_hit_start[sound_id] + gap_sec = gap_samples / SAMPLE_RATE + if gap_sec > 1.0: + reso *= 0.2 + elif gap_sec > 0.5: + reso *= 0.5 + elif gap_sec > 0.25: + reso *= 0.8 + # Build up (caps at 0.6) + reso = min(0.6, reso + 0.08) + _resonance[sound_id] = reso + # Add sympathetic buzz proportional to resonance + if reso > 0.1: + buzz_len = min(int(SAMPLE_RATE * 0.06), hit_len) + buzz = _noise(buzz_len) * reso * 0.18 + if buzz_len > 20: + bl, al = scipy.signal.butter( + 2, [3000, 9000], btype='band', fs=SAMPLE_RATE) + buzz = scipy.signal.lfilter(bl, al, buzz) + buzz *= _exp_decay(buzz_len, 25) + wave[:buzz_len] = wave[:buzz_len] + buzz.astype(numpy.float32) + mono_hit = wave * vel_scale * 0.7 # Sidechain trigger — kick only if hit.sound.value == DrumSound.KICK.value: diff --git a/pytheory/rhythm.py b/pytheory/rhythm.py index 3f201c3..b69e989 100644 --- a/pytheory/rhythm.py +++ b/pytheory/rhythm.py @@ -564,6 +564,10 @@ class DrumSound(Enum): 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 class _DrumTone: @@ -1602,6 +1606,77 @@ Pattern._PRESETS["tabla solo"] = dict( ], ) +# ── Marching snare patterns ─────────────────────────────────────────────── +MS = DrumSound.MARCH_SNARE +MR = DrumSound.MARCH_RIMSHOT + +# 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), + ], +) + # 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. @@ -2633,6 +2708,7 @@ class Part: 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): self.name = name @@ -2679,6 +2755,7 @@ class Part: 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._system = "western" # default, overridden by Score.part() @@ -2773,6 +2850,85 @@ class Part: 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": @@ -3588,6 +3744,7 @@ class Score: cabinet: float = None, cabinet_brightness: float = None, analog: float = None, + ensemble: int = None, fm_ratio: float = None, fm_index: float = None, fretboard=None) -> Part: @@ -3700,7 +3857,7 @@ class Score: "tremolo_depth": tremolo_depth, "tremolo_rate": tremolo_rate, "phaser": phaser, "phaser_rate": phaser_rate, "cabinet": cabinet, "cabinet_brightness": cabinet_brightness, - "analog": analog, + "analog": analog, "ensemble": ensemble, "fm_ratio": fm_ratio, "fm_index": fm_index, } for k, v in _locals.items():