"""Analyze drumloops from the library and output structured forensic data. Usage: python scripts/analyze_drumloop.py [--count N] [--output PATH] [--json PATH] """ from __future__ import annotations import argparse import json import sys from pathlib import Path PROJECT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PROJECT)) from src.composer.drum_analyzer import DrumLoopAnalyzer def load_drumloop_paths(index_path: Path, count: int = 5) -> list[dict]: with open(index_path, "r", encoding="utf-8") as f: data = json.load(f) drumloops = [s for s in data["samples"] if s.get("role") == "drumloop"] scored = [] for d in drumloops: path = Path(d["original_path"]) if not path.exists(): continue dur = d.get("signal", {}).get("duration", 0) onsets = d.get("perceptual", {}).get("onset_count", 0) density = onsets / max(dur, 0.01) tempo = d.get("perceptual", {}).get("tempo", 0) if 85 <= tempo <= 150 and 6 <= dur <= 35: scored.append((d, density)) scored.sort(key=lambda x: abs(x[1] - 4.0)) return [s[0] for s in scored[:count]] def print_analysis(result_dict: dict) -> None: print(f"\n{'='*70}") print(f" {Path(result_dict['file_path']).name}") print(f"{'='*70}") print(f" BPM: {result_dict['bpm']}") print(f" Duration: {result_dict['duration']:.2f}s") print(f" Bars: {result_dict['bar_count']}") print(f" Key: {result_dict['key']} (conf: {result_dict['key_confidence']:.2f})") print(f" Beat grid: {len(result_dict['beat_grid']['quarter'])} quarters, " f"{len(result_dict['beat_grid']['eighth'])} eighths, " f"{len(result_dict['beat_grid']['sixteenth'])} sixteenths") summary = result_dict["summary"] total = summary["kick_count"] + summary["snare_count"] + summary["hihat_count"] + summary["other_count"] print(f"\n Transients: {total} total") print(f" Kicks: {summary['kick_count']}") print(f" Snares: {summary['snare_count']}") print(f" HiHats: {summary['hihat_count']}") print(f" Other: {summary['other_count']}") transients_by_type = {} for t in result_dict["transients"]: transients_by_type.setdefault(t["type"], []).append(t) for ttype in ["kick", "snare", "hihat", "other"]: ts = transients_by_type.get(ttype, []) if not ts: continue print(f"\n {ttype.upper()} positions (beat positions):") positions = [f"{t['beat_pos']:.2f}" for t in ts[:20]] line = " " + " ".join(positions) if len(ts) > 20: line += f" ... +{len(ts)-20} more" print(line) if result_dict["energy_profile"]: print(f"\n Energy profile (first 16 beats):") bars_e = result_dict["energy_profile"][:16] max_e = max(bars_e) if bars_e else 1 for i, e in enumerate(bars_e): bar = i // 4 beat = i % 4 filled = int((e / max_e) * 30) if max_e > 0 else 0 print(f" Bar {bar+1} Beat {beat+1}: {'|' * filled} ({e:.4f})") def main(): parser = argparse.ArgumentParser(description="Analyze drumloops forensically") parser.add_argument("--count", type=int, default=3, help="Number of drumloops to analyze") parser.add_argument("--index", type=str, default=None, help="Path to sample_index.json") parser.add_argument("--file", type=str, default=None, help="Analyze a single file instead") parser.add_argument("--json", type=str, default=None, help="Save results as JSON") args = parser.parse_args() index_path = Path(args.index) if args.index else PROJECT / "data" / "sample_index.json" results = [] if args.file: print(f"Analyzing: {args.file}") analyzer = DrumLoopAnalyzer(args.file) result = analyzer.analyze() results.append(result.to_dict()) print_analysis(results[0]) else: drumloops = load_drumloop_paths(index_path, args.count) if not drumloops: print("No suitable drumloops found.") return print(f"Selected {len(drumloops)} drumloops for analysis:\n") for d in drumloops: print(f" - {d['original_name']} " f"(tempo={d.get('perceptual',{}).get('tempo','?')}, " f"dur={d.get('signal',{}).get('duration',0):.1f}s)") for d in drumloops: path = d["original_path"] print(f"\nAnalyzing: {Path(path).name}...") analyzer = DrumLoopAnalyzer(path) result = analyzer.analyze() results.append(result.to_dict()) print_analysis(result.to_dict()) if args.json: out_path = Path(args.json) out_path.parent.mkdir(parents=True, exist_ok=True) with open(out_path, "w", encoding="utf-8") as f: json.dump(results, f, indent=2, ensure_ascii=False) print(f"\nResults saved to: {out_path}") print(f"\n{'='*70}") print("DRUMLOOP-FIRST GENERATION APPROACH") print(f"{'='*70}") print(""" 1. SELECT drumloop -> extract BPM + beat grid + transient map 2. ALIGN project -> set REAPER tempo to drumloop BPM 3. GENERATE bass -> tresillo pattern in kick-free zones - Reggaeton tresillo: notes at 0.0, 0.75, 1.5, 2.0, 2.75, 3.5 - Place bass between kick transients (margin +/-0.15 beats) 4. GENERATE chords -> change on downbeats (beat 1 of each bar) - Sustain through bar, use i-VI-III-VII progression - Match key from drumloop analysis 5. GENERATE melody -> place on transient-free zones - Emphasize chord tones on strong beats - Syncopation matches dembow feel 6. GENERATE vocals -> chops in gaps between drum transients 7. SELECT samples -> match drumloop key for compatible tonal samples """) if __name__ == "__main__": main()