feat: pattern-based generators from real track analysis, RPP structure fixes, randomization
- Reverse-engineer drum patterns from 2 real reggaeton tracks with librosa - Create patterns.py with extracted frequency data (kick/snare/hihat positions) - Rewrite rhythm.py with pattern-bank generators (dembow, dense, trapico, offbeat) - Rewrite melodic.py with section-aware generators and humanization - Add weighted random sample selection in SampleSelector (top-5 pool) - Add generate_structure() with randomized templates and energy variance - Fix RPP structure: TEMPO arity (3→4 args), string quoting for empty strings - Rewrite quick_drumloop_test.py with correct REAPER ground truth format - Add scripts/analyze_examples.py for reverse engineering audio tracks - Add --seed argument for reproducible generation - 72 tests passing
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
"""Melodic pattern generators for reggaeton production.
|
||||
"""Melodic pattern generators for reggaetón production.
|
||||
|
||||
All patterns follow reggaetón theory:
|
||||
- Bass follows TRESILLO 3-3-2 grouping
|
||||
- Chords follow i-VI-III-VII progression (Am-F-C-G)
|
||||
- Lead syncopation matches dembow (beats 2 and 3&)
|
||||
- Pad follows chord progression with sustained notes
|
||||
|
||||
All generators return list[dict] with format {pos, len, key, vel}.
|
||||
Designed to feed MelodicTrack notes in SongDefinition.
|
||||
"""
|
||||
|
||||
@@ -11,10 +16,10 @@ import random
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SCALES = {
|
||||
"minor": [0, 2, 3, 5, 7, 8, 10], # natural minor
|
||||
"major": [0, 2, 4, 5, 7, 9, 11],
|
||||
"phrygian": [0, 1, 3, 5, 7, 8, 10],
|
||||
"dorian": [0, 2, 3, 5, 7, 9, 10],
|
||||
"minor": [0, 2, 3, 5, 7, 8, 10], # natural minor
|
||||
"major": [0, 2, 4, 5, 7, 9, 11],
|
||||
"phrygian": [0, 1, 3, 5, 7, 8, 10],
|
||||
"dorian": [0, 2, 3, 5, 7, 9, 10],
|
||||
}
|
||||
|
||||
ROOT_SEMITONE = {
|
||||
@@ -24,6 +29,34 @@ ROOT_SEMITONE = {
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chord progressions — i-VI-III-VII is THE reggaetón progression
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Semitone intervals from root for each chord in the progression
|
||||
# classic_minor: Am - F - C - G (i - VI - III - VII in natural minor)
|
||||
_CHORD_INTERVALLS = {
|
||||
# i (root position): 0, 3, 7 (root, minor 3rd, perfect 5th)
|
||||
# VI: F = F(5) + 8 semitones from A (0). F(5) + 8 = A(9) + 8 = 17 → octave = 5. Intervals: (8, 0, 4)
|
||||
# III: C = C(0) + 4 semitones from A. Intervals: (4, 7, 11)
|
||||
# VII: G = G(7) + 10 semitones from A (raised 7th in harmonic minor). Intervals: (10, 1, 5)
|
||||
"classic_minor": {
|
||||
"intervals": [(0, 3, 7), (8, 0, 4), (4, 7, 11), (10, 1, 5)],
|
||||
"root_offsets": [0, 8, 4, 10], # root semitone offsets for each chord
|
||||
},
|
||||
# tension: Am - F - C - G but with different voicing feel
|
||||
"tension": {
|
||||
"intervals": [(0, 3, 7), (8, 0, 4), (4, 7, 11), (10, 1, 5)],
|
||||
"root_offsets": [0, 8, 4, 10],
|
||||
},
|
||||
# romantic: Am - G - F - E (i - VII - VI - V in natural minor)
|
||||
"romantic": {
|
||||
"intervals": [(0, 3, 7), (10, 1, 5), (8, 0, 4), (4, 9, 0)],
|
||||
"root_offsets": [0, 10, 8, 4],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -36,7 +69,6 @@ def _parse_key(key_str: str) -> tuple[int, str]:
|
||||
else:
|
||||
root_str = key_str
|
||||
scale_name = "major"
|
||||
|
||||
root = ROOT_SEMITONE.get(root_str)
|
||||
if root is None:
|
||||
raise ValueError(f"Unknown root: {root_str}")
|
||||
@@ -54,7 +86,7 @@ def _clamp_vel(v: int) -> int:
|
||||
return max(1, min(127, v))
|
||||
|
||||
|
||||
def _apply_humanize(notes, humanize):
|
||||
def _apply_humanize(notes: list[dict], humanize: float) -> list[dict]:
|
||||
"""Apply humanization (velocity jitter + position nudge) to note list."""
|
||||
if humanize <= 0:
|
||||
return notes
|
||||
@@ -67,7 +99,7 @@ def _apply_humanize(notes, humanize):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bass: tresillo
|
||||
# Bass: tresillo 3-3-2 grouping — NOW section-aware
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def bass_tresillo(
|
||||
@@ -76,42 +108,173 @@ def bass_tresillo(
|
||||
octave: int = 3,
|
||||
velocity_mult: float = 1.0,
|
||||
humanize: float = 0.0,
|
||||
section_type: str = "verse",
|
||||
) -> list[dict]:
|
||||
"""Reggaeton tresillo bass pattern.
|
||||
"""Reggaetón tresillo bass pattern — section-aware version.
|
||||
|
||||
6 notes per bar at positions: 0.0, 0.75, 1.5, 2.25, 3.0, 3.75
|
||||
Root note on downbeats (0.0, 1.5, 3.0), fifth (7 semitones) on upbeats.
|
||||
Velocity: 110 for downbeats, 85 for upbeats.
|
||||
Default octave=3 gives root in MIDI range 45-52 (A3-E4), within 36-55.
|
||||
The 3-3-2 grouping is the BASS DNA of reggaetón:
|
||||
- 3 notes in first group (positions 0.0, 0.75, 1.5)
|
||||
- 3 notes in second group (positions 2.25, 3.0, 3.75)
|
||||
- 2 notes would complete the next bar...
|
||||
|
||||
But for 6/8 feel within 4/4, we do 6 notes per bar:
|
||||
Positions: [0.0, 0.75, 1.5, 2.25, 3.0, 3.75]
|
||||
↑ ↑ ↑ ↑ ↑ ↑
|
||||
3 3 2 (groups)
|
||||
|
||||
Root note on downbeats (0.0, 1.5, 3.0)
|
||||
Fifth on upbeats (0.75, 2.25, 3.75)
|
||||
Velocity: Root=110, Fifth=90
|
||||
|
||||
Default octave=3 gives root in MIDI range 45-52 (A3-E4), good for 808.
|
||||
|
||||
Section-aware behavior:
|
||||
- chorus/drop: full velocity, root on beat 1
|
||||
- verse/bridge: slightly softer (0.85x), stripped
|
||||
- break/gap: minimal (0.3x), few notes
|
||||
- intro: building (0.5x then ramp)
|
||||
"""
|
||||
root, scale = _parse_key(key)
|
||||
scale_notes = _get_scale_notes(root, scale, octave)
|
||||
root_note = scale_notes[0] # degree 0
|
||||
fifth_note = root_note + 7 # up a perfect fifth
|
||||
root_note = root + (octave - 1) * 12 # octave 3 → MIDI 45 for A
|
||||
fifth_note = root_note + 7 # perfect fifth above root
|
||||
|
||||
# Section-aware velocity and density adjustments
|
||||
section = section_type.lower()
|
||||
if section in ("chorus", "drop"):
|
||||
vel_root = 115
|
||||
vel_fifth = 95
|
||||
density_mult = 1.0
|
||||
elif section in ("verse", "bridge"):
|
||||
vel_root = 95
|
||||
vel_fifth = 75
|
||||
density_mult = 1.0
|
||||
elif section in ("break", "gap"):
|
||||
vel_root = 40
|
||||
vel_fifth = 30
|
||||
density_mult = 0.3 # sparse
|
||||
elif section == "intro":
|
||||
vel_root = 70
|
||||
vel_fifth = 55
|
||||
density_mult = 0.6 # building up
|
||||
else:
|
||||
vel_root = 100
|
||||
vel_fifth = 80
|
||||
density_mult = 0.9
|
||||
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
# Positions within the bar
|
||||
# 6 positions per bar for the 3-3-2 tresillo feel
|
||||
positions = [0.0, 0.75, 1.5, 2.25, 3.0, 3.75]
|
||||
for idx, pos in enumerate(positions):
|
||||
if idx % 2 == 0: # downbeats: root
|
||||
if idx % 2 == 0: # downbeats: root (positions 0, 2, 4)
|
||||
key_note = root_note
|
||||
vel = 110
|
||||
else: # upbeats: fifth
|
||||
vel = vel_root
|
||||
else: # upbeats: fifth (positions 1, 3, 5)
|
||||
key_note = fifth_note
|
||||
vel = 85
|
||||
vel = vel_fifth
|
||||
|
||||
# Apply density — skip some notes when density_mult < 1
|
||||
if random.random() > density_mult:
|
||||
continue
|
||||
|
||||
vel = _clamp_vel(int(vel * velocity_mult))
|
||||
notes.append({"pos": o + pos, "len": 0.25, "key": key_note, "vel": vel})
|
||||
notes.append({"pos": o + pos, "len": 0.5, "key": key_note, "vel": vel})
|
||||
|
||||
return _apply_humanize(notes, humanize)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lead: hook
|
||||
# Chords: block chords following i-VI-III-VII — section-aware
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def chords_block(
|
||||
key: str,
|
||||
bars: int,
|
||||
octave: int = 4,
|
||||
beats_per_chord: int = 4,
|
||||
velocity_mult: float = 1.0,
|
||||
humanize: float = 0.0,
|
||||
section_type: str = "verse",
|
||||
) -> list[dict]:
|
||||
"""Block chords following the i-VI-III-VII progression — section-aware.
|
||||
|
||||
Default progression: Am - F - C - G (classic_minor)
|
||||
Chord positions: every beats_per_chord beats
|
||||
Each chord: root + third + fifth (3 notes stacked at same position)
|
||||
|
||||
Section-aware behavior:
|
||||
- chorus/drop: full velocity, tight voicings
|
||||
- verse/bridge: softer (0.7x), wider voicings
|
||||
- break/gap: near-silence
|
||||
- intro: soft, building
|
||||
"""
|
||||
root, scale = _parse_key(key)
|
||||
|
||||
# Use classic_minor progression for minor keys
|
||||
progression_key = "classic_minor"
|
||||
if scale != "minor":
|
||||
progression_key = "major"
|
||||
|
||||
prog_data = _CHORD_INTERVALLS.get(progression_key, _CHORD_INTERVALLS["classic_minor"])
|
||||
chord_intervals = prog_data["intervals"]
|
||||
|
||||
# Section-aware velocity adjustments
|
||||
section = section_type.lower()
|
||||
if section in ("chorus", "drop"):
|
||||
base_vel = 90
|
||||
voicing_spread = 0.0 # tight
|
||||
elif section in ("verse", "bridge"):
|
||||
base_vel = 65
|
||||
voicing_spread = 0.3 # wider, more open
|
||||
elif section in ("break", "gap"):
|
||||
base_vel = 30
|
||||
voicing_spread = 0.5
|
||||
elif section == "intro":
|
||||
base_vel = 55
|
||||
voicing_spread = 0.2
|
||||
else:
|
||||
base_vel = 75
|
||||
voicing_spread = 0.1
|
||||
|
||||
notes: list[dict] = []
|
||||
total_beats = bars * 4.0
|
||||
pos = 0.0
|
||||
|
||||
while pos < total_beats:
|
||||
chord_idx = int(pos / beats_per_chord) % len(chord_intervals)
|
||||
intervals = chord_intervals[chord_idx]
|
||||
|
||||
# Add root, 3rd, 5th for this chord
|
||||
for interval in intervals:
|
||||
midi_note = root + octave * 12 + interval
|
||||
# Spread 3rd and 5th slightly for wider voicings
|
||||
if interval == intervals[1]: # third
|
||||
midi_note += round(voicing_spread * 12)
|
||||
elif interval == intervals[2]: # fifth
|
||||
midi_note += round(voicing_spread * 6)
|
||||
|
||||
vel = _clamp_vel(int(base_vel * velocity_mult))
|
||||
notes.append({
|
||||
"pos": pos,
|
||||
"len": beats_per_chord - 0.25,
|
||||
"key": midi_note,
|
||||
"vel": vel,
|
||||
})
|
||||
|
||||
pos += beats_per_chord
|
||||
|
||||
return _apply_humanize(notes, humanize)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lead: hook with dembow syncopation — now section-aware
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Scale degrees for reggaetón hook melody (pentatonic-based for catchiness)
|
||||
_HOOK_DEGREES = [0, 2, 4, 2, 3, 1, 0, 2, 4, 5, 4, 2, 0] # 13-note motif
|
||||
|
||||
|
||||
def lead_hook(
|
||||
key: str,
|
||||
bars: int,
|
||||
@@ -119,54 +282,89 @@ def lead_hook(
|
||||
density: float = 0.6,
|
||||
velocity_mult: float = 1.0,
|
||||
humanize: float = 0.0,
|
||||
section_type: str = "verse",
|
||||
) -> list[dict]:
|
||||
"""Simple melodic hook over 4-8 bars.
|
||||
"""Lead hook with dembow syncopation — section-aware version.
|
||||
|
||||
Uses scalar degrees: [0, 2, 4, 2, 3, 1, 0, 2, 4, 5, 4, 2, 0]
|
||||
Note durations: 0.5 or 1.0 beats.
|
||||
density=1.0 → every slot filled; density=0.5 → half filled.
|
||||
The hook emphasizes the DEMBOW syncopation — notes on beats 2, 3&, 4
|
||||
(positions 4.0, 11.0, 12.0 from the real-track analysis).
|
||||
|
||||
Scale: Pentatonic or minor for reggaetón feel
|
||||
Density: 0.6 means some slots are empty for groove
|
||||
|
||||
Section-aware behavior:
|
||||
- chorus/drop: higher density, longer notes on dembow positions
|
||||
- verse/bridge: sparser, softer
|
||||
- break/gap: near-silence (sparse, very soft)
|
||||
- intro: building up from sparse
|
||||
"""
|
||||
root, scale = _parse_key(key)
|
||||
intervals = SCALES.get(scale, SCALES["major"])
|
||||
|
||||
# Map scale degrees to MIDI notes (extend to cover octave 5 and 6 for melody)
|
||||
scale_notes_oct5 = _get_scale_notes(root, scale, octave) # 7 notes
|
||||
# Build scale notes for octave 5 and 6
|
||||
scale_notes_oct5 = _get_scale_notes(root, scale, octave) # 7 notes
|
||||
scale_notes_oct6 = _get_scale_notes(root, scale, octave + 1)
|
||||
|
||||
# Degree pattern (0-indexed scale degrees)
|
||||
degrees = [0, 2, 4, 2, 3, 1, 0, 2, 4, 5, 4, 2, 0]
|
||||
# Section-aware adjustments
|
||||
section = section_type.lower()
|
||||
if section in ("chorus", "drop"):
|
||||
base_density = 0.8
|
||||
base_vel = 110
|
||||
length_factor = 1.0
|
||||
elif section in ("verse", "bridge"):
|
||||
base_density = 0.5
|
||||
base_vel = 85
|
||||
length_factor = 0.8
|
||||
elif section in ("break", "gap"):
|
||||
base_density = 0.15
|
||||
base_vel = 40
|
||||
length_factor = 0.5
|
||||
elif section == "intro":
|
||||
base_density = 0.4
|
||||
base_vel = 70
|
||||
length_factor = 0.7
|
||||
else:
|
||||
base_density = 0.6
|
||||
base_vel = 95
|
||||
length_factor = 0.9
|
||||
|
||||
# Combine function param density with section density
|
||||
effective_density = density * base_density
|
||||
|
||||
notes: list[dict] = []
|
||||
|
||||
# Step through the pattern at half-beat intervals
|
||||
# density controls whether we actually place a note
|
||||
step = max(1, round(1.0 / density)) if density > 0 else 1
|
||||
# Dembow accent positions from real track analysis: 4, 8, 11, 12
|
||||
# These are beats 2, 3, 3&, 4 in 16th-note positions
|
||||
dembow_positions = [4.0, 8.0, 11.0, 12.0]
|
||||
|
||||
pos = 0.0
|
||||
degree_idx = 0
|
||||
step = max(1, round(1.0 / effective_density)) if effective_density > 0 else 1
|
||||
|
||||
slot = 0
|
||||
while pos < bars * 4.0:
|
||||
slot = int(pos * 2) # 0.5-beat slots
|
||||
if slot % step == 0:
|
||||
# Pick note alternating between octave 5 and 6 for contour
|
||||
use_oct6 = (degree_idx // 2) % 3 == 0 # every few notes go higher
|
||||
midi_note = scale_notes_oct6[degrees[degree_idx] % 7] \
|
||||
if use_oct6 else scale_notes_oct5[degrees[degree_idx] % 7]
|
||||
slot_half = int(pos * 2) # 0.5-beat slots
|
||||
|
||||
# Duration: 1.0 beat on strong beats (quarter), 0.5 elsewhere
|
||||
is_strong = (slot % 4 == 0)
|
||||
length = 1.0 if is_strong else 0.5
|
||||
if slot_half % step == 0:
|
||||
# Check if this position is a dembow accent position
|
||||
is_dembow = any(abs(pos - dp) < 0.01 for dp in dembow_positions)
|
||||
|
||||
vel = 100 if is_strong else 80
|
||||
# Alternate between octave 5 and 6 for contour
|
||||
use_oct6 = (degree_idx // 2) % 3 == 0
|
||||
scale_notes = scale_notes_oct6 if use_oct6 else scale_notes_oct5
|
||||
midi_note = scale_notes[_HOOK_DEGREES[degree_idx % len(_HOOK_DEGREES)] % 7]
|
||||
|
||||
# Duration: longer on dembow positions for emphasis
|
||||
length = 1.0 * length_factor if is_dembow else 0.5 * length_factor
|
||||
vel = base_vel if is_dembow else int(base_vel * 0.75)
|
||||
vel = _clamp_vel(int(vel * velocity_mult))
|
||||
|
||||
notes.append({"pos": pos, "len": length, "key": midi_note, "vel": vel})
|
||||
|
||||
# Advance degree index
|
||||
degree_idx = (degree_idx + 1) % len(degrees)
|
||||
if is_strong:
|
||||
pos += 1.0
|
||||
else:
|
||||
pos += 0.5
|
||||
degree_idx = (degree_idx + 1) % len(_HOOK_DEGREES)
|
||||
|
||||
# Advance by length
|
||||
pos += 0.5 # fixed 8th-note step
|
||||
else:
|
||||
pos += 0.5
|
||||
|
||||
@@ -174,85 +372,7 @@ def lead_hook(
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chords: block chords
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def chords_block(
|
||||
key: str,
|
||||
bars: int,
|
||||
octave: int = 4,
|
||||
velocity_mult: float = 1.0,
|
||||
humanize: float = 0.0,
|
||||
) -> list[dict]:
|
||||
"""Blocked chords every 2 beats (half-bar).
|
||||
|
||||
Minor progression: i - VII - VI - VII (degrees 0, 6, 5, 6 in natural minor)
|
||||
Major progression: I - V - vi - IV (degrees 0, 4, 5, 3 in major)
|
||||
Each chord: root + third + fifth (3 notes stacked at same position).
|
||||
"""
|
||||
root, scale = _parse_key(key)
|
||||
scale_notes_oct4 = _get_scale_notes(root, scale, octave)
|
||||
|
||||
if scale == "minor":
|
||||
# i - VII - VI - VII (natural minor)
|
||||
# VII = degree 6 (raised 7th = 10 semitones from root in minor)
|
||||
# In natural minor: degrees 0,6,5,6
|
||||
# We need to build chords: root, 3rd, 5th
|
||||
chord_degrees = [
|
||||
[0, 2, 4], # i — degrees 0, 2, 4 in minor
|
||||
[6, 1, 3], # VII — degree 6 wraps to next octave; 1=2nd, 3=4th
|
||||
[5, 0, 2], # VI — degree 5 wraps; 0=root of next octave
|
||||
[6, 1, 3], # VII (repeat)
|
||||
]
|
||||
# For proper stacking, use only the first 7 scale degrees
|
||||
# Chord VII in minor: root is degree 6 (10 semitones above)
|
||||
# Build using absolute semitones: i = root+0,root+3,root+7
|
||||
# VII = root+10, root+12 (=0 of next), root+15 (=3 of next)
|
||||
pass # We'll rebuild below
|
||||
|
||||
# Simpler approach: build chords using semitone intervals from root
|
||||
if scale == "minor":
|
||||
# i (0,3,7), VIIb (10,1,5), VI (8,11,2), VII (10,1,5)
|
||||
chord_intervals = [
|
||||
(0, 3, 7), # i
|
||||
(10, 1, 5), # VII (raised 7th in harmonic minor: 10 semitones)
|
||||
(8, 0, 4), # VI
|
||||
(10, 1, 5), # VII
|
||||
]
|
||||
else:
|
||||
# I (0,4,7), V (7,11,2), vi (9,0,4), IV (5,9,0)
|
||||
chord_intervals = [
|
||||
(0, 4, 7), # I
|
||||
(7, 11, 2), # V
|
||||
(9, 0, 4), # vi (9 = root+9)
|
||||
(5, 9, 0), # IV (5 = root+5)
|
||||
]
|
||||
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
chord_idx = b % 4
|
||||
intervals = chord_intervals[chord_idx]
|
||||
|
||||
# Chord positions at half-bar: 0.0 and 2.0
|
||||
chord_positions = [0.0, 2.0]
|
||||
for cpos in chord_positions:
|
||||
for interval in intervals:
|
||||
midi_note = root + octave * 12 + interval
|
||||
vel = 90
|
||||
vel = _clamp_vel(int(vel * velocity_mult))
|
||||
notes.append({
|
||||
"pos": o + cpos,
|
||||
"len": 1.75, # almost 2 beats (leave gap)
|
||||
"key": midi_note,
|
||||
"vel": vel,
|
||||
})
|
||||
|
||||
return _apply_humanize(notes, humanize)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pad: sustain
|
||||
# Pad: sustain following chord progression
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def pad_sustain(
|
||||
@@ -261,46 +381,53 @@ def pad_sustain(
|
||||
octave: int = 4,
|
||||
velocity_mult: float = 1.0,
|
||||
humanize: float = 0.0,
|
||||
section_type: str = "verse",
|
||||
) -> list[dict]:
|
||||
"""Long sustained pad notes, one per bar.
|
||||
"""Sustained pad notes following the i-VI-III-VII chord progression.
|
||||
|
||||
One note per bar (root of each chord)
|
||||
Duration: 3.5 beats (leaves gap for next bar)
|
||||
Velocity: 65-75 (soft, atmospheric)
|
||||
Follows chord progression from chords_block.
|
||||
Notes last 3.5 beats to avoid collision with next bar's note.
|
||||
Soft velocity (65-75).
|
||||
|
||||
Section-aware behavior:
|
||||
- chorus/drop: full velocity (75)
|
||||
- verse/bridge: softer (55)
|
||||
- break/gap: near-silence (25)
|
||||
- intro: building (45)
|
||||
"""
|
||||
root, scale = _parse_key(key)
|
||||
|
||||
if scale == "minor":
|
||||
chord_intervals = [
|
||||
(0, 3, 7),
|
||||
(10, 1, 5),
|
||||
(8, 0, 4),
|
||||
(10, 1, 5),
|
||||
]
|
||||
root_notes_per_bar = [0, 10, 8, 10] # root semitone offsets per bar
|
||||
# Section-aware velocity
|
||||
section = section_type.lower()
|
||||
if section in ("chorus", "drop"):
|
||||
base_vel = 75
|
||||
elif section in ("verse", "bridge"):
|
||||
base_vel = 55
|
||||
elif section in ("break", "gap"):
|
||||
base_vel = 25
|
||||
elif section == "intro":
|
||||
base_vel = 45
|
||||
else:
|
||||
chord_intervals = [
|
||||
(0, 4, 7),
|
||||
(7, 11, 2),
|
||||
(9, 0, 4),
|
||||
(5, 9, 0),
|
||||
]
|
||||
root_notes_per_bar = [0, 7, 9, 5]
|
||||
base_vel = 65
|
||||
|
||||
# Use classic_minor progression
|
||||
prog_data = _CHORD_INTERVALLS.get("classic_minor", _CHORD_INTERVALLS["classic_minor"])
|
||||
root_offsets = prog_data["root_offsets"] # [0, 8, 4, 10]
|
||||
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
cycle = b % 4
|
||||
root_interval = root_notes_per_bar[cycle]
|
||||
chord_idx = b % 4
|
||||
root_interval = root_offsets[chord_idx]
|
||||
midi_note = root + octave * 12 + root_interval
|
||||
|
||||
vel = 70
|
||||
vel = _clamp_vel(int(vel * velocity_mult))
|
||||
vel = _clamp_vel(int(base_vel * velocity_mult))
|
||||
notes.append({
|
||||
"pos": o,
|
||||
"len": 3.5,
|
||||
"len": 3.5, # sustained for most of the bar
|
||||
"key": midi_note,
|
||||
"vel": vel,
|
||||
})
|
||||
|
||||
return notes
|
||||
return _apply_humanize(notes, humanize)
|
||||
425
src/composer/patterns.py
Normal file
425
src/composer/patterns.py
Normal file
@@ -0,0 +1,425 @@
|
||||
"""Pattern Knowledge Base — extracted from real reggaetón track analysis.
|
||||
|
||||
Data sourced from:
|
||||
- Ejemplo 1: 99.4 BPM, 4:07 min, 99 bars
|
||||
- Ejemplo 2: 132.5 BPM, 2:30 min, 61 bars
|
||||
|
||||
These patterns are NOT textbook dembow — they are DENSE DRUM LOOPS with
|
||||
multiple hits per bar. The " textbook 1, 3, 4&" is a simplification.
|
||||
Real tracks have kicks on almost EVERY 16th note position at some point.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Weight tables — extracted from 16th-note position frequency counts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Ejemplo 1 kick frequency (99.4 BPM, 16 bars observed)
|
||||
# Position: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||
# Count: 9 6 2 10 17 10 7 8 11 12 4 14 12 9 11 8
|
||||
#
|
||||
# Key insight: position 4 (beat 2) is the densest hit — NOT beat 1.
|
||||
# The dembow IS there: positions 0, 4, 8, 11, 12 are consistently high.
|
||||
# But the PATTERN IS NOT sparse kick-snare — it's a full drum loop.
|
||||
|
||||
# Normalized weights (0.0-1.0) from frequency counts
|
||||
KICK_WEIGHTS_99: list[float] = [
|
||||
9/17, 6/17, 2/17, 10/17, 17/17, 10/17, 7/17, 8/17,
|
||||
11/17, 12/17, 4/17, 14/17, 12/17, 9/17, 11/17, 8/17,
|
||||
]
|
||||
# Most frequent positions: 4, 11, 8, 12, 9
|
||||
# These are beats: 2, 3&, 3, 4, 2.5& — NOT the textbook 1, 3, 4&
|
||||
|
||||
# Ejemplo 2 kick frequency (132.5 BPM, 61 bars)
|
||||
# Position: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||
# Count: 7 6 3 5 8 9 9 2 3 9 6 11 5 2 6 8
|
||||
KICK_WEIGHTS_132: list[float] = [
|
||||
7/11, 6/11, 3/11, 5/11, 8/11, 9/11, 9/11, 2/11,
|
||||
3/11, 9/11, 6/11, 11/11, 5/11, 2/11, 6/11, 8/11,
|
||||
]
|
||||
# Most frequent: position 11 (beat 3&), 5 (beat 1.5&), 6 (beat 2), 9 (beat 2.5&)
|
||||
|
||||
# Snare weights from Ejemplo 1
|
||||
# Position: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||
# Count: 8 6 8 13 16 5 11 5 11 5 7 13 14 6 10 7
|
||||
SNARE_WEIGHTS_99: list[float] = [
|
||||
8/16, 6/16, 8/16, 13/16, 16/16, 5/16, 11/16, 5/16,
|
||||
11/16, 5/16, 7/16, 13/16, 14/16, 6/16, 10/16, 7/16,
|
||||
]
|
||||
# Most frequent: position 4 (beat 2), 12 (beat 4), 3 (beat 1&), 11 (beat 3&)
|
||||
|
||||
# Hihat weights from Ejemplo 1
|
||||
# Position: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||
# Count: 12 7 14 11 17 4 12 6 11 6 12 15 14 5 13 9
|
||||
HIHAT_WEIGHTS_99: list[float] = [
|
||||
12/17, 7/17, 14/17, 11/17, 17/17, 4/17, 12/17, 6/17,
|
||||
11/17, 6/17, 12/17, 15/17, 14/17, 5/17, 13/17, 9/17,
|
||||
]
|
||||
# Most frequent: position 4 (beat 2), 11 (beat 3&), 2 (beat 1&), 12 (beat 4)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Section energy levels — extracted from amplitude analysis
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# From Ejemplo 1 amplitude data:
|
||||
# - BUILD/PRE-CHORUS: -10.6 dB to -6.6 dB → energy 0.5-0.7
|
||||
# - VERSE/BRIDGE: -13.2 dB to -14.1 dB → energy 0.3-0.4
|
||||
# - CHORUS/DROP: -2.0 dB to -4.4 dB → energy 0.9-1.0
|
||||
# - GAP/BREAK (bar 75-76): -49.4 dB to -53.5 dB → energy 0.0-0.05
|
||||
# - INTRO/FILTER: -27.8 dB → filtered, building up
|
||||
|
||||
SECTION_ENERGY: dict[str, float] = {
|
||||
"intro": 0.3, # filtered, building
|
||||
"verse": 0.35, # low energy, stripped back
|
||||
"build": 0.6, # medium energy, all elements
|
||||
"pre_chorus": 0.65, # slightly higher than verse
|
||||
"chorus": 0.95, # maximum energy
|
||||
"drop": 1.0, # peak loudness
|
||||
"break": 0.05, # near silence (gap technique)
|
||||
"bridge": 0.4, # transition, moderate
|
||||
"outro": 0.35, # wind down
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Section template — real structure from Ejemplo 1
|
||||
# ---------------------------------------------------------------------------
|
||||
# 16 bars BUILD → 2 bars VERSE → 47 bars BUILD → 1 bar VERSE →
|
||||
# 1 bar GAP (-27.8dB) → 1 bar DROP (-2.6dB) → 1 bar DROP (-2.0dB) →
|
||||
# 5 bars BUILD → 1 bar BREAK (-49.4dB) → 1 bar BREAK (-53.5dB) →
|
||||
# 1 bar DROP (-4.4dB) → 12 bars BUILD → 2 bars VERSE → 8 bars BUILD
|
||||
#
|
||||
# Key insight: The GAP/BREAK followed by LOUD DROP is a signature technique.
|
||||
# 1-2 bars of near-silence creates huge contrast for the drop.
|
||||
|
||||
SECTION_TEMPLATE: list[dict] = [
|
||||
{"name": "build", "bars": 16, "energy": 0.6},
|
||||
{"name": "verse", "bars": 2, "energy": 0.35},
|
||||
{"name": "build", "bars": 47, "energy": 0.65},
|
||||
{"name": "verse", "bars": 1, "energy": 0.35},
|
||||
{"name": "gap", "bars": 1, "energy": 0.05},
|
||||
{"name": "drop", "bars": 2, "energy": 1.0},
|
||||
{"name": "build", "bars": 5, "energy": 0.7},
|
||||
{"name": "break", "bars": 2, "energy": 0.05},
|
||||
{"name": "drop", "bars": 1, "energy": 0.95},
|
||||
{"name": "build", "bars": 12, "energy": 0.65},
|
||||
{"name": "verse", "bars": 2, "energy": 0.35},
|
||||
{"name": "build", "bars": 8, "energy": 0.6},
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pattern bank entries — per-style variants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class PatternEntry:
|
||||
"""A single note entry in a pattern."""
|
||||
pos: float # 16th-note position (0.0-3.75 for one bar)
|
||||
vel: int # base velocity
|
||||
len: float = 0.25 # note length in beats
|
||||
|
||||
|
||||
# Classic dembow — the canonical reggaetón pattern
|
||||
# NOT the textbook 1, 3, 4& — this is a DENSE drum loop pattern
|
||||
# Extracted from frequency analysis: kicks at 0, 3, 4, 5, 7, 8, 9, 11, 12
|
||||
DEMBOW_CLASSICO: list[PatternEntry] = [
|
||||
# Beat 1 area (positions 0, 1, 2, 3)
|
||||
PatternEntry(pos=0.0, vel=110),
|
||||
PatternEntry(pos=3.0, vel=95), # beat 3
|
||||
PatternEntry(pos=4.0, vel=100), # beat 2 — THE densest position
|
||||
PatternEntry(pos=5.0, vel=85),
|
||||
PatternEntry(pos=7.0, vel=90),
|
||||
# Beat 3 area (positions 8, 9, 10, 11)
|
||||
PatternEntry(pos=8.0, vel=100), # beat 3
|
||||
PatternEntry(pos=9.0, vel=90),
|
||||
PatternEntry(pos=11.0, vel=105), # beat 3& — THE signature dembow
|
||||
PatternEntry(pos=12.0, vel=95), # beat 4
|
||||
]
|
||||
|
||||
|
||||
# Perreo style — more aggressive, denser kicks at positions 5, 6 (beat 1.5&, 2)
|
||||
PERREO_STYLE: list[PatternEntry] = [
|
||||
PatternEntry(pos=0.0, vel=115),
|
||||
PatternEntry(pos=3.0, vel=100),
|
||||
PatternEntry(pos=4.0, vel=105), # beat 2 — denser
|
||||
PatternEntry(pos=5.0, vel=95), # beat 2& — perreo accent
|
||||
PatternEntry(pos=6.0, vel=90), # beat 2
|
||||
PatternEntry(pos=7.0, vel=85),
|
||||
PatternEntry(pos=8.0, vel=105),
|
||||
PatternEntry(pos=9.0, vel=90),
|
||||
PatternEntry(pos=11.0, vel=110), # beat 3& — strong
|
||||
PatternEntry(pos=12.0, vel=100),
|
||||
PatternEntry(pos=13.0, vel=85),
|
||||
]
|
||||
|
||||
|
||||
# Trápico — half-time feel, fewer kicks
|
||||
TRAPICO_STYLE: list[PatternEntry] = [
|
||||
PatternEntry(pos=0.0, vel=110),
|
||||
PatternEntry(pos=4.0, vel=100), # beat 2 only
|
||||
PatternEntry(pos=8.0, vel=105),
|
||||
PatternEntry(pos=12.0, vel=95),
|
||||
]
|
||||
|
||||
|
||||
# Denser variant — closer to real track analysis
|
||||
# Uses positions with highest frequency counts: 4, 11, 8, 12, 9
|
||||
DENSE_VARIANT: list[PatternEntry] = [
|
||||
PatternEntry(pos=0.0, vel=100),
|
||||
PatternEntry(pos=3.0, vel=90),
|
||||
PatternEntry(pos=4.0, vel=115), # beat 2 — densest hit
|
||||
PatternEntry(pos=5.0, vel=90),
|
||||
PatternEntry(pos=6.0, vel=85),
|
||||
PatternEntry(pos=7.0, vel=80),
|
||||
PatternEntry(pos=8.0, vel=105), # beat 3
|
||||
PatternEntry(pos=9.0, vel=90),
|
||||
PatternEntry(pos=10.0, vel=80),
|
||||
PatternEntry(pos=11.0, vel=110), # beat 3&
|
||||
PatternEntry(pos=12.0, vel=100), # beat 4
|
||||
PatternEntry(pos=13.0, vel=85),
|
||||
PatternEntry(pos=14.0, vel=90),
|
||||
]
|
||||
|
||||
|
||||
PATTERN_BANKS: dict[str, list[PatternEntry]] = {
|
||||
"dembow_classico": DEMBOW_CLASSICO,
|
||||
"perreo": PERREO_STYLE,
|
||||
"trapico": TRAPICO_STYLE,
|
||||
"dense": DENSE_VARIANT,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Snare patterns — follow dembow positions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SNARE_DEMBOW_CLASSICO: list[PatternEntry] = [
|
||||
PatternEntry(pos=3.0, vel=100), # beat 1&
|
||||
PatternEntry(pos=4.0, vel=110), # beat 2 — primary
|
||||
PatternEntry(pos=11.0, vel=105), # beat 3& — THE signature
|
||||
PatternEntry(pos=12.0, vel=95), # beat 4
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hihat patterns — 16th-note grid with dembow accents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
HIHAT_DEMBOW_CLASSICO: list[PatternEntry] = [
|
||||
# All 16ths with accents on dembow positions
|
||||
PatternEntry(pos=0.0, vel=85), # beat 1
|
||||
PatternEntry(pos=0.25, vel=55),
|
||||
PatternEntry(pos=0.5, vel=60),
|
||||
PatternEntry(pos=0.75, vel=55),
|
||||
PatternEntry(pos=1.0, vel=80), # beat 2
|
||||
PatternEntry(pos=1.25, vel=55),
|
||||
PatternEntry(pos=1.5, vel=60),
|
||||
PatternEntry(pos=1.75, vel=55),
|
||||
PatternEntry(pos=2.0, vel=85), # beat 3
|
||||
PatternEntry(pos=2.25, vel=55),
|
||||
PatternEntry(pos=2.5, vel=80), # beat 3& — accent
|
||||
PatternEntry(pos=2.75, vel=55),
|
||||
PatternEntry(pos=3.0, vel=70), # beat 3
|
||||
PatternEntry(pos=3.25, vel=55),
|
||||
PatternEntry(pos=3.5, vel=75), # beat 4&
|
||||
PatternEntry(pos=3.75, vel=55),
|
||||
]
|
||||
|
||||
|
||||
# Offbeat hihat — for breakdowns/intervals
|
||||
HIHAT_OFFBEAT: list[PatternEntry] = [
|
||||
PatternEntry(pos=0.5, vel=75),
|
||||
PatternEntry(pos=1.5, vel=70),
|
||||
PatternEntry(pos=2.5, vel=75),
|
||||
PatternEntry(pos=3.5, vel=70),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chord progression — i-VI-III-VII from real track analysis
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CHORD_PROGRESSION_MINOR: list[tuple[int, list[int]]] = [
|
||||
# (root_semitone_offset, [intervals from root])
|
||||
(0, [0, 3, 7]), # i (Am): root, minor 3rd, perfect 5th
|
||||
(8, [0, 4, 7]), # VI (F): root, major 3rd, perfect 5th
|
||||
(4, [0, 3, 7]), # III (C): root, minor 3rd, perfect 5th
|
||||
(10, [0, 4, 7]), # VII (G): root, major 3rd, perfect 5th
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bass pattern — follows kick positions with tresillo grouping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
BASS_TRESILLO_POSITIONS: list[float] = [
|
||||
0.0, 0.75, 1.5, # first group of 3
|
||||
2.25, 3.0, 3.75, # second group of 3
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Section-aware generator presets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SECTION_GENERATOR_MAP: dict[str, dict[str, str]] = {
|
||||
"intro": {
|
||||
"kick": "dembow_classico",
|
||||
"snare": "dembow_classico",
|
||||
"hihat": "offbeat",
|
||||
},
|
||||
"verse": {
|
||||
"kick": "dembow_classico",
|
||||
"snare": "dembow_classico",
|
||||
"hihat": "dembow_classico",
|
||||
},
|
||||
"build": {
|
||||
"kick": "dense",
|
||||
"snare": "dembow_classico",
|
||||
"hihat": "dembow_classico",
|
||||
},
|
||||
"pre_chorus": {
|
||||
"kick": "dense",
|
||||
"snare": "dembow_classico",
|
||||
"hihat": "dembow_classico",
|
||||
},
|
||||
"chorus": {
|
||||
"kick": "dense",
|
||||
"snare": "dembow_classico",
|
||||
"hihat": "dembow_classico",
|
||||
},
|
||||
"drop": {
|
||||
"kick": "dense",
|
||||
"snare": "dembow_classico",
|
||||
"hihat": "dembow_classico",
|
||||
},
|
||||
"break": {
|
||||
"kick": "offbeat",
|
||||
"snare": "offbeat",
|
||||
"hihat": "offbeat",
|
||||
},
|
||||
"gap": {
|
||||
"kick": "offbeat",
|
||||
"snare": "offbeat",
|
||||
"hihat": "offbeat",
|
||||
},
|
||||
"bridge": {
|
||||
"kick": "dembow_classico",
|
||||
"snare": "dembow_classico",
|
||||
"hihat": "offbeat",
|
||||
},
|
||||
"outro": {
|
||||
"kick": "trapico",
|
||||
"snare": "dembow_classico",
|
||||
"hihat": "dembow_classico",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_pattern_bank(name: str) -> list[PatternEntry]:
|
||||
"""Return the pattern bank by name."""
|
||||
return PATTERN_BANKS.get(name, DEMBOW_CLASSICO)
|
||||
|
||||
|
||||
def get_section_energy(section_name: str) -> float:
|
||||
"""Get energy level for a section type."""
|
||||
return SECTION_ENERGY.get(section_name.lower(), 0.5)
|
||||
|
||||
|
||||
def get_section_generator_map(section_name: str) -> dict[str, str]:
|
||||
"""Get pattern bank assignments for a section type."""
|
||||
return SECTION_GENERATOR_MAP.get(section_name.lower(), SECTION_GENERATOR_MAP["verse"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Structure Templates — genre-aware section arrangement patterns
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Templates are lists of section names that define the high-level arrangement.
|
||||
# Bar counts and energy levels are randomized within bounds when generated.
|
||||
SECTION_TEMPLATES: dict[str, list[str]] = {
|
||||
"extracted_real_tracks": [
|
||||
"build", "verse", "build", "verse", "gap", "drop", "build", "break",
|
||||
"drop", "build", "verse", "build",
|
||||
],
|
||||
"standard": [
|
||||
"intro", "verse", "chorus", "verse", "chorus", "bridge", "chorus", "outro",
|
||||
],
|
||||
"with_pre_chorus": [
|
||||
"intro", "verse", "pre_chorus", "chorus", "verse", "pre_chorus", "chorus", "outro",
|
||||
],
|
||||
"breakdown_drop": [
|
||||
"intro", "verse", "chorus", "break", "drop", "verse", "chorus", "outro",
|
||||
],
|
||||
}
|
||||
|
||||
# Default bar count and energy per section type (used as baseline for randomization)
|
||||
_SECTION_DEFAULTS: dict[str, tuple[int, float]] = {
|
||||
"intro": (4, 0.3),
|
||||
"verse": (8, 0.35),
|
||||
"build": (16, 0.6),
|
||||
"pre_chorus": (4, 0.65),
|
||||
"chorus": (8, 0.95),
|
||||
"drop": (4, 1.0),
|
||||
"break": (2, 0.05),
|
||||
"gap": (2, 0.05),
|
||||
"bridge": (8, 0.4),
|
||||
"outro": (8, 0.35),
|
||||
}
|
||||
|
||||
|
||||
def generate_structure(
|
||||
genre_config: dict,
|
||||
bpm: float,
|
||||
key: str,
|
||||
seed: int | None = None,
|
||||
) -> list[dict]:
|
||||
"""Generate section structure from template with randomization.
|
||||
|
||||
Args:
|
||||
genre_config: Loaded genre JSON dict
|
||||
bpm: BPM for potential future use
|
||||
key: Musical key for potential future use
|
||||
seed: Random seed for reproducibility (default: None = unseeded).
|
||||
When set, reseeds the global random state.
|
||||
|
||||
Returns:
|
||||
List of section dicts with 'name', 'bars', 'energy' keys.
|
||||
Each call without seed produces different bar counts and energy levels.
|
||||
"""
|
||||
if seed is not None:
|
||||
random.seed(seed)
|
||||
|
||||
templates_dict = genre_config.get("structure", {}).get("templates", SECTION_TEMPLATES)
|
||||
template_names = list(templates_dict.keys())
|
||||
template_key = random.choice(template_names)
|
||||
template_sections = templates_dict[template_key]
|
||||
|
||||
sections: list[dict] = []
|
||||
for s in template_sections:
|
||||
# Handle both string names (from SECTION_TEMPLATES) and dicts (from JSON)
|
||||
if isinstance(s, str):
|
||||
name = s
|
||||
base_bars, base_energy = _SECTION_DEFAULTS.get(name, (4, 0.5))
|
||||
else:
|
||||
name = s.get("name", "unknown")
|
||||
base_bars = s.get("bars", 4)
|
||||
base_energy = s.get("energy", 0.5)
|
||||
|
||||
# Randomize bar counts ±2, energy ±0.1
|
||||
bars = max(1, base_bars + random.randint(-2, 2))
|
||||
energy = max(0.0, min(1.0, base_energy + random.uniform(-0.1, 0.1)))
|
||||
sections.append({"name": name, "bars": bars, "energy": round(energy, 2)})
|
||||
|
||||
return sections
|
||||
@@ -1,4 +1,15 @@
|
||||
"""Reggaeton rhythm generators — pure functions returning note dicts per channel."""
|
||||
"""Reggaeton rhythm generators — pure functions returning note dicts per channel.
|
||||
|
||||
All patterns follow the DEMBOW — the foundational rhythm of reggaetón:
|
||||
|
||||
Beat: 1 & 2 & 3 & 4 &
|
||||
Pos: 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5
|
||||
Kick: X . . . X . . X ← 1, 3, 4&
|
||||
Snare: . . X . . X . . ← 2, 3&
|
||||
|
||||
The 3&-position snare hit is the signature dembow characteristic.
|
||||
Everything locks to this grid.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
@@ -19,38 +30,141 @@ CH_CL = 16 # clap.wav
|
||||
# key — always 60 for drum samples (pitch irrelevant, sample just plays)
|
||||
# vel — 1–127 after applying velocity_mult
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DEMBOW PATTERN CONSTANTS
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Dembow kick: beats 1, 3, 4& (positions 0.0, 2.0, 3.5)
|
||||
# The 4&-kick is what makes it dembow, not just a straight 1-3 pattern
|
||||
KICK_DEMBOW_CLASSICO = [
|
||||
(0.0, 115), # beat 1 — hard hit
|
||||
(2.0, 100), # beat 3 — medium
|
||||
(3.5, 90), # beat 4& — ghost/dembow accent
|
||||
]
|
||||
|
||||
# Dembow perreo: adds kick on 2& for aggressive perreo style
|
||||
KICK_DEMBOW_PERREO = [
|
||||
(0.0, 115), # beat 1
|
||||
(1.5, 90), # beat 2& — extra perreo kick
|
||||
(2.0, 95), # beat 3
|
||||
(3.5, 100), # beat 4&
|
||||
]
|
||||
|
||||
# Trápico half-time: kicks on 1 and 3 only, simpler 808 feel
|
||||
KICK_TRAPICO = [
|
||||
(0.0, 110), # beat 1
|
||||
(2.0, 100), # beat 3
|
||||
]
|
||||
|
||||
# Dembow snare: beats 2 and 3& (positions 1.0, 2.5)
|
||||
# The 3&-position is the NON-NEGOTIABLE syncopation — this is what makes it reggaetón
|
||||
SNARE_DEMBOW = [
|
||||
(1.0, 105), # beat 2 — primary snare hit
|
||||
(2.5, 100), # beat 3& — THE signature dembow syncopation
|
||||
]
|
||||
|
||||
# Snare fills: adds extra notes on 2, 3, 3.5 for chorus intensity
|
||||
SNARE_FILLS = [
|
||||
(1.0, 105), # beat 2
|
||||
(2.0, 80), # beat 2& — fill
|
||||
(2.5, 100), # beat 3& — dembow accent
|
||||
(3.0, 85), # beat 3
|
||||
(3.5, 90), # beat 4& — fill
|
||||
]
|
||||
|
||||
# Hihat 16th with dembow accent pattern
|
||||
# Accents align with dembow kick/snare positions for drive
|
||||
HIHAT_16TH_DEMBOW = [
|
||||
(0.0, 85), # beat 1 — accent
|
||||
(0.25, 55),
|
||||
(0.5, 60),
|
||||
(0.75, 55),
|
||||
(1.0, 80), # beat 2 — accent (snare position)
|
||||
(1.25, 55),
|
||||
(1.5, 60),
|
||||
(1.75, 55),
|
||||
(2.0, 85), # beat 3 — accent
|
||||
(2.25, 55),
|
||||
(2.5, 80), # beat 3& — accent (snare position)
|
||||
(2.75, 55),
|
||||
(3.0, 70), # beat 3 downbeat (medium)
|
||||
(3.25, 55),
|
||||
(3.5, 75), # beat 4& — accent
|
||||
(3.75, 55),
|
||||
]
|
||||
|
||||
# Offbeat hihat for intro/breakdown: plays on all & positions
|
||||
HIHAT_OFFBEAT = [
|
||||
(0.5, 75), # 1&
|
||||
(1.5, 70), # 2&
|
||||
(2.5, 75), # 3&
|
||||
(3.5, 70), # 4&
|
||||
]
|
||||
|
||||
# Clap layered with snare: matches snare positions exactly for punch
|
||||
CLAP_DEMBOW = [
|
||||
(1.0, 115), # beat 2 — reinforce snare
|
||||
(2.5, 110), # beat 3& — reinforce dembow syncopation
|
||||
]
|
||||
|
||||
# Congas following dembow: slap on beat 2, open tone on 3&
|
||||
CONGAS_DEMBOW = [
|
||||
(1.0, 95), # slap on beat 2 (matches snare)
|
||||
(2.5, 85), # open tone on 3& (matches snare)
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _apply_groove(notes: list[dict], groove_strength: float) -> list[dict]:
|
||||
def _apply_groove(note_data, groove_strength: float):
|
||||
"""Apply groove timing and velocity variations to notes.
|
||||
|
||||
note_data: either a list[dict] of notes OR a dict[int, list[dict]] (channel → notes).
|
||||
groove_strength: 0.0 = no effect, 1.0 = maximum groove feel.
|
||||
"""
|
||||
if groove_strength <= 0:
|
||||
return notes
|
||||
return note_data
|
||||
jitter = 5 + groove_strength * 10
|
||||
nudge = groove_strength * 0.02
|
||||
for n in notes:
|
||||
n["vel"] = max(1, min(127, n["vel"] + random.uniform(-jitter, jitter)))
|
||||
n["pos"] = max(0, n["pos"] + random.uniform(-nudge, nudge))
|
||||
return notes
|
||||
|
||||
# Normalize to {channel: [notes]} format
|
||||
if isinstance(note_data, dict):
|
||||
channel_notes_map = note_data
|
||||
else:
|
||||
channel_notes_map = {"_single": note_data}
|
||||
|
||||
for channel, notes in channel_notes_map.items():
|
||||
for n in notes:
|
||||
n["vel"] = max(1, min(127, int(n["vel"] + random.uniform(-jitter, jitter))))
|
||||
n["pos"] = max(0, n["pos"] + random.uniform(-nudge, nudge))
|
||||
|
||||
return note_data
|
||||
|
||||
def _clamp_vel(vel: int) -> int:
|
||||
"""Clamp velocity to valid MIDI range [1, 127]."""
|
||||
return max(1, min(127, vel))
|
||||
|
||||
|
||||
def _apply_vel(base_vel: int, velocity_mult: float) -> int:
|
||||
"""Multiply base velocity by velocity_mult and clamp."""
|
||||
return _clamp_vel(int(base_vel * velocity_mult))
|
||||
|
||||
|
||||
def _note(pos: float, length: float, vel: int) -> dict:
|
||||
"""Create a note dict with key=60."""
|
||||
return {"pos": pos, "len": length, "key": 60, "vel": vel}
|
||||
|
||||
def _build_bar(pattern: list[tuple[float, int]], bar_offset: float) -> list[dict]:
|
||||
"""Build one bar of notes from a pattern list.
|
||||
|
||||
Args:
|
||||
pattern: List of (beat_position, base_velocity) tuples
|
||||
bar_offset: Bar offset in beats (bar_n * 4.0)
|
||||
"""
|
||||
notes = []
|
||||
for pos, base_vel in pattern:
|
||||
notes.append(_note(bar_offset + pos, 0.25, base_vel))
|
||||
return notes
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kick generators
|
||||
@@ -62,16 +176,17 @@ def kick_main_notes(
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Dembow kick: beat 1 (hard, vel 115) + beat 2-and (the dembow hit, vel 105).
|
||||
"""Dembow kick — beats 1, 3, 4& (positions 0.0, 2.0, 3.5).
|
||||
|
||||
Positions per bar: 0.0 and 1.5 (the classic "one — &-two" reggaeton kick).
|
||||
This is the CLASSIC dembow kick. The 4&-position is what separates
|
||||
reggaetón from generic Latin music.
|
||||
Returns {CH_K: [notes...]}.
|
||||
"""
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
notes.append(_note(o, 0.25, _apply_vel(115, velocity_mult)))
|
||||
notes.append(_note(o + 1.5, 0.25, _apply_vel(105, velocity_mult)))
|
||||
for pos, base_vel in KICK_DEMBOW_CLASSICO:
|
||||
notes.append(_note(o + pos, 0.25, _apply_vel(base_vel, velocity_mult)))
|
||||
return _apply_groove({CH_K: notes}, groove_strength)
|
||||
|
||||
|
||||
@@ -81,14 +196,16 @@ def kick_sparse_notes(
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Sparse intro/outro kick: just beat 1 per bar (vel 110).
|
||||
"""Trápico kick — beats 1 and 3 only (positions 0.0, 2.0).
|
||||
|
||||
Half-time feel for trap/reggaetón fusion. Simpler than dembow.
|
||||
Returns {CH_K: [notes...]}.
|
||||
"""
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
notes.append(_note(o, 0.25, _apply_vel(110, velocity_mult)))
|
||||
for pos, base_vel in KICK_TRAPICO:
|
||||
notes.append(_note(o + pos, 0.25, _apply_vel(base_vel, velocity_mult)))
|
||||
return _apply_groove({CH_K: notes}, groove_strength)
|
||||
|
||||
|
||||
@@ -98,9 +215,9 @@ def kick_outro_notes(
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Outro kick: dembow pattern with 0.75 baseline softness.
|
||||
"""Outro kick — dembow pattern with softer velocity.
|
||||
|
||||
Delegates to kick_main_notes with an additional 0.75 velocity scaling.
|
||||
Delegates to kick_main_notes with 0.75 velocity baseline.
|
||||
Returns {CH_K: [notes...]}.
|
||||
"""
|
||||
return kick_main_notes(bars, velocity_mult=velocity_mult * 0.75, density=density, groove_strength=groove_strength)
|
||||
@@ -116,17 +233,17 @@ def snare_verse_notes(
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Reggaeton snare: beats 2, 3, 3-and, 4 per bar.
|
||||
"""Dembow snare — beats 2 and 3& (positions 1.0, 2.5).
|
||||
|
||||
Positions: 1.0 (vel 100), 2.0 (vel 95), 2.5 (vel 110), 3.0 (vel 90).
|
||||
THE signature dembow characteristic. The 3&-position syncopation
|
||||
is what makes reggaetón groove the way it does.
|
||||
Returns {CH_S: [notes...]}.
|
||||
"""
|
||||
_PATTERN = [(1.0, 100), (2.0, 95), (2.5, 110), (3.0, 90)]
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for p, v in _PATTERN:
|
||||
notes.append(_note(o + p, 0.15, _apply_vel(v, velocity_mult)))
|
||||
for pos, base_vel in SNARE_DEMBOW:
|
||||
notes.append(_note(o + pos, 0.15, _apply_vel(base_vel, velocity_mult)))
|
||||
return _apply_groove({CH_S: notes}, groove_strength)
|
||||
|
||||
|
||||
@@ -136,24 +253,16 @@ def snare_fill_notes(
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Busier snare with 16th-note fills: adds positions 2.25 and 3.75.
|
||||
"""Chorus snare fills — dembow base plus extras on 2, 3, 3&.
|
||||
|
||||
Verse base (1.0, 2.0, 2.5, 3.0) plus 16th fills at 2.25 and 3.75.
|
||||
Adds extra notes for build-up sections while keeping the 3& accent.
|
||||
Returns {CH_S: [notes...]}.
|
||||
"""
|
||||
_PATTERN = [
|
||||
(1.0, 100),
|
||||
(2.0, 95),
|
||||
(2.25, 80), # 16th fill
|
||||
(2.5, 110),
|
||||
(3.0, 90),
|
||||
(3.75, 85), # 16th fill
|
||||
]
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for p, v in _PATTERN:
|
||||
notes.append(_note(o + p, 0.15, _apply_vel(v, velocity_mult)))
|
||||
for pos, base_vel in SNARE_FILLS:
|
||||
notes.append(_note(o + pos, 0.15, _apply_vel(base_vel, velocity_mult)))
|
||||
return _apply_groove({CH_S: notes}, groove_strength)
|
||||
|
||||
|
||||
@@ -163,9 +272,9 @@ def snare_outro_notes(
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Softer outro snare (velocity_mult on top of 0.7 baseline).
|
||||
"""Outro snare — dembow with softer velocity.
|
||||
|
||||
Delegates to snare_verse_notes with an additional 0.7 velocity scaling.
|
||||
Delegates to snare_verse_notes with 0.7 velocity baseline.
|
||||
Returns {CH_S: [notes...]}.
|
||||
"""
|
||||
return snare_verse_notes(bars, velocity_mult=velocity_mult * 0.7, density=density, groove_strength=groove_strength)
|
||||
@@ -181,25 +290,19 @@ def hihat_16th_notes(
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""16th-note hihat with three-tier accent mapping.
|
||||
"""16th-note hihat with dembow accent pattern.
|
||||
|
||||
Accented on quarter notes (vel 85), medium on 8ths (vel 60), soft on
|
||||
off-8ths (vel 40). density=1.0 → all 16ths; density=0.5 → every other.
|
||||
Accents align with kick/snare positions (beats 1, 2, 3, 3&) to drive the groove.
|
||||
density=1.0 → all 16ths; density=0.5 → every other 16th.
|
||||
Returns {CH_H: [notes...]}.
|
||||
"""
|
||||
notes: list[dict] = []
|
||||
step = max(1, round(1.0 / density)) if density > 0 else 1
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for i in range(0, 16, step):
|
||||
beat_frac = i * 0.25 # position within bar in beats
|
||||
if beat_frac % 1.0 == 0.0: # quarter note position
|
||||
base_vel = 85
|
||||
elif beat_frac % 0.5 == 0.0: # 8th note position
|
||||
base_vel = 60
|
||||
else: # 16th note position
|
||||
base_vel = 40
|
||||
notes.append(_note(o + beat_frac, 0.1, _apply_vel(base_vel, velocity_mult)))
|
||||
for idx, (pos, base_vel) in enumerate(HIHAT_16TH_DEMBOW):
|
||||
if idx % step == 0:
|
||||
notes.append(_note(o + pos, 0.1, _apply_vel(base_vel, velocity_mult)))
|
||||
return _apply_groove({CH_H: notes}, groove_strength)
|
||||
|
||||
|
||||
@@ -209,59 +312,65 @@ def hihat_8th_notes(
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""8th-note hihat for intro/breakdown.
|
||||
"""Offbeat hihat for intro/breakdown.
|
||||
|
||||
Accented on beats (vel 70), off-beats softer (vel 50).
|
||||
Plays on all & positions (0.5, 1.5, 2.5, 3.5) — creates breathing room.
|
||||
Returns {CH_H: [notes...]}.
|
||||
"""
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for i in range(8):
|
||||
base_vel = 70 if i % 2 == 0 else 50
|
||||
notes.append(_note(o + i * 0.5, 0.1, _apply_vel(base_vel, velocity_mult)))
|
||||
for pos, base_vel in HIHAT_OFFBEAT:
|
||||
notes.append(_note(o + pos, 0.1, _apply_vel(base_vel, velocity_mult)))
|
||||
return _apply_groove({CH_H: notes}, groove_strength)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Clap generator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def clap_24_notes(
|
||||
bars: int,
|
||||
velocity_mult: float = 1.0,
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Classic reggaeton clap: beats 2 and 4 → positions 1.0 and 3.0 per bar.
|
||||
"""Dembow clap — beats 2 and 3& (positions 1.0, 2.5).
|
||||
|
||||
Hard clap (vel 120).
|
||||
Layered with snare to reinforce the dembow characteristic.
|
||||
Matches SNARE_DEMBOW positions exactly.
|
||||
Returns {CH_CL: [notes...]}.
|
||||
"""
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
notes.append(_note(o + 1.0, 0.15, _apply_vel(120, velocity_mult)))
|
||||
notes.append(_note(o + 3.0, 0.15, _apply_vel(120, velocity_mult)))
|
||||
for pos, base_vel in CLAP_DEMBOW:
|
||||
notes.append(_note(o + pos, 0.15, _apply_vel(base_vel, velocity_mult)))
|
||||
return _apply_groove({CH_CL: notes}, groove_strength)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Percussion generators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def perc_combo_notes(
|
||||
bars: int,
|
||||
velocity_mult: float = 1.0,
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Perc1 + Perc2 offbeat combo (tumba feel).
|
||||
"""Conga pattern following dembow — slap on beat 2, open on 3&.
|
||||
|
||||
perc2 (CH_P2): positions 0.75 (vel 85) and 2.75 (vel 80).
|
||||
perc1 (CH_P1): positions 1.5 (vel 70) and 3.5 (vel 65).
|
||||
Returns {CH_P1: [...], CH_P2: [...]}.
|
||||
perc2 (CH_P2): conga slap and open tone
|
||||
perc1 (CH_P1): reserved for additional perc (not used in base pattern)
|
||||
Returns {CH_P2: [...], CH_P1: [...]}.
|
||||
"""
|
||||
p2_notes: list[dict] = []
|
||||
p1_notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
p2_notes.append(_note(o + 0.75, 0.1, _apply_vel(85, velocity_mult)))
|
||||
p2_notes.append(_note(o + 2.75, 0.1, _apply_vel(80, velocity_mult)))
|
||||
p1_notes.append(_note(o + 1.5, 0.1, _apply_vel(70, velocity_mult)))
|
||||
p1_notes.append(_note(o + 3.5, 0.1, _apply_vel(65, velocity_mult)))
|
||||
for pos, base_vel in CONGAS_DEMBOW:
|
||||
p2_notes.append(_note(o + pos, 0.1, _apply_vel(base_vel, velocity_mult)))
|
||||
return _apply_groove({CH_P1: p1_notes, CH_P2: p2_notes}, groove_strength)
|
||||
|
||||
|
||||
@@ -271,14 +380,13 @@ def rim_build_notes(
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Rim roll that builds intensity across bars (4-bar cycle).
|
||||
"""Rim roll that builds intensity across a 4-bar cycle.
|
||||
|
||||
Bar N%4=0: 16th indices 0,2,8,14 (sparse opening)
|
||||
Bar N%4=1: indices 0,2,4,8,10,14 (filling in)
|
||||
Bar N%4=2: indices 0,2,4,6,8,10,12,14 (every other 16th)
|
||||
Bar N%4=3: all 16 indices (full roll)
|
||||
|
||||
Velocity ramps: 50 → 65 → 80 → 100 across the 4-bar cycle.
|
||||
Bar N%4=0: sparse (positions 0, 2, 8, 14 in 16ths)
|
||||
Bar N%4=1: filling in (positions 0, 2, 4, 8, 10, 14)
|
||||
Bar N%4=2: every other 16th (positions 0, 2, 4, 6, 8, 10, 12, 14)
|
||||
Bar N%4=3: full roll (all 16 positions)
|
||||
Velocity ramps: 50 → 65 → 80 → 100 across the cycle.
|
||||
Returns {CH_R: [notes...]}.
|
||||
"""
|
||||
_PATTERNS = [
|
||||
@@ -319,13 +427,145 @@ GENERATORS: dict[str, callable] = {
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pattern-bank-aware generators (new — use extracted real-track data)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def kick_pattern_bank_notes(
|
||||
bars: int,
|
||||
velocity_mult: float = 1.0,
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
bank: str = "dembow_classico",
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Generate kick notes from a named pattern bank.
|
||||
|
||||
Args:
|
||||
bars: Number of bars
|
||||
velocity_mult: Velocity multiplier per section energy
|
||||
density: 1.0 = all notes in pattern, 0.5 = every other, etc.
|
||||
groove_strength: Groove amount (0.0 = none)
|
||||
bank: Pattern bank name — "dembow_classico", "perreo", "trapico", "dense"
|
||||
"""
|
||||
from src.composer.patterns import get_pattern_bank, PatternEntry
|
||||
|
||||
pattern = get_pattern_bank(bank)
|
||||
step = max(1, round(1.0 / density)) if density > 0 else 1
|
||||
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for idx, entry in enumerate(pattern):
|
||||
if idx % step == 0:
|
||||
notes.append(_note(
|
||||
o + entry.pos,
|
||||
entry.len,
|
||||
_apply_vel(entry.vel, velocity_mult),
|
||||
))
|
||||
return _apply_groove({CH_K: notes}, groove_strength)
|
||||
|
||||
|
||||
def snare_pattern_bank_notes(
|
||||
bars: int,
|
||||
velocity_mult: float = 1.0,
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
bank: str = "dembow_classico",
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Generate snare notes from a named pattern bank.
|
||||
|
||||
Uses SNARE_DEMBOW_CLASSICO positions by default (beats 2 and 3&).
|
||||
"""
|
||||
from src.composer.patterns import SNARE_DEMBOW_CLASSICO
|
||||
|
||||
pattern = SNARE_DEMBOW_CLASSICO
|
||||
step = max(1, round(1.0 / density)) if density > 0 else 1
|
||||
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for idx, entry in enumerate(pattern):
|
||||
if idx % step == 0:
|
||||
notes.append(_note(
|
||||
o + entry.pos,
|
||||
entry.len,
|
||||
_apply_vel(entry.vel, velocity_mult),
|
||||
))
|
||||
return _apply_groove({CH_S: notes}, groove_strength)
|
||||
|
||||
|
||||
def hihat_pattern_bank_notes(
|
||||
bars: int,
|
||||
velocity_mult: float = 1.0,
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
bank: str = "dembow_classico",
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Generate hihat notes from a named pattern bank.
|
||||
|
||||
bank="dembow_classico" → 16th-note grid with dembow accents
|
||||
bank="offbeat" → offbeat positions only (for breakdowns)
|
||||
"""
|
||||
from src.composer.patterns import HIHAT_DEMBOW_CLASSICO, HIHAT_OFFBEAT
|
||||
|
||||
pattern = HIHAT_OFFBEAT if bank == "offbeat" else HIHAT_DEMBOW_CLASSICO
|
||||
step = max(1, round(1.0 / density)) if density > 0 else 1
|
||||
|
||||
notes: list[dict] = []
|
||||
for b in range(bars):
|
||||
o = b * 4.0
|
||||
for idx, entry in enumerate(pattern):
|
||||
if idx % step == 0:
|
||||
notes.append(_note(
|
||||
o + entry.pos,
|
||||
entry.len,
|
||||
_apply_vel(entry.vel, velocity_mult),
|
||||
))
|
||||
return _apply_groove({CH_H: notes}, groove_strength)
|
||||
|
||||
|
||||
# Extended registry including pattern-bank generators
|
||||
PATTERN_GENERATORS: dict[str, callable] = {
|
||||
"kick_pattern_bank_notes": kick_pattern_bank_notes,
|
||||
"snare_pattern_bank_notes": snare_pattern_bank_notes,
|
||||
"hihat_pattern_bank_notes": hihat_pattern_bank_notes,
|
||||
# Fallback to legacy generators
|
||||
"kick_main_notes": kick_main_notes,
|
||||
"kick_sparse_notes": kick_sparse_notes,
|
||||
"snare_verse_notes": snare_verse_notes,
|
||||
"snare_fill_notes": snare_fill_notes,
|
||||
"hihat_16th_notes": hihat_16th_notes,
|
||||
"hihat_8th_notes": hihat_8th_notes,
|
||||
"clap_24_notes": clap_24_notes,
|
||||
"perc_combo_notes": perc_combo_notes,
|
||||
"rim_build_notes": rim_build_notes,
|
||||
}
|
||||
|
||||
|
||||
def get_notes(
|
||||
generator_name: str,
|
||||
bars: int,
|
||||
velocity_mult: float = 1.0,
|
||||
density: float = 1.0,
|
||||
groove_strength: float = 0.0,
|
||||
bank: str = "dembow_classico",
|
||||
) -> dict[int, list[dict]]:
|
||||
"""Dispatch to the named generator. Raises KeyError if not found."""
|
||||
gen = GENERATORS[generator_name]
|
||||
return gen(bars, velocity_mult, density, groove_strength)
|
||||
"""Dispatch to the named generator.
|
||||
|
||||
Pattern-bank-aware generators accept `bank` parameter.
|
||||
Legacy generators ignore the bank parameter.
|
||||
"""
|
||||
# Try pattern generators first, fall back to legacy
|
||||
if generator_name in PATTERN_GENERATORS:
|
||||
gen = PATTERN_GENERATORS[generator_name]
|
||||
elif generator_name in GENERATORS:
|
||||
gen = GENERATORS[generator_name]
|
||||
else:
|
||||
raise KeyError(f"Unknown generator: {generator_name}")
|
||||
|
||||
# Pattern-bank generators accept bank parameter
|
||||
import inspect
|
||||
sig = inspect.signature(gen)
|
||||
if "bank" in sig.parameters:
|
||||
return gen(bars, velocity_mult, density, groove_strength, bank=bank)
|
||||
return gen(bars, velocity_mult, density, groove_strength)
|
||||
Reference in New Issue
Block a user