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:
@@ -2,64 +2,90 @@
|
||||
"genre": "reggaeton",
|
||||
"era": "2009",
|
||||
"display_name": "Reggaeton 2009 (Era de Oro)",
|
||||
"description": "Reggaeton comercial 2006-2010. Daddy Yankee, Wisin y Yandel, Don Omar, Tito El Bambino, Hector El Father. Beat dembow con 808, piano stabs, brass hits.",
|
||||
"description": "Reggaeton comercial 2006-2010. Daddy Yankee, Wisin y Yandel, Don Omar, Tito El Bambino, Hector El Father. Beat dembow con 808, piano stabs, brass hits. AGGRESSIVE groove, dense drum loops, GAP/BREAK section technique.",
|
||||
"bpm": {
|
||||
"min": 88,
|
||||
"max": 102,
|
||||
"default": 96
|
||||
"default": 99
|
||||
},
|
||||
"keys": ["Am", "Dm", "Gm", "Cm", "Em", "Fm", "Bbm"],
|
||||
"time_signature": [4, 4],
|
||||
"ppq": 96,
|
||||
"structure": {
|
||||
"template": "intro-verse-chorus-verse-chorus-outro",
|
||||
"sections": [
|
||||
{"name": "intro", "bars": 4, "energy": 0.3},
|
||||
{"name": "verse", "bars": 8, "energy": 0.6},
|
||||
{"name": "chorus", "bars": 8, "energy": 0.9},
|
||||
{"name": "verse2", "bars": 8, "energy": 0.6},
|
||||
{"name": "chorus2", "bars": 8, "energy": 1.0},
|
||||
{"name": "bridge", "bars": 4, "energy": 0.5},
|
||||
{"name": "chorus3", "bars": 8, "energy": 1.0},
|
||||
{"name": "outro", "bars": 4, "energy": 0.4}
|
||||
]
|
||||
"template": "extracted_real_tracks",
|
||||
"templates": {
|
||||
"extracted_real_tracks": [
|
||||
{"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}
|
||||
],
|
||||
"standard": [
|
||||
{"name": "intro", "bars": 4, "energy": 0.3},
|
||||
{"name": "verse", "bars": 8, "energy": 0.35},
|
||||
{"name": "chorus", "bars": 8, "energy": 0.95},
|
||||
{"name": "verse", "bars": 8, "energy": 0.35},
|
||||
{"name": "chorus", "bars": 8, "energy": 0.95},
|
||||
{"name": "bridge", "bars": 8, "energy": 0.4},
|
||||
{"name": "chorus", "bars": 8, "energy": 0.95},
|
||||
{"name": "outro", "bars": 8, "energy": 0.35}
|
||||
]
|
||||
},
|
||||
"gap_break_energy_contrast": 50.0,
|
||||
"dembow_positions": [0, 4, 8, 11, 12],
|
||||
"note": "GAP/BREAK technique: 1-2 bars near-silence (-50dB) followed by loud DROP (+6dB) creates massive contrast. Real tracks use this extensively."
|
||||
},
|
||||
"pattern_banks": {
|
||||
"dembow_classico": "extracted from 99.4 BPM track (Ejemplo 1)",
|
||||
"perreo": "aggressive variant with extra kicks at beat 1.5& and 2",
|
||||
"trapico": "half-time feel, kicks at beats 1 and 3 only",
|
||||
"dense": "full drum loop density, highest hit frequency positions"
|
||||
},
|
||||
"roles": {
|
||||
"drums": {
|
||||
"description": "Patron dembow - kick en 1 y 2.5, snare en 2 y 4, hi-hats en corcheas",
|
||||
"pattern_type": "dembow",
|
||||
"description": "DENSE drum loop (NOT sparse kick-snare). Kicks at positions 4, 11, 8, 12, 9 — the dembow IS there but buried in a full loop. Pattern bank: kick_pattern_bank_notes",
|
||||
"pattern_type": "drum_loop",
|
||||
"preferred_plugins": ["FPC", "Fruity DrumSynth Live", "DirectWave", "Kontakt 7"],
|
||||
"midi_channel": 0,
|
||||
"mixer_slot": 0,
|
||||
"notes_template": "dembow"
|
||||
"notes_template": "drum_loop_dembow",
|
||||
"bank": "dembow_classico"
|
||||
},
|
||||
"bass": {
|
||||
"description": "808 sub bass que sigue al kick. Sostenido, octave 2.",
|
||||
"description": "808 sub bass. Section-aware: follows section energy. Tresillo grouping in drop/chorus, sparse in break/gap.",
|
||||
"pattern_type": "808_follow_kick",
|
||||
"preferred_plugins": ["Serum 2", "Transistor Bass", "Sytrus", "3x Osc", "ravity(S)"],
|
||||
"midi_channel": 1,
|
||||
"mixer_slot": 1,
|
||||
"octave": 2,
|
||||
"notes_template": "bass_808"
|
||||
"notes_template": "bass_tresillo_section_aware"
|
||||
},
|
||||
"harmony": {
|
||||
"description": "Piano stabs en offbeats. Closed triads.",
|
||||
"description": "Piano stabs in offbeats. Closed triads. Section-aware velocity.",
|
||||
"pattern_type": "piano_stabs",
|
||||
"preferred_plugins": ["FL Keys", "Nexus2", "Kontakt 7", "Sakura", "Pigments"],
|
||||
"midi_channel": 2,
|
||||
"mixer_slot": 2,
|
||||
"notes_template": "piano_stabs"
|
||||
"notes_template": "piano_stabs_section_aware"
|
||||
},
|
||||
"lead": {
|
||||
"description": "Brass hit o melodia sintetizada. Hook del coro.",
|
||||
"description": "Brass hit o melodia sintetizada. Hook syncopation on dembow positions (4, 11, 12). Section-aware density.",
|
||||
"pattern_type": "brass_hook",
|
||||
"preferred_plugins": ["Serum 2", "Omnisphere", "Harmor", "Electra", "ravity(S)"],
|
||||
"midi_channel": 3,
|
||||
"mixer_slot": 3,
|
||||
"notes_template": "lead_hook"
|
||||
"notes_template": "lead_hook_section_aware"
|
||||
},
|
||||
"pad": {
|
||||
"description": "Pad atmosferico sutil para llenar el fondo.",
|
||||
"description": "Pad atmosferico sutil para llenar el fondo. Sustained chord tones.",
|
||||
"pattern_type": "sustained_pad",
|
||||
"preferred_plugins": ["Harmor", "Serum 2", "Omnisphere", "FLEX", "Pigments"],
|
||||
"midi_channel": 4,
|
||||
@@ -83,7 +109,7 @@
|
||||
"popularity": 0.9
|
||||
},
|
||||
{
|
||||
"name": "tensión",
|
||||
"name": "tension",
|
||||
"chords": ["Am", "F", "C", "G"],
|
||||
"beats_per_chord": 4,
|
||||
"popularity": 0.7
|
||||
@@ -152,5 +178,11 @@
|
||||
"Rumor de Guerra - Hector El Father",
|
||||
"Pose - Daddy Yankee",
|
||||
"Llamé Pa Verte - Wisin y Yandel"
|
||||
]
|
||||
],
|
||||
"analysis_notes": {
|
||||
"key_finding": "The dembow IS present (positions 0, 4, 8, 11, 12 high across all instruments) but it's buried in a DENSE drum loop, NOT the textbook sparse kick-snare pattern.",
|
||||
"kick_insight": "Position 4 (beat 2) is the DENSEST kick hit — not beat 1. Real tracks have kicks on almost EVERY 16th note position.",
|
||||
"gap_break_technique": "1-2 bars near-silence (-50dB) followed by loud DROP (+6dB over baseline) creates massive contrast. This is a signature reggaetón technique.",
|
||||
"filter_sweeps": "Spectral centroid drops detected in breakdowns, confirming HPF filtering in intro/filtered sections."
|
||||
}
|
||||
}
|
||||
|
||||
227
scripts/analyze_examples.py
Normal file
227
scripts/analyze_examples.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python
|
||||
"""Reverse-engineer drum patterns from example reggaetón tracks."""
|
||||
from __future__ import annotations
|
||||
import librosa
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def analyze_track(path: str, track_name: str) -> dict:
|
||||
y, sr = librosa.load(path, sr=44100, mono=True)
|
||||
duration = len(y) / sr
|
||||
|
||||
# Tempo and beats
|
||||
tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
|
||||
if isinstance(tempo, np.ndarray):
|
||||
tempo = float(tempo[0]) if tempo.ndim > 0 else float(tempo)
|
||||
beat_times = librosa.frames_to_time(beat_frames, sr=sr)
|
||||
bar_duration = 4 * 60.0 / tempo
|
||||
sixteenth = 60.0 / tempo / 4
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {track_name}")
|
||||
print(f" Duration: {duration:.1f}s | Tempo: {tempo:.1f} BPM")
|
||||
print(f" Bar: {bar_duration:.3f}s | 16th: {sixteenth:.4f}s")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# ---- KICK DETECTION (low frequency onsets) ----
|
||||
onset_env_low = librosa.onset.onset_strength(
|
||||
y=y, sr=sr,
|
||||
feature=librosa.feature.melspectrogram,
|
||||
fmin=20, fmax=300,
|
||||
)
|
||||
kick_onsets = librosa.onset.onset_detect(
|
||||
onset_envelope=onset_env_low, sr=sr,
|
||||
units="time", backtrack=False,
|
||||
)
|
||||
|
||||
# ---- SNARE DETECTION (mid-high frequency onsets) ----
|
||||
onset_env_high = librosa.onset.onset_strength(
|
||||
y=y, sr=sr,
|
||||
feature=librosa.feature.melspectrogram,
|
||||
fmin=800, fmax=8000,
|
||||
)
|
||||
snare_onsets = librosa.onset.onset_detect(
|
||||
onset_envelope=onset_env_high, sr=sr,
|
||||
units="time", backtrack=False,
|
||||
)
|
||||
|
||||
# ---- HIHAT DETECTION (very high frequency) ----
|
||||
onset_env_hh = librosa.onset.onset_strength(
|
||||
y=y, sr=sr,
|
||||
feature=librosa.feature.melspectrogram,
|
||||
fmin=5000, fmax=16000,
|
||||
)
|
||||
hihat_onsets = librosa.onset.onset_detect(
|
||||
onset_envelope=onset_env_hh, sr=sr,
|
||||
units="time", backtrack=False,
|
||||
)
|
||||
|
||||
first_beat = beat_times[0]
|
||||
|
||||
# Analyze first 16 bars
|
||||
n_bars = min(16, int(len(beat_times) / 4))
|
||||
print("KICK PATTERN (first 16 bars, 16th note grid):")
|
||||
print(" Grid: 1 & 2 & 3 & 4 & 1e &a 2e &a 3e &a 4e &a\n")
|
||||
kick_pattern_counts: dict[int, int] = {}
|
||||
|
||||
for bar in range(n_bars):
|
||||
bar_start = first_beat + bar * bar_duration
|
||||
bar_end = bar_start + bar_duration
|
||||
bar_kicks = kick_onsets[(kick_onsets >= bar_start) & (kick_onsets < bar_end)]
|
||||
positions = []
|
||||
for k in bar_kicks:
|
||||
offset = k - bar_start
|
||||
sixteenth_pos = round(offset / sixteenth)
|
||||
if 0 <= sixteenth_pos < 16:
|
||||
positions.append(sixteenth_pos)
|
||||
kick_pattern_counts[sixteenth_pos] = kick_pattern_counts.get(sixteenth_pos, 0) + 1
|
||||
|
||||
pattern = ["."] * 16
|
||||
for p in positions:
|
||||
pattern[p] = "K"
|
||||
line = " ".join(pattern)
|
||||
print(f" Bar {bar+1:2d}: {line}")
|
||||
|
||||
# Most common kick positions
|
||||
print(f"\n Kick frequency: {dict(sorted(kick_pattern_counts.items()))}")
|
||||
|
||||
print(f"\nSNARE PATTERN (first 16 bars, 16th note grid):\n")
|
||||
snare_pattern_counts: dict[int, int] = {}
|
||||
|
||||
for bar in range(n_bars):
|
||||
bar_start = first_beat + bar * bar_duration
|
||||
bar_end = bar_start + bar_duration
|
||||
bar_snares = snare_onsets[(snare_onsets >= bar_start) & (snare_onsets < bar_end)]
|
||||
positions = []
|
||||
for s in bar_snares:
|
||||
offset = s - bar_start
|
||||
sixteenth_pos = round(offset / sixteenth)
|
||||
if 0 <= sixteenth_pos < 16:
|
||||
positions.append(sixteenth_pos)
|
||||
snare_pattern_counts[sixteenth_pos] = snare_pattern_counts.get(sixteenth_pos, 0) + 1
|
||||
|
||||
pattern = ["."] * 16
|
||||
for p in positions:
|
||||
pattern[p] = "S"
|
||||
line = " ".join(pattern)
|
||||
print(f" Bar {bar+1:2d}: {line}")
|
||||
|
||||
print(f"\n Snare frequency: {dict(sorted(snare_pattern_counts.items()))}")
|
||||
|
||||
print(f"\nHIHAT PATTERN (first 16 bars, 16th note grid):\n")
|
||||
hihat_pattern_counts: dict[int, int] = {}
|
||||
|
||||
for bar in range(n_bars):
|
||||
bar_start = first_beat + bar * bar_duration
|
||||
bar_end = bar_start + bar_duration
|
||||
bar_hh = hihat_onsets[(hihat_onsets >= bar_start) & (hihat_onsets < bar_end)]
|
||||
positions = []
|
||||
for h in bar_hh:
|
||||
offset = h - bar_start
|
||||
sixteenth_pos = round(offset / sixteenth)
|
||||
if 0 <= sixteenth_pos < 16:
|
||||
positions.append(sixteenth_pos)
|
||||
hihat_pattern_counts[sixteenth_pos] = hihat_pattern_counts.get(sixteenth_pos, 0) + 1
|
||||
|
||||
pattern = ["."] * 16
|
||||
for p in positions:
|
||||
pattern[p] = "H"
|
||||
line = " ".join(pattern)
|
||||
print(f" Bar {bar+1:2d}: {line}")
|
||||
|
||||
print(f"\n Hihat frequency: {dict(sorted(hihat_pattern_counts.items()))}")
|
||||
|
||||
# ---- SECTION ANALYSIS (full track) ----
|
||||
hop = 2048
|
||||
rms = librosa.feature.rms(y=y, hop_length=hop, frame_length=4096)[0]
|
||||
rms_times = librosa.times_like(rms, sr=sr, hop_length=hop)
|
||||
rms_db = librosa.amplitude_to_db(rms, ref=np.max)
|
||||
|
||||
# Energy per bar
|
||||
total_bars = int(len(beat_times) / 4)
|
||||
bar_energies = []
|
||||
for b in range(total_bars):
|
||||
start = beat_times[b * 4]
|
||||
end = beat_times[min((b + 1) * 4, len(beat_times) - 1)]
|
||||
mask = (rms_times >= start) & (rms_times <= end)
|
||||
if np.any(mask):
|
||||
bar_energies.append(float(np.mean(rms_db[mask])))
|
||||
else:
|
||||
bar_energies.append(-60.0)
|
||||
|
||||
# Detect sections by energy clustering
|
||||
from scipy.ndimage import uniform_filter1d
|
||||
smooth = uniform_filter1d(np.array(bar_energies), size=2)
|
||||
|
||||
# Find section boundaries (>6dB change)
|
||||
diff = np.diff(smooth)
|
||||
boundaries = [0]
|
||||
for idx, d in enumerate(diff):
|
||||
if abs(d) > 6:
|
||||
boundaries.append(idx + 1)
|
||||
boundaries.append(total_bars)
|
||||
|
||||
print(f"\n\nDETECTED SECTIONS ({len(boundaries)-1} sections):\n")
|
||||
section_labels = []
|
||||
for s in range(len(boundaries) - 1):
|
||||
start_bar = boundaries[s]
|
||||
end_bar = boundaries[s + 1]
|
||||
start_time = beat_times[start_bar * 4] if start_bar * 4 < len(beat_times) else 0
|
||||
n_section_bars = end_bar - start_bar
|
||||
avg_energy = np.mean(bar_energies[start_bar:end_bar])
|
||||
|
||||
# Classify section by energy
|
||||
if avg_energy < -30:
|
||||
label = "SILENCE/BREAK"
|
||||
elif avg_energy < -20:
|
||||
label = "INTRO/FILTER"
|
||||
elif avg_energy < -12:
|
||||
label = "VERSE/BRIDGE"
|
||||
elif avg_energy < -6:
|
||||
label = "BUILD/PRE-CHORUS"
|
||||
else:
|
||||
label = "CHORUS/DROP"
|
||||
|
||||
section_labels.append(label)
|
||||
print(f" {start_time:6.1f}s | Bars {start_bar+1:3d}-{end_bar:3d} ({n_section_bars:2d} bars) | {avg_energy:+6.1f} dB | {label}")
|
||||
|
||||
# ---- SPECTRAL ANALYSIS (filter sweeps) ----
|
||||
spectral_centroid = librosa.feature.spectral_centroid(y=y, sr=sr)[0]
|
||||
sc_times = librosa.times_like(spectral_centroid, sr=sr)
|
||||
|
||||
# Smooth and find big changes
|
||||
sc_smooth = uniform_filter1d(spectral_centroid, size=50)
|
||||
sc_diff = np.diff(sc_smooth)
|
||||
big_drops = np.where(sc_diff < -500)[0]
|
||||
big_rises = np.where(sc_diff > 500)[0]
|
||||
|
||||
if len(big_drops) > 0:
|
||||
print(f"\n\nFILTER SWEEPS (spectral centroid drops):\n")
|
||||
for d in big_drops[:10]:
|
||||
t = sc_times[d]
|
||||
print(f" {t:.1f}s - centroid dropped {sc_diff[d]:.0f} Hz (HPF engaging)")
|
||||
|
||||
if len(big_rises) > 0:
|
||||
print(f"\nFILTER OPENS (spectral centroid rises):\n")
|
||||
for r in big_rises[:10]:
|
||||
t = sc_times[r]
|
||||
print(f" {t:.1f}s - centroid rose {sc_diff[r]:.0f} Hz (filter opening)")
|
||||
|
||||
return {
|
||||
"tempo": tempo,
|
||||
"duration": duration,
|
||||
"n_bars": total_bars,
|
||||
"kick_pattern": kick_pattern_counts,
|
||||
"snare_pattern": snare_pattern_counts,
|
||||
"hihat_pattern": hihat_pattern_counts,
|
||||
"sections": section_labels,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for i in [1, 2]:
|
||||
path = ROOT / "ejemplos" / f"ejemplo{i}.mp3"
|
||||
result = analyze_track(str(path), f"ejemplo{i}.mp3")
|
||||
@@ -12,6 +12,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -26,6 +27,7 @@ from src.core.schema import (
|
||||
from src.composer.rhythm import get_notes, GENERATORS as RHYTHM_GENERATORS
|
||||
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
|
||||
from src.composer.converters import rhythm_to_midi, melodic_to_midi
|
||||
from src.composer.patterns import generate_structure
|
||||
from src.selector import SampleSelector
|
||||
from src.reaper_builder import RPPBuilder
|
||||
from src.reaper_builder.render import render_project
|
||||
@@ -67,9 +69,9 @@ ROLE_MELODIC_GENERATORS = {
|
||||
}
|
||||
|
||||
ROLE_RHYTHM_GENERATORS = {
|
||||
"drums": "kick_main_notes",
|
||||
"snare": "snare_verse_notes",
|
||||
"hihat": "hihat_16th_notes",
|
||||
"drums": "kick_pattern_bank_notes",
|
||||
"snare": "snare_pattern_bank_notes",
|
||||
"hihat": "hihat_pattern_bank_notes",
|
||||
"perc": "perc_combo_notes",
|
||||
}
|
||||
|
||||
@@ -197,6 +199,10 @@ def build_section_tracks(
|
||||
selector: SampleSelector,
|
||||
key: str,
|
||||
bpm: float,
|
||||
sections_data: list[dict] | None = None,
|
||||
humanize: float = 0.3,
|
||||
groove_strength: float = 0.3,
|
||||
bank_weights: list[tuple[str, float]] | None = None,
|
||||
) -> tuple[list[TrackDef], list[SectionDef]]:
|
||||
"""Build all tracks from genre config sections.
|
||||
|
||||
@@ -208,17 +214,33 @@ def build_section_tracks(
|
||||
selector: SampleSelector for sample queries
|
||||
key: Musical key (e.g. "Am")
|
||||
bpm: BPM for sample selection
|
||||
sections_data: List of section dicts with 'name', 'bars', 'energy' keys.
|
||||
If None, falls back to reading 'sections' from genre_config.
|
||||
humanize: Humanization amount for melodic generators (0.0-1.0)
|
||||
groove_strength: Groove amount for rhythm generators (0.0-1.0)
|
||||
bank_weights: List of (bank_name, weight) tuples for weighted random bank selection
|
||||
|
||||
Returns:
|
||||
(tracks, sections)
|
||||
"""
|
||||
structure = genre_config.get("structure", {})
|
||||
sections_raw = structure.get("sections", [])
|
||||
roles = genre_config.get("roles", {})
|
||||
|
||||
# Fall back to fixed sections from genre config for backward compatibility
|
||||
if sections_data is None:
|
||||
sections_data = genre_config.get("structure", {}).get("sections", [])
|
||||
|
||||
# Default bank weights for drums — weighted random selection
|
||||
if bank_weights is None:
|
||||
bank_weights = [
|
||||
("dembow_classico", 3),
|
||||
("dense", 3),
|
||||
("perreo", 2),
|
||||
("trapico", 1),
|
||||
]
|
||||
|
||||
# Parse sections into SectionDef list
|
||||
sections: list[SectionDef] = []
|
||||
for s in sections_raw:
|
||||
for s in sections_data:
|
||||
sections.append(SectionDef(
|
||||
name=s.get("name", "unknown"),
|
||||
bars=s.get("bars", 4),
|
||||
@@ -265,7 +287,17 @@ def build_section_tracks(
|
||||
|
||||
if role in ROLE_RHYTHM_GENERATORS:
|
||||
gen_name = ROLE_RHYTHM_GENERATORS[role]
|
||||
note_dict = get_notes(gen_name, section.bars, velocity_mult=vel_mult)
|
||||
# Weighted random bank selection for variation
|
||||
bank_names = [b[0] for b in bank_weights]
|
||||
bank_weight_values = [b[1] for b in bank_weights]
|
||||
bank = random.choices(bank_names, weights=bank_weight_values, k=1)[0]
|
||||
|
||||
note_dict = get_notes(
|
||||
gen_name, section.bars,
|
||||
velocity_mult=vel_mult,
|
||||
bank=bank,
|
||||
groove_strength=groove_strength,
|
||||
)
|
||||
|
||||
# Audio roles: one clip per hit (one-shot samples placed at beat positions)
|
||||
if role in AUDIO_ROLES:
|
||||
@@ -291,7 +323,13 @@ def build_section_tracks(
|
||||
section_clips.append(clip)
|
||||
elif role in ROLE_MELODIC_GENERATORS:
|
||||
gen_fn = ROLE_MELODIC_GENERATORS[role]
|
||||
note_list = gen_fn(key=key, bars=section.bars, velocity_mult=vel_mult)
|
||||
note_list = gen_fn(
|
||||
key=key,
|
||||
bars=section.bars,
|
||||
velocity_mult=vel_mult,
|
||||
section_type=section.name,
|
||||
humanize=humanize,
|
||||
)
|
||||
midi_notes = melodic_to_midi(note_list)
|
||||
# Melodic roles use MIDI instruments — no audio_path needed
|
||||
clip = ClipDef(
|
||||
@@ -377,6 +415,12 @@ def main() -> None:
|
||||
default=None,
|
||||
help="Output WAV path for rendering.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Random seed for reproducible output (default: unseeded for max variation).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate BPM
|
||||
@@ -404,8 +448,15 @@ def main() -> None:
|
||||
|
||||
selector = SampleSelector(str(index_path))
|
||||
|
||||
# Build tracks and sections from genre config
|
||||
tracks, sections = build_section_tracks(genre_config, selector, args.key, args.bpm)
|
||||
# Generate section structure from template with randomization
|
||||
# Note: generate_structure reseeds random internally if seed is provided
|
||||
sections_data = generate_structure(genre_config, args.bpm, args.key, seed=args.seed)
|
||||
|
||||
# Build tracks and sections
|
||||
tracks, sections = build_section_tracks(
|
||||
genre_config, selector, args.key, args.bpm, sections_data,
|
||||
humanize=0.3, groove_strength=0.3,
|
||||
)
|
||||
|
||||
# Create return tracks
|
||||
return_tracks = create_return_tracks()
|
||||
@@ -434,7 +485,7 @@ def main() -> None:
|
||||
print(f" - {e}", file=sys.stderr)
|
||||
|
||||
# Write .rpp
|
||||
builder = RPPBuilder(song)
|
||||
builder = RPPBuilder(song, seed=args.seed)
|
||||
builder.write(str(output_path))
|
||||
|
||||
# Render if requested
|
||||
|
||||
242
scripts/quick_drumloop_test.py
Normal file
242
scripts/quick_drumloop_test.py
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python
|
||||
"""Quick test: one drum loop repeated 4 times in track 2.
|
||||
|
||||
This script demonstrates the CORRECT REAPER .rpp format by matching
|
||||
the ground truth from output/all_plugins.rpp and output/test_vst3.rpp.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from rpp import Element
|
||||
import rpp
|
||||
|
||||
|
||||
DRUM_LOOP = r"C:\Users\Administrator\Documents\fl_control\libreria\samples\drumloop\drumloop_D2_099_boomy_f8b5a5.wav"
|
||||
OUTPUT = str(ROOT / "output" / "drumloop_test.rpp")
|
||||
|
||||
|
||||
def build():
|
||||
"""Build a valid REAPER .rpp with drum loop items."""
|
||||
# Project root — matching ground truth format from all_plugins.rpp
|
||||
project = Element("REAPER_PROJECT", ["0.1", "7.65/win64", "12345678901234567890", "0"])
|
||||
|
||||
# Project header from ground truth — tokens match exactly what REAPER expects
|
||||
project.append(Element("NOTES", ["0", "2"]))
|
||||
project.append([])
|
||||
project.append(["RIPPLE", "0", "0"])
|
||||
project.append(["GROUPOVERRIDE", "0", "0", "0", "0"])
|
||||
project.append(["AUTOXFADE", "129"])
|
||||
project.append(["ENVATTACH", "3"])
|
||||
project.append(["POOLEDENVATTACH", "0"])
|
||||
project.append(["TCPUIFLAGS", "0"])
|
||||
project.append(["MIXERUIFLAGS", "11", "48"])
|
||||
project.append(["ENVFADESZ10", "40"])
|
||||
project.append(["PEAKGAIN", "1"])
|
||||
project.append(["FEEDBACK", "0"])
|
||||
project.append(["PANLAW", "1"])
|
||||
project.append(["PROJOFFS", "0", "0", "0"])
|
||||
project.append(["MAXPROJLEN", "0", "0"])
|
||||
project.append(["GRID", "3199", "8", "1", "8", "1", "0", "0", "0"])
|
||||
project.append(["TIMEMODE", "1", "5", "-1", "30", "0", "0", "-1", "0"])
|
||||
project.append(["VIDEO_CONFIG", "0", "0", "65792"])
|
||||
project.append(["PANMODE", "3"])
|
||||
project.append(["PANLAWFLAGS", "3"])
|
||||
project.append(["CURSOR", "0"])
|
||||
project.append(["ZOOM", "100", "0", "0"])
|
||||
project.append(["VZOOMEX", "6", "0"])
|
||||
project.append(["USE_REC_CFG", "0"])
|
||||
project.append(["RECMODE", "1"])
|
||||
project.append(["SMPTESYNC", "0", "30", "100", "40", "1000", "300", "0", "0", "1", "0", "0"])
|
||||
project.append(["LOOP", "0"])
|
||||
project.append(["LOOPGRAN", "0", "4"])
|
||||
project.append(["RECORD_PATH", "Media", ""])
|
||||
project.append(Element("RECORD_CFG", [], children=["ZXZhdxgAAQ=="]))
|
||||
project.append([])
|
||||
project.append(Element("APPLYFX_CFG", [], children=[]))
|
||||
project.append([])
|
||||
project.append(["RENDER_FILE", ""])
|
||||
project.append(["RENDER_PATTERN", ""])
|
||||
project.append(["RENDER_FMT", "0", "2", "0"])
|
||||
project.append(["RENDER_1X", "0"])
|
||||
project.append(["RENDER_RANGE", "1", "0", "0", "0", "1000"])
|
||||
project.append(["RENDER_RESAMPLE", "3", "0", "1"])
|
||||
project.append(["RENDER_ADDTOPROJ", "0"])
|
||||
project.append(["RENDER_STEMS", "0"])
|
||||
project.append(["RENDER_DITHER", "0"])
|
||||
project.append(["RENDER_TRIM", "0.000001", "0.000001", "0", "0"])
|
||||
project.append(["TIMELOCKMODE", "1"])
|
||||
project.append(["TEMPOENVLOCKMODE", "1"])
|
||||
project.append(["ITEMMIX", "1"])
|
||||
project.append(["DEFPITCHMODE", "589824", "0"])
|
||||
project.append(["TAKELANE", "1"])
|
||||
project.append(["SAMPLERATE", "44100", "0", "0"])
|
||||
project.append([])
|
||||
project.append(["LOCK", "1"])
|
||||
project.append(Element("METRONOME", ["6", "2"], children=[
|
||||
["VOL", "0.25", "0.125"],
|
||||
["BEATLEN", "4"],
|
||||
["FREQ", "1760", "880", "1"],
|
||||
["SAMPLES", "", "", "", ""],
|
||||
["SPLIGNORE", "0", "0"],
|
||||
["SPLDEF", "2", "660", "", "0", ""],
|
||||
["SPLDEF", "3", "440", "", "0", ""],
|
||||
["PATTERN", "0", "169"],
|
||||
["PATTERNSTR", "ABBB"],
|
||||
["MULT", "1"],
|
||||
]))
|
||||
project.append([])
|
||||
project.append(["GLOBAL_AUTO", "-1"])
|
||||
# TEMPO with 4 args: bpm, time_sig_num, time_sig_den, flag
|
||||
project.append(["TEMPO", "99", "4", "4", "0"])
|
||||
project.append(["PLAYRATE", "1", "0", "0.25", "4"])
|
||||
project.append(["SELECTION", "0", "0"])
|
||||
project.append(["SELECTION2", "0", "0"])
|
||||
project.append(["MASTERAUTOMODE", "0"])
|
||||
project.append(["MASTERTRACKHEIGHT", "0", "0"])
|
||||
project.append(["MASTERPEAKCOL", "16576"])
|
||||
project.append(["MASTERMUTESOLO", "0"])
|
||||
project.append(["MASTERTRACKVIEW", "0", "0.6667", "0.5", "0.5", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"])
|
||||
project.append(["MASTERHWOUT", "0", "0", "1", "0", "0", "0", "0", "-1"])
|
||||
project.append(["MASTER_NCH", "2", "2"])
|
||||
project.append(["MASTER_VOLUME", "1", "0", "-1", "-1", "1"])
|
||||
project.append(["MASTER_PANMODE", "3"])
|
||||
project.append(["MASTER_PANLAWFLAGS", "3"])
|
||||
project.append(["MASTER_FX", "1"])
|
||||
project.append(["MASTER_SEL", "0"])
|
||||
project.append(Element("MASTERPLAYSPEEDENV", [], children=[
|
||||
["EGUID", "{DEF87440-E07C-4B72-B9F8-D2AC60A0D0AC}"],
|
||||
["ACT", "0", "-1"],
|
||||
["VIS", "0", "1", "1"],
|
||||
["LANEHEIGHT", "0", "0"],
|
||||
["ARM", "0"],
|
||||
["DEFSHAPE", "0", "-1", "-1"],
|
||||
]))
|
||||
project.append([])
|
||||
project.append(Element("TEMPOENVEX", [], children=[
|
||||
["EGUID", "{15E58A72-7149-4783-9A04-838503786012}"],
|
||||
["ACT", "1", "-1"],
|
||||
["VIS", "1", "0", "1"],
|
||||
["LANEHEIGHT", "0", "0"],
|
||||
["ARM", "0"],
|
||||
["DEFSHAPE", "1", "-1", "-1"],
|
||||
]))
|
||||
project.append([])
|
||||
project.append(["RULERHEIGHT", "86", "86"])
|
||||
project.append(["RULERLANE", "1", "4", "", "0", "-1"])
|
||||
project.append(["RULERLANE", "2", "8", "", "0", "-1"])
|
||||
project.append([])
|
||||
|
||||
# Master track
|
||||
master_guid = "{00000000-0000-0000-0000-000000000001}"
|
||||
master = Element("TRACK", [master_guid])
|
||||
master.append(["NAME", "master"])
|
||||
master.append(["VOLPAN", "1", "0", "-1", "-1", "1"])
|
||||
master.append(["PEAKCOL", "16576"])
|
||||
master.append(["BEAT", "-1"])
|
||||
master.append(["AUTOMODE", "0"])
|
||||
master.append(["PANLAWFLAGS", "3"])
|
||||
master.append(["MUTESOLO", "0", "0", "0"])
|
||||
master.append(["IPHASE", "0"])
|
||||
master.append(["PLAYOFFS", "0", "1"])
|
||||
master.append(["ISBUS", "0", "0"])
|
||||
master.append(["BUSCOMP", "0", "0", "0", "0", "0"])
|
||||
master.append(["SHOWINMIX", "1", "0.6667", "0.5", "1", "0.5", "0", "0", "0", "0"])
|
||||
master.append(["FIXEDLANES", "9", "0", "0", "0", "0"])
|
||||
master.append(["SEL", "0"])
|
||||
master.append(["REC", "0", "0", "1", "0", "0", "0", "0", "0"])
|
||||
master.append(["VU", "64"])
|
||||
master.append(["TRACKHEIGHT", "0", "0", "0", "0", "0", "0", "0"])
|
||||
master.append(["INQ", "0", "0", "0", "0.5", "100", "0", "0", "100"])
|
||||
master.append(["NCHAN", "2"])
|
||||
master.append(["FX", "1"])
|
||||
master.append(["TRACKID", f"{{{master_guid}}}"])
|
||||
master.append(["PERF", "0"])
|
||||
master.append(["MIDIOUT", "-1"])
|
||||
master.append(["MAINSEND", "1", "0"])
|
||||
# Master FXCHAIN
|
||||
master_fxchain = Element("FXCHAIN", [])
|
||||
master_fxchain.append(["WNDRECT", "24", "52", "655", "408"])
|
||||
master_fxchain.append(["SHOW", "0"])
|
||||
master_fxchain.append(["LASTSEL", "0"])
|
||||
master_fxchain.append(["DOCKED", "0"])
|
||||
master_fxchain.append(["BYPASS", "0", "0", "0"])
|
||||
master_fxchain.append(["PRESETNAME", "Program 1"])
|
||||
master_fxchain.append(["FLOATPOS", "0", "0", "0", "0"])
|
||||
master_fxchain.append(["FXID", "{A0F6CA8C-99E7-4B1A-8411-CA7201811EAD}"])
|
||||
master.append(master_fxchain)
|
||||
project.append(master)
|
||||
|
||||
# Track 2: Drum Loop
|
||||
track_guid = "{00000000-0000-0000-0000-000000000002}"
|
||||
track = Element("TRACK", [track_guid])
|
||||
track.append(["NAME", "Drum Loop"])
|
||||
track.append(["VOLPAN", "0.85", "0", "-1", "-1", "1"])
|
||||
track.append(["PEAKCOL", "16576"])
|
||||
track.append(["BEAT", "-1"])
|
||||
track.append(["AUTOMODE", "0"])
|
||||
track.append(["PANLAWFLAGS", "3"])
|
||||
track.append(["MUTESOLO", "0", "0", "0"])
|
||||
track.append(["IPHASE", "0"])
|
||||
track.append(["PLAYOFFS", "0", "1"])
|
||||
track.append(["ISBUS", "0", "0"])
|
||||
track.append(["BUSCOMP", "0", "0", "0", "0", "0"])
|
||||
track.append(["SHOWINMIX", "1", "0.6667", "0.5", "1", "0.5", "0", "0", "0", "0"])
|
||||
track.append(["FIXEDLANES", "9", "0", "0", "0", "0"])
|
||||
track.append(["SEL", "1"])
|
||||
track.append(["REC", "0", "0", "1", "0", "0", "0", "0", "0"])
|
||||
track.append(["VU", "64"])
|
||||
track.append(["TRACKHEIGHT", "0", "0", "0", "0", "0", "0", "0"])
|
||||
track.append(["INQ", "0", "0", "0", "0.5", "100", "0", "0", "100"])
|
||||
track.append(["NCHAN", "2"])
|
||||
track.append(["FX", "1"])
|
||||
track.append(["TRACKID", f"{{{track_guid}}}"])
|
||||
track.append(["PERF", "0"])
|
||||
track.append(["MIDIOUT", "-1"])
|
||||
track.append(["MAINSEND", "1", "0"])
|
||||
|
||||
# 4 clips of the drum loop, each 16 beats (4 bars)
|
||||
loop_duration_beats = 16.0
|
||||
|
||||
for i in range(4):
|
||||
position = i * loop_duration_beats
|
||||
item = Element("ITEM", [])
|
||||
item.append(["POSITION", f"{position:.6f}"])
|
||||
item.append(["LENGTH", f"{loop_duration_beats:.6f}"])
|
||||
item.append(["NAME", f"Drum Loop {i+1}"])
|
||||
item.append(["SOFFS", "0.0"])
|
||||
item.append(["PLAYRATE", "1", "0", "0.25", "4"])
|
||||
item.append(["CHANMODE", "0"])
|
||||
item.append(["GUID", f"{{00000000-0000-0000-0000-0000000000{i+10:02X}}}"])
|
||||
item.append(["MUTE", "0", "0"])
|
||||
item.append(["LOOP", "1"])
|
||||
item.append(["COLOR", "0"])
|
||||
|
||||
# Audio source — uses correct <SOURCE WAVE> format
|
||||
source = Element("SOURCE", ["WAVE"])
|
||||
source.append(["FILE", DRUM_LOOP])
|
||||
item.append(source)
|
||||
|
||||
track.append(item)
|
||||
|
||||
project.append(track)
|
||||
|
||||
# Write
|
||||
output_str = rpp.dumps(project)
|
||||
# Quote the version string in the header
|
||||
output_str = output_str.replace(
|
||||
"<REAPER_PROJECT 0.1 7.65/win64",
|
||||
'<REAPER_PROJECT 0.1 "7.65/win64"'
|
||||
)
|
||||
|
||||
Path(OUTPUT).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(OUTPUT).write_text(output_str, encoding="utf-8")
|
||||
print(f"Written: {OUTPUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
@@ -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]
|
||||
"""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)
|
||||
@@ -51,13 +51,13 @@ _PROJECT_HEADER: list[list[str] | Element] = [
|
||||
["SMPTESYNC", "0", "30", "100", "40", "1000", "300", "0", "0", "1", "0", "0"],
|
||||
["LOOP", "0"],
|
||||
["LOOPGRAN", "0", "4"],
|
||||
["RECORD_PATH", '"Media"', '""'],
|
||||
["RECORD_PATH", "Media", ""],
|
||||
Element("RECORD_CFG", [], children=["ZXZhdxgAAQ=="]),
|
||||
[],
|
||||
Element("APPLYFX_CFG", [], children=[]),
|
||||
[],
|
||||
["RENDER_FILE", '""'],
|
||||
["RENDER_PATTERN", '""'],
|
||||
["RENDER_FILE", ""],
|
||||
["RENDER_PATTERN", ""],
|
||||
["RENDER_FMT", "0", "2", "0"],
|
||||
["RENDER_1X", "0"],
|
||||
["RENDER_RANGE", "1", "0", "0", "0", "1000"],
|
||||
@@ -181,8 +181,10 @@ _FXCHAIN_FOOTER: list[list[str]] = [
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_guid() -> str:
|
||||
"""Generate a random REAPER GUID string."""
|
||||
return str(uuid.uuid4()).upper()
|
||||
"""Generate a random REAPER GUID string using random module (seedable)."""
|
||||
# Use random module directly so seeding works
|
||||
import random
|
||||
return str(uuid.UUID(bytes=bytes(random.getrandbits(8) for _ in range(16)), version=4)).upper()
|
||||
|
||||
|
||||
def vst3_element(display_name: str, filename: str, uid_guid: str = "", preset_data: list[str] | None = None) -> Element:
|
||||
@@ -233,8 +235,17 @@ class RPPBuilder:
|
||||
builder.write("output.rpp")
|
||||
"""
|
||||
|
||||
def __init__(self, song: SongDefinition) -> None:
|
||||
def __init__(self, song: SongDefinition, seed: int | None = None) -> None:
|
||||
self.song = song
|
||||
self._seed = seed
|
||||
if seed is not None:
|
||||
import random
|
||||
random.seed(seed)
|
||||
|
||||
def _make_seeded_guid(self) -> str:
|
||||
"""Generate a random REAPER GUID string. Uses seed if provided to RPPBuilder."""
|
||||
import random
|
||||
return str(uuid.UUID(bytes=bytes(random.getrandbits(8) for _ in range(16)), version=4)).upper()
|
||||
|
||||
def write(self, path: str | Path) -> None:
|
||||
"""Serialize the project to a .rpp file at *path*.
|
||||
@@ -255,7 +266,9 @@ class RPPBuilder:
|
||||
m = self.song.meta
|
||||
|
||||
# Project root — version from test_vst3.rpp line 1
|
||||
root = Element("REAPER_PROJECT", ["0.1", "7.65/win64", str(int(uuid.uuid4().time)), "0"])
|
||||
# Use random to make it seedable for reproducible output
|
||||
import random
|
||||
root = Element("REAPER_PROJECT", ["0.1", "7.65/win64", str(random.getrandbits(64)), "0"])
|
||||
|
||||
# Add all static project header lines
|
||||
for line in _PROJECT_HEADER:
|
||||
@@ -263,10 +276,11 @@ class RPPBuilder:
|
||||
root.append(line)
|
||||
|
||||
# TEMPO is injected dynamically (overrides static header)
|
||||
root.append(["TEMPO", str(m.bpm), str(m.time_sig_num), str(m.time_sig_den)])
|
||||
# REAPER format: TEMPO bpm time_sig_num time_sig_den flag (flag=0 for standard)
|
||||
root.append(["TEMPO", str(m.bpm), str(m.time_sig_num), str(m.time_sig_den), "0"])
|
||||
|
||||
# Master track
|
||||
master_guid = _make_guid()
|
||||
master_guid = self._make_seeded_guid()
|
||||
master = Element("TRACK", [master_guid])
|
||||
master.append(['NAME', "master"])
|
||||
master.append(["VOLPAN", "1.0", "0", "-1", "-1", "1"])
|
||||
@@ -285,7 +299,7 @@ class RPPBuilder:
|
||||
if line:
|
||||
footer_copy = [v for v in line]
|
||||
if footer_copy[0] == "FXID":
|
||||
footer_copy[1] = f"{{{_make_guid()}}}"
|
||||
footer_copy[1] = f"{{{self._make_seeded_guid()}}}"
|
||||
master_fxchain.append(footer_copy)
|
||||
master.append(master_fxchain)
|
||||
root.append(master)
|
||||
@@ -298,7 +312,7 @@ class RPPBuilder:
|
||||
|
||||
def _build_track(self, track: TrackDef) -> Element:
|
||||
"""Build a TRACK Element with all default attributes from test_vst3.rpp."""
|
||||
track_guid = _make_guid()
|
||||
track_guid = self._make_seeded_guid()
|
||||
track_elem = Element("TRACK", [f"{{{track_guid}}}"])
|
||||
track_elem.append(["NAME", track.name])
|
||||
|
||||
@@ -332,7 +346,7 @@ class RPPBuilder:
|
||||
fxchain.append([v for v in line])
|
||||
for plugin in track.plugins:
|
||||
fxchain.append(self._build_plugin(plugin))
|
||||
fxid_guid = _make_guid()
|
||||
fxid_guid = self._make_seeded_guid()
|
||||
fxchain.append(["PRESETNAME", "Program 1"])
|
||||
fxchain.append(["FLOATPOS", "0", "0", "0", "0"])
|
||||
fxchain.append(["FXID", f"{{{fxid_guid}}}"])
|
||||
|
||||
@@ -16,6 +16,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass, field
|
||||
@@ -306,10 +307,27 @@ class SampleSelector:
|
||||
matches.sort(key=lambda m: m.score, reverse=True)
|
||||
return matches[:limit]
|
||||
|
||||
def select_one(self, role: str, **kwargs) -> Optional[dict]:
|
||||
"""Select the single best matching sample."""
|
||||
results = self.select(role=role, limit=1, **kwargs)
|
||||
return results[0].sample if results else None
|
||||
def select_one(
|
||||
self,
|
||||
role: str,
|
||||
seed: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> Optional[dict]:
|
||||
"""Select one sample using weighted random from top-5 candidates.
|
||||
|
||||
The top-5 candidates are selected with weights [5, 4, 3, 2, 1],
|
||||
favoring higher-scored results while allowing variation across calls.
|
||||
Pass seed for reproducible output.
|
||||
"""
|
||||
if seed is not None:
|
||||
random.seed(seed)
|
||||
results = self.select(role=role, limit=5, **kwargs)
|
||||
if not results:
|
||||
return None
|
||||
candidates = results[:5]
|
||||
weights = [5, 4, 3, 2, 1][: len(candidates)]
|
||||
selected = random.choices(candidates, weights=weights, k=1)[0]
|
||||
return selected.sample
|
||||
|
||||
def get_roles(self) -> list[str]:
|
||||
"""Get all available roles and their counts."""
|
||||
|
||||
@@ -200,14 +200,17 @@ class TestSectionBuilderIntegration:
|
||||
index_path = _ROOT / "data" / "sample_index.json"
|
||||
selector = SampleSelector(str(index_path))
|
||||
|
||||
tracks, sections = build_section_tracks(genre_config, selector, "Am", 95.0)
|
||||
# Pass explicit sections_data since JSON now uses templates format
|
||||
sections_data = genre_config.get("structure", {}).get("templates", {}).get("extracted_real_tracks", [])
|
||||
tracks, sections = build_section_tracks(genre_config, selector, "Am", 95.0, sections_data=sections_data)
|
||||
|
||||
assert len(tracks) > 0, "Expected at least one track"
|
||||
assert len(sections) > 0, "Expected at least one section"
|
||||
# Sections should have names
|
||||
# Sections should have names from the genre config
|
||||
valid_names = {"intro", "verse", "build", "pre_chorus", "chorus", "drop",
|
||||
"break", "gap", "bridge", "outro", "verse2", "chorus2", "chorus3"}
|
||||
for sec in sections:
|
||||
assert sec.name in ["intro", "verse", "chorus", "outro",
|
||||
"verse2", "chorus2", "bridge", "chorus3"]
|
||||
assert sec.name in valid_names, f"Unexpected section name: {sec.name}"
|
||||
|
||||
def test_song_definition_has_sections_field(self):
|
||||
"""SongDefinition has a sections field."""
|
||||
|
||||
Reference in New Issue
Block a user