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",
|
"genre": "reggaeton",
|
||||||
"era": "2009",
|
"era": "2009",
|
||||||
"display_name": "Reggaeton 2009 (Era de Oro)",
|
"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": {
|
"bpm": {
|
||||||
"min": 88,
|
"min": 88,
|
||||||
"max": 102,
|
"max": 102,
|
||||||
"default": 96
|
"default": 99
|
||||||
},
|
},
|
||||||
"keys": ["Am", "Dm", "Gm", "Cm", "Em", "Fm", "Bbm"],
|
"keys": ["Am", "Dm", "Gm", "Cm", "Em", "Fm", "Bbm"],
|
||||||
"time_signature": [4, 4],
|
"time_signature": [4, 4],
|
||||||
"ppq": 96,
|
"ppq": 96,
|
||||||
"structure": {
|
"structure": {
|
||||||
"template": "intro-verse-chorus-verse-chorus-outro",
|
"template": "extracted_real_tracks",
|
||||||
"sections": [
|
"templates": {
|
||||||
{"name": "intro", "bars": 4, "energy": 0.3},
|
"extracted_real_tracks": [
|
||||||
{"name": "verse", "bars": 8, "energy": 0.6},
|
{"name": "build", "bars": 16, "energy": 0.6},
|
||||||
{"name": "chorus", "bars": 8, "energy": 0.9},
|
{"name": "verse", "bars": 2, "energy": 0.35},
|
||||||
{"name": "verse2", "bars": 8, "energy": 0.6},
|
{"name": "build", "bars": 47, "energy": 0.65},
|
||||||
{"name": "chorus2", "bars": 8, "energy": 1.0},
|
{"name": "verse", "bars": 1, "energy": 0.35},
|
||||||
{"name": "bridge", "bars": 4, "energy": 0.5},
|
{"name": "gap", "bars": 1, "energy": 0.05},
|
||||||
{"name": "chorus3", "bars": 8, "energy": 1.0},
|
{"name": "drop", "bars": 2, "energy": 1.0},
|
||||||
{"name": "outro", "bars": 4, "energy": 0.4}
|
{"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": {
|
"roles": {
|
||||||
"drums": {
|
"drums": {
|
||||||
"description": "Patron dembow - kick en 1 y 2.5, snare en 2 y 4, hi-hats en corcheas",
|
"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": "dembow",
|
"pattern_type": "drum_loop",
|
||||||
"preferred_plugins": ["FPC", "Fruity DrumSynth Live", "DirectWave", "Kontakt 7"],
|
"preferred_plugins": ["FPC", "Fruity DrumSynth Live", "DirectWave", "Kontakt 7"],
|
||||||
"midi_channel": 0,
|
"midi_channel": 0,
|
||||||
"mixer_slot": 0,
|
"mixer_slot": 0,
|
||||||
"notes_template": "dembow"
|
"notes_template": "drum_loop_dembow",
|
||||||
|
"bank": "dembow_classico"
|
||||||
},
|
},
|
||||||
"bass": {
|
"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",
|
"pattern_type": "808_follow_kick",
|
||||||
"preferred_plugins": ["Serum 2", "Transistor Bass", "Sytrus", "3x Osc", "ravity(S)"],
|
"preferred_plugins": ["Serum 2", "Transistor Bass", "Sytrus", "3x Osc", "ravity(S)"],
|
||||||
"midi_channel": 1,
|
"midi_channel": 1,
|
||||||
"mixer_slot": 1,
|
"mixer_slot": 1,
|
||||||
"octave": 2,
|
"octave": 2,
|
||||||
"notes_template": "bass_808"
|
"notes_template": "bass_tresillo_section_aware"
|
||||||
},
|
},
|
||||||
"harmony": {
|
"harmony": {
|
||||||
"description": "Piano stabs en offbeats. Closed triads.",
|
"description": "Piano stabs in offbeats. Closed triads. Section-aware velocity.",
|
||||||
"pattern_type": "piano_stabs",
|
"pattern_type": "piano_stabs",
|
||||||
"preferred_plugins": ["FL Keys", "Nexus2", "Kontakt 7", "Sakura", "Pigments"],
|
"preferred_plugins": ["FL Keys", "Nexus2", "Kontakt 7", "Sakura", "Pigments"],
|
||||||
"midi_channel": 2,
|
"midi_channel": 2,
|
||||||
"mixer_slot": 2,
|
"mixer_slot": 2,
|
||||||
"notes_template": "piano_stabs"
|
"notes_template": "piano_stabs_section_aware"
|
||||||
},
|
},
|
||||||
"lead": {
|
"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",
|
"pattern_type": "brass_hook",
|
||||||
"preferred_plugins": ["Serum 2", "Omnisphere", "Harmor", "Electra", "ravity(S)"],
|
"preferred_plugins": ["Serum 2", "Omnisphere", "Harmor", "Electra", "ravity(S)"],
|
||||||
"midi_channel": 3,
|
"midi_channel": 3,
|
||||||
"mixer_slot": 3,
|
"mixer_slot": 3,
|
||||||
"notes_template": "lead_hook"
|
"notes_template": "lead_hook_section_aware"
|
||||||
},
|
},
|
||||||
"pad": {
|
"pad": {
|
||||||
"description": "Pad atmosferico sutil para llenar el fondo.",
|
"description": "Pad atmosferico sutil para llenar el fondo. Sustained chord tones.",
|
||||||
"pattern_type": "sustained_pad",
|
"pattern_type": "sustained_pad",
|
||||||
"preferred_plugins": ["Harmor", "Serum 2", "Omnisphere", "FLEX", "Pigments"],
|
"preferred_plugins": ["Harmor", "Serum 2", "Omnisphere", "FLEX", "Pigments"],
|
||||||
"midi_channel": 4,
|
"midi_channel": 4,
|
||||||
@@ -83,7 +109,7 @@
|
|||||||
"popularity": 0.9
|
"popularity": 0.9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tensión",
|
"name": "tension",
|
||||||
"chords": ["Am", "F", "C", "G"],
|
"chords": ["Am", "F", "C", "G"],
|
||||||
"beats_per_chord": 4,
|
"beats_per_chord": 4,
|
||||||
"popularity": 0.7
|
"popularity": 0.7
|
||||||
@@ -152,5 +178,11 @@
|
|||||||
"Rumor de Guerra - Hector El Father",
|
"Rumor de Guerra - Hector El Father",
|
||||||
"Pose - Daddy Yankee",
|
"Pose - Daddy Yankee",
|
||||||
"Llamé Pa Verte - Wisin y Yandel"
|
"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 argparse
|
||||||
import json
|
import json
|
||||||
|
import random
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
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.rhythm import get_notes, GENERATORS as RHYTHM_GENERATORS
|
||||||
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
|
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.converters import rhythm_to_midi, melodic_to_midi
|
||||||
|
from src.composer.patterns import generate_structure
|
||||||
from src.selector import SampleSelector
|
from src.selector import SampleSelector
|
||||||
from src.reaper_builder import RPPBuilder
|
from src.reaper_builder import RPPBuilder
|
||||||
from src.reaper_builder.render import render_project
|
from src.reaper_builder.render import render_project
|
||||||
@@ -67,9 +69,9 @@ ROLE_MELODIC_GENERATORS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ROLE_RHYTHM_GENERATORS = {
|
ROLE_RHYTHM_GENERATORS = {
|
||||||
"drums": "kick_main_notes",
|
"drums": "kick_pattern_bank_notes",
|
||||||
"snare": "snare_verse_notes",
|
"snare": "snare_pattern_bank_notes",
|
||||||
"hihat": "hihat_16th_notes",
|
"hihat": "hihat_pattern_bank_notes",
|
||||||
"perc": "perc_combo_notes",
|
"perc": "perc_combo_notes",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +199,10 @@ def build_section_tracks(
|
|||||||
selector: SampleSelector,
|
selector: SampleSelector,
|
||||||
key: str,
|
key: str,
|
||||||
bpm: float,
|
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]]:
|
) -> tuple[list[TrackDef], list[SectionDef]]:
|
||||||
"""Build all tracks from genre config sections.
|
"""Build all tracks from genre config sections.
|
||||||
|
|
||||||
@@ -208,17 +214,33 @@ def build_section_tracks(
|
|||||||
selector: SampleSelector for sample queries
|
selector: SampleSelector for sample queries
|
||||||
key: Musical key (e.g. "Am")
|
key: Musical key (e.g. "Am")
|
||||||
bpm: BPM for sample selection
|
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:
|
Returns:
|
||||||
(tracks, sections)
|
(tracks, sections)
|
||||||
"""
|
"""
|
||||||
structure = genre_config.get("structure", {})
|
|
||||||
sections_raw = structure.get("sections", [])
|
|
||||||
roles = genre_config.get("roles", {})
|
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
|
# Parse sections into SectionDef list
|
||||||
sections: list[SectionDef] = []
|
sections: list[SectionDef] = []
|
||||||
for s in sections_raw:
|
for s in sections_data:
|
||||||
sections.append(SectionDef(
|
sections.append(SectionDef(
|
||||||
name=s.get("name", "unknown"),
|
name=s.get("name", "unknown"),
|
||||||
bars=s.get("bars", 4),
|
bars=s.get("bars", 4),
|
||||||
@@ -265,7 +287,17 @@ def build_section_tracks(
|
|||||||
|
|
||||||
if role in ROLE_RHYTHM_GENERATORS:
|
if role in ROLE_RHYTHM_GENERATORS:
|
||||||
gen_name = ROLE_RHYTHM_GENERATORS[role]
|
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)
|
# Audio roles: one clip per hit (one-shot samples placed at beat positions)
|
||||||
if role in AUDIO_ROLES:
|
if role in AUDIO_ROLES:
|
||||||
@@ -291,7 +323,13 @@ def build_section_tracks(
|
|||||||
section_clips.append(clip)
|
section_clips.append(clip)
|
||||||
elif role in ROLE_MELODIC_GENERATORS:
|
elif role in ROLE_MELODIC_GENERATORS:
|
||||||
gen_fn = ROLE_MELODIC_GENERATORS[role]
|
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)
|
midi_notes = melodic_to_midi(note_list)
|
||||||
# Melodic roles use MIDI instruments — no audio_path needed
|
# Melodic roles use MIDI instruments — no audio_path needed
|
||||||
clip = ClipDef(
|
clip = ClipDef(
|
||||||
@@ -377,6 +415,12 @@ def main() -> None:
|
|||||||
default=None,
|
default=None,
|
||||||
help="Output WAV path for rendering.",
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Validate BPM
|
# Validate BPM
|
||||||
@@ -404,8 +448,15 @@ def main() -> None:
|
|||||||
|
|
||||||
selector = SampleSelector(str(index_path))
|
selector = SampleSelector(str(index_path))
|
||||||
|
|
||||||
# Build tracks and sections from genre config
|
# Generate section structure from template with randomization
|
||||||
tracks, sections = build_section_tracks(genre_config, selector, args.key, args.bpm)
|
# 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
|
# Create return tracks
|
||||||
return_tracks = create_return_tracks()
|
return_tracks = create_return_tracks()
|
||||||
@@ -434,7 +485,7 @@ def main() -> None:
|
|||||||
print(f" - {e}", file=sys.stderr)
|
print(f" - {e}", file=sys.stderr)
|
||||||
|
|
||||||
# Write .rpp
|
# Write .rpp
|
||||||
builder = RPPBuilder(song)
|
builder = RPPBuilder(song, seed=args.seed)
|
||||||
builder.write(str(output_path))
|
builder.write(str(output_path))
|
||||||
|
|
||||||
# Render if requested
|
# 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.
|
Designed to feed MelodicTrack notes in SongDefinition.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -11,10 +16,10 @@ import random
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
SCALES = {
|
SCALES = {
|
||||||
"minor": [0, 2, 3, 5, 7, 8, 10], # natural minor
|
"minor": [0, 2, 3, 5, 7, 8, 10], # natural minor
|
||||||
"major": [0, 2, 4, 5, 7, 9, 11],
|
"major": [0, 2, 4, 5, 7, 9, 11],
|
||||||
"phrygian": [0, 1, 3, 5, 7, 8, 10],
|
"phrygian": [0, 1, 3, 5, 7, 8, 10],
|
||||||
"dorian": [0, 2, 3, 5, 7, 9, 10],
|
"dorian": [0, 2, 3, 5, 7, 9, 10],
|
||||||
}
|
}
|
||||||
|
|
||||||
ROOT_SEMITONE = {
|
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
|
# Internal helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -36,7 +69,6 @@ def _parse_key(key_str: str) -> tuple[int, str]:
|
|||||||
else:
|
else:
|
||||||
root_str = key_str
|
root_str = key_str
|
||||||
scale_name = "major"
|
scale_name = "major"
|
||||||
|
|
||||||
root = ROOT_SEMITONE.get(root_str)
|
root = ROOT_SEMITONE.get(root_str)
|
||||||
if root is None:
|
if root is None:
|
||||||
raise ValueError(f"Unknown root: {root_str}")
|
raise ValueError(f"Unknown root: {root_str}")
|
||||||
@@ -54,7 +86,7 @@ def _clamp_vel(v: int) -> int:
|
|||||||
return max(1, min(127, v))
|
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."""
|
"""Apply humanization (velocity jitter + position nudge) to note list."""
|
||||||
if humanize <= 0:
|
if humanize <= 0:
|
||||||
return notes
|
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(
|
def bass_tresillo(
|
||||||
@@ -76,42 +108,173 @@ def bass_tresillo(
|
|||||||
octave: int = 3,
|
octave: int = 3,
|
||||||
velocity_mult: float = 1.0,
|
velocity_mult: float = 1.0,
|
||||||
humanize: float = 0.0,
|
humanize: float = 0.0,
|
||||||
|
section_type: str = "verse",
|
||||||
) -> list[dict]:
|
) -> 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
|
The 3-3-2 grouping is the BASS DNA of reggaetón:
|
||||||
Root note on downbeats (0.0, 1.5, 3.0), fifth (7 semitones) on upbeats.
|
- 3 notes in first group (positions 0.0, 0.75, 1.5)
|
||||||
Velocity: 110 for downbeats, 85 for upbeats.
|
- 3 notes in second group (positions 2.25, 3.0, 3.75)
|
||||||
Default octave=3 gives root in MIDI range 45-52 (A3-E4), within 36-55.
|
- 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)
|
root, scale = _parse_key(key)
|
||||||
scale_notes = _get_scale_notes(root, scale, octave)
|
root_note = root + (octave - 1) * 12 # octave 3 → MIDI 45 for A
|
||||||
root_note = scale_notes[0] # degree 0
|
fifth_note = root_note + 7 # perfect fifth above root
|
||||||
fifth_note = root_note + 7 # up a perfect fifth
|
|
||||||
|
# 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] = []
|
notes: list[dict] = []
|
||||||
for b in range(bars):
|
for b in range(bars):
|
||||||
o = b * 4.0
|
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]
|
positions = [0.0, 0.75, 1.5, 2.25, 3.0, 3.75]
|
||||||
for idx, pos in enumerate(positions):
|
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
|
key_note = root_note
|
||||||
vel = 110
|
vel = vel_root
|
||||||
else: # upbeats: fifth
|
else: # upbeats: fifth (positions 1, 3, 5)
|
||||||
key_note = fifth_note
|
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))
|
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)
|
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(
|
def lead_hook(
|
||||||
key: str,
|
key: str,
|
||||||
bars: int,
|
bars: int,
|
||||||
@@ -119,54 +282,89 @@ def lead_hook(
|
|||||||
density: float = 0.6,
|
density: float = 0.6,
|
||||||
velocity_mult: float = 1.0,
|
velocity_mult: float = 1.0,
|
||||||
humanize: float = 0.0,
|
humanize: float = 0.0,
|
||||||
|
section_type: str = "verse",
|
||||||
) -> list[dict]:
|
) -> 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]
|
The hook emphasizes the DEMBOW syncopation — notes on beats 2, 3&, 4
|
||||||
Note durations: 0.5 or 1.0 beats.
|
(positions 4.0, 11.0, 12.0 from the real-track analysis).
|
||||||
density=1.0 → every slot filled; density=0.5 → half filled.
|
|
||||||
|
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)
|
root, scale = _parse_key(key)
|
||||||
intervals = SCALES.get(scale, SCALES["major"])
|
intervals = SCALES.get(scale, SCALES["major"])
|
||||||
|
|
||||||
# Map scale degrees to MIDI notes (extend to cover octave 5 and 6 for melody)
|
# Build scale notes for octave 5 and 6
|
||||||
scale_notes_oct5 = _get_scale_notes(root, scale, octave) # 7 notes
|
scale_notes_oct5 = _get_scale_notes(root, scale, octave) # 7 notes
|
||||||
scale_notes_oct6 = _get_scale_notes(root, scale, octave + 1)
|
scale_notes_oct6 = _get_scale_notes(root, scale, octave + 1)
|
||||||
|
|
||||||
# Degree pattern (0-indexed scale degrees)
|
# Section-aware adjustments
|
||||||
degrees = [0, 2, 4, 2, 3, 1, 0, 2, 4, 5, 4, 2, 0]
|
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] = []
|
notes: list[dict] = []
|
||||||
|
|
||||||
# Step through the pattern at half-beat intervals
|
# Dembow accent positions from real track analysis: 4, 8, 11, 12
|
||||||
# density controls whether we actually place a note
|
# These are beats 2, 3, 3&, 4 in 16th-note positions
|
||||||
step = max(1, round(1.0 / density)) if density > 0 else 1
|
dembow_positions = [4.0, 8.0, 11.0, 12.0]
|
||||||
|
|
||||||
pos = 0.0
|
pos = 0.0
|
||||||
degree_idx = 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:
|
while pos < bars * 4.0:
|
||||||
slot = int(pos * 2) # 0.5-beat slots
|
slot_half = 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]
|
|
||||||
|
|
||||||
# Duration: 1.0 beat on strong beats (quarter), 0.5 elsewhere
|
if slot_half % step == 0:
|
||||||
is_strong = (slot % 4 == 0)
|
# Check if this position is a dembow accent position
|
||||||
length = 1.0 if is_strong else 0.5
|
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))
|
vel = _clamp_vel(int(vel * velocity_mult))
|
||||||
|
|
||||||
notes.append({"pos": pos, "len": length, "key": midi_note, "vel": vel})
|
notes.append({"pos": pos, "len": length, "key": midi_note, "vel": vel})
|
||||||
|
|
||||||
# Advance degree index
|
degree_idx = (degree_idx + 1) % len(_HOOK_DEGREES)
|
||||||
degree_idx = (degree_idx + 1) % len(degrees)
|
|
||||||
if is_strong:
|
# Advance by length
|
||||||
pos += 1.0
|
pos += 0.5 # fixed 8th-note step
|
||||||
else:
|
|
||||||
pos += 0.5
|
|
||||||
else:
|
else:
|
||||||
pos += 0.5
|
pos += 0.5
|
||||||
|
|
||||||
@@ -174,85 +372,7 @@ def lead_hook(
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Chords: block chords
|
# Pad: sustain following chord progression
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
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
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def pad_sustain(
|
def pad_sustain(
|
||||||
@@ -261,46 +381,53 @@ def pad_sustain(
|
|||||||
octave: int = 4,
|
octave: int = 4,
|
||||||
velocity_mult: float = 1.0,
|
velocity_mult: float = 1.0,
|
||||||
humanize: float = 0.0,
|
humanize: float = 0.0,
|
||||||
|
section_type: str = "verse",
|
||||||
) -> list[dict]:
|
) -> 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.
|
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)
|
root, scale = _parse_key(key)
|
||||||
|
|
||||||
if scale == "minor":
|
# Section-aware velocity
|
||||||
chord_intervals = [
|
section = section_type.lower()
|
||||||
(0, 3, 7),
|
if section in ("chorus", "drop"):
|
||||||
(10, 1, 5),
|
base_vel = 75
|
||||||
(8, 0, 4),
|
elif section in ("verse", "bridge"):
|
||||||
(10, 1, 5),
|
base_vel = 55
|
||||||
]
|
elif section in ("break", "gap"):
|
||||||
root_notes_per_bar = [0, 10, 8, 10] # root semitone offsets per bar
|
base_vel = 25
|
||||||
|
elif section == "intro":
|
||||||
|
base_vel = 45
|
||||||
else:
|
else:
|
||||||
chord_intervals = [
|
base_vel = 65
|
||||||
(0, 4, 7),
|
|
||||||
(7, 11, 2),
|
# Use classic_minor progression
|
||||||
(9, 0, 4),
|
prog_data = _CHORD_INTERVALLS.get("classic_minor", _CHORD_INTERVALLS["classic_minor"])
|
||||||
(5, 9, 0),
|
root_offsets = prog_data["root_offsets"] # [0, 8, 4, 10]
|
||||||
]
|
|
||||||
root_notes_per_bar = [0, 7, 9, 5]
|
|
||||||
|
|
||||||
notes: list[dict] = []
|
notes: list[dict] = []
|
||||||
for b in range(bars):
|
for b in range(bars):
|
||||||
o = b * 4.0
|
o = b * 4.0
|
||||||
cycle = b % 4
|
chord_idx = b % 4
|
||||||
root_interval = root_notes_per_bar[cycle]
|
root_interval = root_offsets[chord_idx]
|
||||||
midi_note = root + octave * 12 + root_interval
|
midi_note = root + octave * 12 + root_interval
|
||||||
|
|
||||||
vel = 70
|
vel = _clamp_vel(int(base_vel * velocity_mult))
|
||||||
vel = _clamp_vel(int(vel * velocity_mult))
|
|
||||||
notes.append({
|
notes.append({
|
||||||
"pos": o,
|
"pos": o,
|
||||||
"len": 3.5,
|
"len": 3.5, # sustained for most of the bar
|
||||||
"key": midi_note,
|
"key": midi_note,
|
||||||
"vel": vel,
|
"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
|
import random
|
||||||
|
|
||||||
@@ -19,38 +30,141 @@ CH_CL = 16 # clap.wav
|
|||||||
# key — always 60 for drum samples (pitch irrelevant, sample just plays)
|
# key — always 60 for drum samples (pitch irrelevant, sample just plays)
|
||||||
# vel — 1–127 after applying velocity_mult
|
# 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
|
# 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.
|
"""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.
|
groove_strength: 0.0 = no effect, 1.0 = maximum groove feel.
|
||||||
"""
|
"""
|
||||||
if groove_strength <= 0:
|
if groove_strength <= 0:
|
||||||
return notes
|
return note_data
|
||||||
jitter = 5 + groove_strength * 10
|
jitter = 5 + groove_strength * 10
|
||||||
nudge = groove_strength * 0.02
|
nudge = groove_strength * 0.02
|
||||||
for n in notes:
|
|
||||||
n["vel"] = max(1, min(127, n["vel"] + random.uniform(-jitter, jitter)))
|
# Normalize to {channel: [notes]} format
|
||||||
n["pos"] = max(0, n["pos"] + random.uniform(-nudge, nudge))
|
if isinstance(note_data, dict):
|
||||||
return notes
|
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:
|
def _clamp_vel(vel: int) -> int:
|
||||||
"""Clamp velocity to valid MIDI range [1, 127]."""
|
"""Clamp velocity to valid MIDI range [1, 127]."""
|
||||||
return max(1, min(127, vel))
|
return max(1, min(127, vel))
|
||||||
|
|
||||||
|
|
||||||
def _apply_vel(base_vel: int, velocity_mult: float) -> int:
|
def _apply_vel(base_vel: int, velocity_mult: float) -> int:
|
||||||
"""Multiply base velocity by velocity_mult and clamp."""
|
"""Multiply base velocity by velocity_mult and clamp."""
|
||||||
return _clamp_vel(int(base_vel * velocity_mult))
|
return _clamp_vel(int(base_vel * velocity_mult))
|
||||||
|
|
||||||
|
|
||||||
def _note(pos: float, length: float, vel: int) -> dict:
|
def _note(pos: float, length: float, vel: int) -> dict:
|
||||||
"""Create a note dict with key=60."""
|
"""Create a note dict with key=60."""
|
||||||
return {"pos": pos, "len": length, "key": 60, "vel": vel}
|
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
|
# Kick generators
|
||||||
@@ -62,16 +176,17 @@ def kick_main_notes(
|
|||||||
density: float = 1.0,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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...]}.
|
Returns {CH_K: [notes...]}.
|
||||||
"""
|
"""
|
||||||
notes: list[dict] = []
|
notes: list[dict] = []
|
||||||
for b in range(bars):
|
for b in range(bars):
|
||||||
o = b * 4.0
|
o = b * 4.0
|
||||||
notes.append(_note(o, 0.25, _apply_vel(115, velocity_mult)))
|
for pos, base_vel in KICK_DEMBOW_CLASSICO:
|
||||||
notes.append(_note(o + 1.5, 0.25, _apply_vel(105, velocity_mult)))
|
notes.append(_note(o + pos, 0.25, _apply_vel(base_vel, velocity_mult)))
|
||||||
return _apply_groove({CH_K: notes}, groove_strength)
|
return _apply_groove({CH_K: notes}, groove_strength)
|
||||||
|
|
||||||
|
|
||||||
@@ -81,14 +196,16 @@ def kick_sparse_notes(
|
|||||||
density: float = 1.0,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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...]}.
|
Returns {CH_K: [notes...]}.
|
||||||
"""
|
"""
|
||||||
notes: list[dict] = []
|
notes: list[dict] = []
|
||||||
for b in range(bars):
|
for b in range(bars):
|
||||||
o = b * 4.0
|
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)
|
return _apply_groove({CH_K: notes}, groove_strength)
|
||||||
|
|
||||||
|
|
||||||
@@ -98,9 +215,9 @@ def kick_outro_notes(
|
|||||||
density: float = 1.0,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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...]}.
|
Returns {CH_K: [notes...]}.
|
||||||
"""
|
"""
|
||||||
return kick_main_notes(bars, velocity_mult=velocity_mult * 0.75, density=density, groove_strength=groove_strength)
|
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,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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...]}.
|
Returns {CH_S: [notes...]}.
|
||||||
"""
|
"""
|
||||||
_PATTERN = [(1.0, 100), (2.0, 95), (2.5, 110), (3.0, 90)]
|
|
||||||
notes: list[dict] = []
|
notes: list[dict] = []
|
||||||
for b in range(bars):
|
for b in range(bars):
|
||||||
o = b * 4.0
|
o = b * 4.0
|
||||||
for p, v in _PATTERN:
|
for pos, base_vel in SNARE_DEMBOW:
|
||||||
notes.append(_note(o + p, 0.15, _apply_vel(v, velocity_mult)))
|
notes.append(_note(o + pos, 0.15, _apply_vel(base_vel, velocity_mult)))
|
||||||
return _apply_groove({CH_S: notes}, groove_strength)
|
return _apply_groove({CH_S: notes}, groove_strength)
|
||||||
|
|
||||||
|
|
||||||
@@ -136,24 +253,16 @@ def snare_fill_notes(
|
|||||||
density: float = 1.0,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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...]}.
|
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] = []
|
notes: list[dict] = []
|
||||||
for b in range(bars):
|
for b in range(bars):
|
||||||
o = b * 4.0
|
o = b * 4.0
|
||||||
for p, v in _PATTERN:
|
for pos, base_vel in SNARE_FILLS:
|
||||||
notes.append(_note(o + p, 0.15, _apply_vel(v, velocity_mult)))
|
notes.append(_note(o + pos, 0.15, _apply_vel(base_vel, velocity_mult)))
|
||||||
return _apply_groove({CH_S: notes}, groove_strength)
|
return _apply_groove({CH_S: notes}, groove_strength)
|
||||||
|
|
||||||
|
|
||||||
@@ -163,9 +272,9 @@ def snare_outro_notes(
|
|||||||
density: float = 1.0,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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...]}.
|
Returns {CH_S: [notes...]}.
|
||||||
"""
|
"""
|
||||||
return snare_verse_notes(bars, velocity_mult=velocity_mult * 0.7, density=density, groove_strength=groove_strength)
|
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,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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
|
Accents align with kick/snare positions (beats 1, 2, 3, 3&) to drive the groove.
|
||||||
off-8ths (vel 40). density=1.0 → all 16ths; density=0.5 → every other.
|
density=1.0 → all 16ths; density=0.5 → every other 16th.
|
||||||
Returns {CH_H: [notes...]}.
|
Returns {CH_H: [notes...]}.
|
||||||
"""
|
"""
|
||||||
notes: list[dict] = []
|
notes: list[dict] = []
|
||||||
step = max(1, round(1.0 / density)) if density > 0 else 1
|
step = max(1, round(1.0 / density)) if density > 0 else 1
|
||||||
for b in range(bars):
|
for b in range(bars):
|
||||||
o = b * 4.0
|
o = b * 4.0
|
||||||
for i in range(0, 16, step):
|
for idx, (pos, base_vel) in enumerate(HIHAT_16TH_DEMBOW):
|
||||||
beat_frac = i * 0.25 # position within bar in beats
|
if idx % step == 0:
|
||||||
if beat_frac % 1.0 == 0.0: # quarter note position
|
notes.append(_note(o + pos, 0.1, _apply_vel(base_vel, velocity_mult)))
|
||||||
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)))
|
|
||||||
return _apply_groove({CH_H: notes}, groove_strength)
|
return _apply_groove({CH_H: notes}, groove_strength)
|
||||||
|
|
||||||
|
|
||||||
@@ -209,59 +312,65 @@ def hihat_8th_notes(
|
|||||||
density: float = 1.0,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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...]}.
|
Returns {CH_H: [notes...]}.
|
||||||
"""
|
"""
|
||||||
notes: list[dict] = []
|
notes: list[dict] = []
|
||||||
for b in range(bars):
|
for b in range(bars):
|
||||||
o = b * 4.0
|
o = b * 4.0
|
||||||
for i in range(8):
|
for pos, base_vel in HIHAT_OFFBEAT:
|
||||||
base_vel = 70 if i % 2 == 0 else 50
|
notes.append(_note(o + pos, 0.1, _apply_vel(base_vel, velocity_mult)))
|
||||||
notes.append(_note(o + i * 0.5, 0.1, _apply_vel(base_vel, velocity_mult)))
|
|
||||||
return _apply_groove({CH_H: notes}, groove_strength)
|
return _apply_groove({CH_H: notes}, groove_strength)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Clap generator
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def clap_24_notes(
|
def clap_24_notes(
|
||||||
bars: int,
|
bars: int,
|
||||||
velocity_mult: float = 1.0,
|
velocity_mult: float = 1.0,
|
||||||
density: float = 1.0,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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...]}.
|
Returns {CH_CL: [notes...]}.
|
||||||
"""
|
"""
|
||||||
notes: list[dict] = []
|
notes: list[dict] = []
|
||||||
for b in range(bars):
|
for b in range(bars):
|
||||||
o = b * 4.0
|
o = b * 4.0
|
||||||
notes.append(_note(o + 1.0, 0.15, _apply_vel(120, velocity_mult)))
|
for pos, base_vel in CLAP_DEMBOW:
|
||||||
notes.append(_note(o + 3.0, 0.15, _apply_vel(120, velocity_mult)))
|
notes.append(_note(o + pos, 0.15, _apply_vel(base_vel, velocity_mult)))
|
||||||
return _apply_groove({CH_CL: notes}, groove_strength)
|
return _apply_groove({CH_CL: notes}, groove_strength)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Percussion generators
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def perc_combo_notes(
|
def perc_combo_notes(
|
||||||
bars: int,
|
bars: int,
|
||||||
velocity_mult: float = 1.0,
|
velocity_mult: float = 1.0,
|
||||||
density: float = 1.0,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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).
|
perc2 (CH_P2): conga slap and open tone
|
||||||
perc1 (CH_P1): positions 1.5 (vel 70) and 3.5 (vel 65).
|
perc1 (CH_P1): reserved for additional perc (not used in base pattern)
|
||||||
Returns {CH_P1: [...], CH_P2: [...]}.
|
Returns {CH_P2: [...], CH_P1: [...]}.
|
||||||
"""
|
"""
|
||||||
p2_notes: list[dict] = []
|
p2_notes: list[dict] = []
|
||||||
p1_notes: list[dict] = []
|
p1_notes: list[dict] = []
|
||||||
for b in range(bars):
|
for b in range(bars):
|
||||||
o = b * 4.0
|
o = b * 4.0
|
||||||
p2_notes.append(_note(o + 0.75, 0.1, _apply_vel(85, velocity_mult)))
|
for pos, base_vel in CONGAS_DEMBOW:
|
||||||
p2_notes.append(_note(o + 2.75, 0.1, _apply_vel(80, velocity_mult)))
|
p2_notes.append(_note(o + pos, 0.1, _apply_vel(base_vel, 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)))
|
|
||||||
return _apply_groove({CH_P1: p1_notes, CH_P2: p2_notes}, groove_strength)
|
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,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
) -> dict[int, list[dict]]:
|
) -> 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=0: sparse (positions 0, 2, 8, 14 in 16ths)
|
||||||
Bar N%4=1: indices 0,2,4,8,10,14 (filling in)
|
Bar N%4=1: filling in (positions 0, 2, 4, 8, 10, 14)
|
||||||
Bar N%4=2: indices 0,2,4,6,8,10,12,14 (every other 16th)
|
Bar N%4=2: every other 16th (positions 0, 2, 4, 6, 8, 10, 12, 14)
|
||||||
Bar N%4=3: all 16 indices (full roll)
|
Bar N%4=3: full roll (all 16 positions)
|
||||||
|
Velocity ramps: 50 → 65 → 80 → 100 across the cycle.
|
||||||
Velocity ramps: 50 → 65 → 80 → 100 across the 4-bar cycle.
|
|
||||||
Returns {CH_R: [notes...]}.
|
Returns {CH_R: [notes...]}.
|
||||||
"""
|
"""
|
||||||
_PATTERNS = [
|
_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(
|
def get_notes(
|
||||||
generator_name: str,
|
generator_name: str,
|
||||||
bars: int,
|
bars: int,
|
||||||
velocity_mult: float = 1.0,
|
velocity_mult: float = 1.0,
|
||||||
density: float = 1.0,
|
density: float = 1.0,
|
||||||
groove_strength: float = 0.0,
|
groove_strength: float = 0.0,
|
||||||
|
bank: str = "dembow_classico",
|
||||||
) -> dict[int, list[dict]]:
|
) -> dict[int, list[dict]]:
|
||||||
"""Dispatch to the named generator. Raises KeyError if not found."""
|
"""Dispatch to the named generator.
|
||||||
gen = GENERATORS[generator_name]
|
|
||||||
|
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)
|
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"],
|
["SMPTESYNC", "0", "30", "100", "40", "1000", "300", "0", "0", "1", "0", "0"],
|
||||||
["LOOP", "0"],
|
["LOOP", "0"],
|
||||||
["LOOPGRAN", "0", "4"],
|
["LOOPGRAN", "0", "4"],
|
||||||
["RECORD_PATH", '"Media"', '""'],
|
["RECORD_PATH", "Media", ""],
|
||||||
Element("RECORD_CFG", [], children=["ZXZhdxgAAQ=="]),
|
Element("RECORD_CFG", [], children=["ZXZhdxgAAQ=="]),
|
||||||
[],
|
[],
|
||||||
Element("APPLYFX_CFG", [], children=[]),
|
Element("APPLYFX_CFG", [], children=[]),
|
||||||
[],
|
[],
|
||||||
["RENDER_FILE", '""'],
|
["RENDER_FILE", ""],
|
||||||
["RENDER_PATTERN", '""'],
|
["RENDER_PATTERN", ""],
|
||||||
["RENDER_FMT", "0", "2", "0"],
|
["RENDER_FMT", "0", "2", "0"],
|
||||||
["RENDER_1X", "0"],
|
["RENDER_1X", "0"],
|
||||||
["RENDER_RANGE", "1", "0", "0", "0", "1000"],
|
["RENDER_RANGE", "1", "0", "0", "0", "1000"],
|
||||||
@@ -181,8 +181,10 @@ _FXCHAIN_FOOTER: list[list[str]] = [
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _make_guid() -> str:
|
def _make_guid() -> str:
|
||||||
"""Generate a random REAPER GUID string."""
|
"""Generate a random REAPER GUID string using random module (seedable)."""
|
||||||
return str(uuid.uuid4()).upper()
|
# 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:
|
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")
|
builder.write("output.rpp")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, song: SongDefinition) -> None:
|
def __init__(self, song: SongDefinition, seed: int | None = None) -> None:
|
||||||
self.song = song
|
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:
|
def write(self, path: str | Path) -> None:
|
||||||
"""Serialize the project to a .rpp file at *path*.
|
"""Serialize the project to a .rpp file at *path*.
|
||||||
@@ -255,7 +266,9 @@ class RPPBuilder:
|
|||||||
m = self.song.meta
|
m = self.song.meta
|
||||||
|
|
||||||
# Project root — version from test_vst3.rpp line 1
|
# 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
|
# Add all static project header lines
|
||||||
for line in _PROJECT_HEADER:
|
for line in _PROJECT_HEADER:
|
||||||
@@ -263,10 +276,11 @@ class RPPBuilder:
|
|||||||
root.append(line)
|
root.append(line)
|
||||||
|
|
||||||
# TEMPO is injected dynamically (overrides static header)
|
# 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 track
|
||||||
master_guid = _make_guid()
|
master_guid = self._make_seeded_guid()
|
||||||
master = Element("TRACK", [master_guid])
|
master = Element("TRACK", [master_guid])
|
||||||
master.append(['NAME', "master"])
|
master.append(['NAME', "master"])
|
||||||
master.append(["VOLPAN", "1.0", "0", "-1", "-1", "1"])
|
master.append(["VOLPAN", "1.0", "0", "-1", "-1", "1"])
|
||||||
@@ -285,7 +299,7 @@ class RPPBuilder:
|
|||||||
if line:
|
if line:
|
||||||
footer_copy = [v for v in line]
|
footer_copy = [v for v in line]
|
||||||
if footer_copy[0] == "FXID":
|
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_fxchain.append(footer_copy)
|
||||||
master.append(master_fxchain)
|
master.append(master_fxchain)
|
||||||
root.append(master)
|
root.append(master)
|
||||||
@@ -298,7 +312,7 @@ class RPPBuilder:
|
|||||||
|
|
||||||
def _build_track(self, track: TrackDef) -> Element:
|
def _build_track(self, track: TrackDef) -> Element:
|
||||||
"""Build a TRACK Element with all default attributes from test_vst3.rpp."""
|
"""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 = Element("TRACK", [f"{{{track_guid}}}"])
|
||||||
track_elem.append(["NAME", track.name])
|
track_elem.append(["NAME", track.name])
|
||||||
|
|
||||||
@@ -332,7 +346,7 @@ class RPPBuilder:
|
|||||||
fxchain.append([v for v in line])
|
fxchain.append([v for v in line])
|
||||||
for plugin in track.plugins:
|
for plugin in track.plugins:
|
||||||
fxchain.append(self._build_plugin(plugin))
|
fxchain.append(self._build_plugin(plugin))
|
||||||
fxid_guid = _make_guid()
|
fxid_guid = self._make_seeded_guid()
|
||||||
fxchain.append(["PRESETNAME", "Program 1"])
|
fxchain.append(["PRESETNAME", "Program 1"])
|
||||||
fxchain.append(["FLOATPOS", "0", "0", "0", "0"])
|
fxchain.append(["FLOATPOS", "0", "0", "0", "0"])
|
||||||
fxchain.append(["FXID", f"{{{fxid_guid}}}"])
|
fxchain.append(["FXID", f"{{{fxid_guid}}}"])
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
@@ -306,10 +307,27 @@ class SampleSelector:
|
|||||||
matches.sort(key=lambda m: m.score, reverse=True)
|
matches.sort(key=lambda m: m.score, reverse=True)
|
||||||
return matches[:limit]
|
return matches[:limit]
|
||||||
|
|
||||||
def select_one(self, role: str, **kwargs) -> Optional[dict]:
|
def select_one(
|
||||||
"""Select the single best matching sample."""
|
self,
|
||||||
results = self.select(role=role, limit=1, **kwargs)
|
role: str,
|
||||||
return results[0].sample if results else None
|
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]:
|
def get_roles(self) -> list[str]:
|
||||||
"""Get all available roles and their counts."""
|
"""Get all available roles and their counts."""
|
||||||
|
|||||||
@@ -200,14 +200,17 @@ class TestSectionBuilderIntegration:
|
|||||||
index_path = _ROOT / "data" / "sample_index.json"
|
index_path = _ROOT / "data" / "sample_index.json"
|
||||||
selector = SampleSelector(str(index_path))
|
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(tracks) > 0, "Expected at least one track"
|
||||||
assert len(sections) > 0, "Expected at least one section"
|
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:
|
for sec in sections:
|
||||||
assert sec.name in ["intro", "verse", "chorus", "outro",
|
assert sec.name in valid_names, f"Unexpected section name: {sec.name}"
|
||||||
"verse2", "chorus2", "bridge", "chorus3"]
|
|
||||||
|
|
||||||
def test_song_definition_has_sections_field(self):
|
def test_song_definition_has_sections_field(self):
|
||||||
"""SongDefinition has a sections field."""
|
"""SongDefinition has a sections field."""
|
||||||
|
|||||||
Reference in New Issue
Block a user