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:
@@ -12,6 +12,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -26,6 +27,7 @@ from src.core.schema import (
|
||||
from src.composer.rhythm import get_notes, GENERATORS as RHYTHM_GENERATORS
|
||||
from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain
|
||||
from src.composer.converters import rhythm_to_midi, melodic_to_midi
|
||||
from src.composer.patterns import generate_structure
|
||||
from src.selector import SampleSelector
|
||||
from src.reaper_builder import RPPBuilder
|
||||
from src.reaper_builder.render import render_project
|
||||
@@ -67,9 +69,9 @@ ROLE_MELODIC_GENERATORS = {
|
||||
}
|
||||
|
||||
ROLE_RHYTHM_GENERATORS = {
|
||||
"drums": "kick_main_notes",
|
||||
"snare": "snare_verse_notes",
|
||||
"hihat": "hihat_16th_notes",
|
||||
"drums": "kick_pattern_bank_notes",
|
||||
"snare": "snare_pattern_bank_notes",
|
||||
"hihat": "hihat_pattern_bank_notes",
|
||||
"perc": "perc_combo_notes",
|
||||
}
|
||||
|
||||
@@ -197,6 +199,10 @@ def build_section_tracks(
|
||||
selector: SampleSelector,
|
||||
key: str,
|
||||
bpm: float,
|
||||
sections_data: list[dict] | None = None,
|
||||
humanize: float = 0.3,
|
||||
groove_strength: float = 0.3,
|
||||
bank_weights: list[tuple[str, float]] | None = None,
|
||||
) -> tuple[list[TrackDef], list[SectionDef]]:
|
||||
"""Build all tracks from genre config sections.
|
||||
|
||||
@@ -208,17 +214,33 @@ def build_section_tracks(
|
||||
selector: SampleSelector for sample queries
|
||||
key: Musical key (e.g. "Am")
|
||||
bpm: BPM for sample selection
|
||||
sections_data: List of section dicts with 'name', 'bars', 'energy' keys.
|
||||
If None, falls back to reading 'sections' from genre_config.
|
||||
humanize: Humanization amount for melodic generators (0.0-1.0)
|
||||
groove_strength: Groove amount for rhythm generators (0.0-1.0)
|
||||
bank_weights: List of (bank_name, weight) tuples for weighted random bank selection
|
||||
|
||||
Returns:
|
||||
(tracks, sections)
|
||||
"""
|
||||
structure = genre_config.get("structure", {})
|
||||
sections_raw = structure.get("sections", [])
|
||||
roles = genre_config.get("roles", {})
|
||||
|
||||
# Fall back to fixed sections from genre config for backward compatibility
|
||||
if sections_data is None:
|
||||
sections_data = genre_config.get("structure", {}).get("sections", [])
|
||||
|
||||
# Default bank weights for drums — weighted random selection
|
||||
if bank_weights is None:
|
||||
bank_weights = [
|
||||
("dembow_classico", 3),
|
||||
("dense", 3),
|
||||
("perreo", 2),
|
||||
("trapico", 1),
|
||||
]
|
||||
|
||||
# Parse sections into SectionDef list
|
||||
sections: list[SectionDef] = []
|
||||
for s in sections_raw:
|
||||
for s in sections_data:
|
||||
sections.append(SectionDef(
|
||||
name=s.get("name", "unknown"),
|
||||
bars=s.get("bars", 4),
|
||||
@@ -265,7 +287,17 @@ def build_section_tracks(
|
||||
|
||||
if role in ROLE_RHYTHM_GENERATORS:
|
||||
gen_name = ROLE_RHYTHM_GENERATORS[role]
|
||||
note_dict = get_notes(gen_name, section.bars, velocity_mult=vel_mult)
|
||||
# Weighted random bank selection for variation
|
||||
bank_names = [b[0] for b in bank_weights]
|
||||
bank_weight_values = [b[1] for b in bank_weights]
|
||||
bank = random.choices(bank_names, weights=bank_weight_values, k=1)[0]
|
||||
|
||||
note_dict = get_notes(
|
||||
gen_name, section.bars,
|
||||
velocity_mult=vel_mult,
|
||||
bank=bank,
|
||||
groove_strength=groove_strength,
|
||||
)
|
||||
|
||||
# Audio roles: one clip per hit (one-shot samples placed at beat positions)
|
||||
if role in AUDIO_ROLES:
|
||||
@@ -291,7 +323,13 @@ def build_section_tracks(
|
||||
section_clips.append(clip)
|
||||
elif role in ROLE_MELODIC_GENERATORS:
|
||||
gen_fn = ROLE_MELODIC_GENERATORS[role]
|
||||
note_list = gen_fn(key=key, bars=section.bars, velocity_mult=vel_mult)
|
||||
note_list = gen_fn(
|
||||
key=key,
|
||||
bars=section.bars,
|
||||
velocity_mult=vel_mult,
|
||||
section_type=section.name,
|
||||
humanize=humanize,
|
||||
)
|
||||
midi_notes = melodic_to_midi(note_list)
|
||||
# Melodic roles use MIDI instruments — no audio_path needed
|
||||
clip = ClipDef(
|
||||
@@ -377,6 +415,12 @@ def main() -> None:
|
||||
default=None,
|
||||
help="Output WAV path for rendering.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Random seed for reproducible output (default: unseeded for max variation).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate BPM
|
||||
@@ -404,8 +448,15 @@ def main() -> None:
|
||||
|
||||
selector = SampleSelector(str(index_path))
|
||||
|
||||
# Build tracks and sections from genre config
|
||||
tracks, sections = build_section_tracks(genre_config, selector, args.key, args.bpm)
|
||||
# Generate section structure from template with randomization
|
||||
# Note: generate_structure reseeds random internally if seed is provided
|
||||
sections_data = generate_structure(genre_config, args.bpm, args.key, seed=args.seed)
|
||||
|
||||
# Build tracks and sections
|
||||
tracks, sections = build_section_tracks(
|
||||
genre_config, selector, args.key, args.bpm, sections_data,
|
||||
humanize=0.3, groove_strength=0.3,
|
||||
)
|
||||
|
||||
# Create return tracks
|
||||
return_tracks = create_return_tracks()
|
||||
@@ -434,7 +485,7 @@ def main() -> None:
|
||||
print(f" - {e}", file=sys.stderr)
|
||||
|
||||
# Write .rpp
|
||||
builder = RPPBuilder(song)
|
||||
builder = RPPBuilder(song, seed=args.seed)
|
||||
builder.write(str(output_path))
|
||||
|
||||
# Render if requested
|
||||
|
||||
Reference in New Issue
Block a user