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:
renato97
2026-05-03 16:08:07 -03:00
parent 32dafd94e0
commit 3444006411
10 changed files with 1664 additions and 285 deletions

View File

@@ -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
View 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

View File

@@ -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 — 1127 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)