feat: pattern-based generators from real track analysis, RPP structure fixes, randomization

- Reverse-engineer drum patterns from 2 real reggaeton tracks with librosa
- Create patterns.py with extracted frequency data (kick/snare/hihat positions)
- Rewrite rhythm.py with pattern-bank generators (dembow, dense, trapico, offbeat)
- Rewrite melodic.py with section-aware generators and humanization
- Add weighted random sample selection in SampleSelector (top-5 pool)
- Add generate_structure() with randomized templates and energy variance
- Fix RPP structure: TEMPO arity (3→4 args), string quoting for empty strings
- Rewrite quick_drumloop_test.py with correct REAPER ground truth format
- Add scripts/analyze_examples.py for reverse engineering audio tracks
- Add --seed argument for reproducible generation
- 72 tests passing
This commit is contained in:
renato97
2026-05-03 16:08:07 -03:00
parent 32dafd94e0
commit 3444006411
10 changed files with 1664 additions and 285 deletions

View File

@@ -16,6 +16,7 @@ from __future__ import annotations
import json
import os
import random
from pathlib import Path
from typing import Optional
from dataclasses import dataclass, field
@@ -306,10 +307,27 @@ class SampleSelector:
matches.sort(key=lambda m: m.score, reverse=True)
return matches[:limit]
def select_one(self, role: str, **kwargs) -> Optional[dict]:
"""Select the single best matching sample."""
results = self.select(role=role, limit=1, **kwargs)
return results[0].sample if results else None
def select_one(
self,
role: str,
seed: Optional[int] = None,
**kwargs,
) -> Optional[dict]:
"""Select one sample using weighted random from top-5 candidates.
The top-5 candidates are selected with weights [5, 4, 3, 2, 1],
favoring higher-scored results while allowing variation across calls.
Pass seed for reproducible output.
"""
if seed is not None:
random.seed(seed)
results = self.select(role=role, limit=5, **kwargs)
if not results:
return None
candidates = results[:5]
weights = [5, 4, 3, 2, 1][: len(candidates)]
selected = random.choices(candidates, weights=weights, k=1)[0]
return selected.sample
def get_roles(self) -> list[str]:
"""Get all available roles and their counts."""