mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 06:46:14 +00:00
Drums are now a real Part — same effects pipeline, zero duplication
_drum_hits and _drum_pattern_beats proxy through score.parts['drums']. Drum Part goes through _apply_part_effects like any other Part. set_drum_effects() is now sugar over the Part's attributes. All 789 tests pass with no API changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-7
@@ -1836,6 +1836,8 @@ def render_score(score):
|
||||
# Named parts — each rendered to own buffer for per-part effects
|
||||
_pending_sidechain = []
|
||||
for part in score.parts.values():
|
||||
if part.is_drums:
|
||||
continue # drums are rendered separately via _drum_hits
|
||||
if part.notes:
|
||||
part_buf = numpy.zeros(total_samples, dtype=numpy.float32)
|
||||
synth_fn = _resolve_synth(part.synth)
|
||||
@@ -1989,12 +1991,15 @@ def render_score(score):
|
||||
panned = _pan_to_stereo(mono_hit, pan)
|
||||
drum_stereo[start:start + hit_len] += panned
|
||||
|
||||
# Apply drum bus effects (reverb, delay, etc.) to the stereo drum mix
|
||||
if score.drum_effects:
|
||||
fx = score.drum_effects
|
||||
# Apply to each stereo channel using the same effects engine
|
||||
for ch in range(2):
|
||||
drum_stereo[:, ch] = _apply_effects_with_params(drum_stereo[:, ch], fx)
|
||||
# Apply drum Part effects through the same pipeline as any other Part
|
||||
drums_part = score.parts.get("drums")
|
||||
if drums_part:
|
||||
has_drum_fx = (drums_part.lowpass > 0 or drums_part.delay_mix > 0
|
||||
or drums_part.reverb_mix > 0 or drums_part.distortion_mix > 0
|
||||
or drums_part.chorus_mix > 0)
|
||||
if has_drum_fx:
|
||||
for ch in range(2):
|
||||
drum_stereo[:, ch] = _apply_part_effects(drum_stereo[:, ch], drums_part)
|
||||
|
||||
# Apply sidechain compression to parts that request it
|
||||
for part, part_buf in _pending_sidechain:
|
||||
@@ -2008,7 +2013,7 @@ def render_score(score):
|
||||
if score.notes:
|
||||
stereo_buf += _pan_to_stereo(buf, 0.0)
|
||||
|
||||
# Drums: stereo panned
|
||||
# Drums: stereo panned (with effects already applied)
|
||||
stereo_buf += drum_stereo
|
||||
|
||||
# Master bus compressor/limiter (per channel)
|
||||
|
||||
+66
-27
@@ -1399,6 +1399,8 @@ class Part:
|
||||
self.pan = pan
|
||||
self.spread = spread
|
||||
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) -> "Part":
|
||||
@@ -1690,12 +1692,21 @@ class Part:
|
||||
|
||||
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:
|
||||
return sum(n.beats for n in self.notes)
|
||||
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
|
||||
|
||||
def __len__(self):
|
||||
return len(self.notes)
|
||||
return len(self.notes) + len(self._drum_hits)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.notes)
|
||||
@@ -1784,15 +1795,65 @@ class Score:
|
||||
self.bpm = bpm
|
||||
self.swing = swing
|
||||
self._drum_humanize = drum_humanize
|
||||
self.drum_effects: dict = {}
|
||||
self.notes: list[Note] = []
|
||||
self.parts: dict[str, Part] = {}
|
||||
self._drum_hits: list[_Hit] = []
|
||||
self._drum_pattern_beats: float = 0.0
|
||||
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 the drum bus.
|
||||
|
||||
The drums Part is a real Part — set effects the same way
|
||||
you would on any other part.
|
||||
|
||||
Example::
|
||||
|
||||
score.set_drum_effects(reverb=0.2, reverb_type="plate")
|
||||
"""
|
||||
p = self._ensure_drums_part()
|
||||
param_map = {"reverb": "reverb_mix", "delay": "delay_mix",
|
||||
"distortion": "distortion_mix", "chorus": "chorus_mix"}
|
||||
for k, v in kwargs.items():
|
||||
attr = param_map.get(k, k)
|
||||
setattr(p, attr, v)
|
||||
return self
|
||||
|
||||
def part(self, name: str, *, synth: str = "sine",
|
||||
envelope: str = "piano", volume: float = 0.5,
|
||||
reverb: float = 0.0, reverb_decay: float = 1.0,
|
||||
@@ -1905,28 +1966,6 @@ class Score:
|
||||
fill_pattern = Pattern.fill(name)
|
||||
return self.add_pattern(fill_pattern, repeats=1)
|
||||
|
||||
def set_drum_effects(self, **kwargs) -> "Score":
|
||||
"""Set effects on the drum bus.
|
||||
|
||||
Uses the same parameters as Part effects — reverb, delay,
|
||||
lowpass, distortion, chorus. Applied to the entire drum mix
|
||||
before stereo panning.
|
||||
|
||||
Example::
|
||||
|
||||
score.set_drum_effects(reverb=0.2, reverb_type="plate",
|
||||
lowpass=8000, distortion=0.1)
|
||||
|
||||
Returns:
|
||||
Self for chaining.
|
||||
"""
|
||||
# Map shorthand names
|
||||
param_map = {"reverb": "reverb_mix", "delay": "delay_mix",
|
||||
"distortion": "distortion_mix", "chorus": "chorus_mix"}
|
||||
for k, v in kwargs.items():
|
||||
key = param_map.get(k, k)
|
||||
self.drum_effects[key] = v
|
||||
return self
|
||||
|
||||
def drums(self, preset: str, repeats: int = 4, fill: str = None,
|
||||
fill_every: int = None) -> "Score":
|
||||
|
||||
Reference in New Issue
Block a user