"""Analyze chord sequence outputs.""" import argparse import json import statistics from pathlib import Path def analyze_chords( chords: list, config: dict | None = None, ) -> dict: """Analyze chord sequence and return metrics. Args: chords: List of chords, each chord is a list of pitch dicts config: Optional config with target_octaves, melodic_max, max_path Returns: Dict with analysis metrics """ if config is None: config = {} target_octaves = config.get("target_range_octaves", 2.0) melodic_max = config.get("melodic_threshold_max", 300) max_path = config.get("max_path", 50) # Basic info num_chords = len(chords) num_voices = len(chords[0]) if chords else 0 # Melodic violations violations = 0 max_violation = 0 voice_changes = 0 for i in range(1, num_chords): for v in range(num_voices): diff = abs(chords[i][v]["cents"] - chords[i - 1][v]["cents"]) if diff > 0: voice_changes += 1 if diff > melodic_max: violations += 1 max_violation = max(max_violation, diff) # Target range target_octaves = config.get("target_range_octaves", 2.0) target_cents = target_octaves * 1200 if chords: start_avg = sum(p["cents"] for p in chords[0]) / len(chords[0]) end_avg = sum(p["cents"] for p in chords[-1]) / len(chords[-1]) actual_cents = end_avg - start_avg target_percent = (actual_cents / target_cents) * 100 if target_cents > 0 else 0 else: start_avg = end_avg = actual_cents = target_percent = 0 return { "num_chords": num_chords, "num_voices": num_voices, "melodic_max": melodic_max, "violations": violations, "max_violation": max_violation, "voice_changes": voice_changes, "start_avg_cents": start_avg, "end_avg_cents": end_avg, "target_cents": target_cents, "actual_cents": actual_cents, "target_percent": target_percent, "target_octaves": target_octaves, } def format_analysis(metrics: dict) -> str: """Format analysis metrics as readable output.""" lines = [ "=== Analysis ===", f"Path length: {metrics['num_chords']} chords, {metrics['num_voices']} voices", "", "--- Melodic Threshold ---", f"Max allowed: {metrics['melodic_max']} cents", f"Violations: {metrics['violations']}", f"Max violation: {metrics['max_violation']:.0f} cents", f"Voice changes: {metrics['voice_changes']}", "", "--- Target Range ---", f"Target: {metrics['target_octaves']} octaves ({metrics['target_cents']:.0f} cents)", f"Start avg: {metrics['start_avg_cents']:.0f} cents", f"End avg: {metrics['end_avg_cents']:.0f} cents", f"Achieved: {metrics['actual_cents']:.0f} cents ({metrics['target_percent']:.1f}%)", ] return "\n".join(lines) def analyze_file(file_path: str | Path) -> dict: """Load and analyze a chord file.""" with open(file_path) as f: chords = json.load(f) return analyze_chords(chords) def main(): parser = argparse.ArgumentParser(description="Analyze chord sequence outputs") parser.add_argument( "file", nargs="?", default="output/output_chords.json", help="Path to chord JSON file (default: output/output_chords.json)", ) parser.add_argument( "--json", action="store_true", help="Output raw JSON instead of formatted text", ) args = parser.parse_args() file_path = Path(args.file) if not file_path.exists(): print(f"Error: File not found: {file_path}") return 1 metrics = analyze_file(file_path) if args.json: print(json.dumps(metrics, indent=2)) else: print(format_analysis(metrics)) return 0 if __name__ == "__main__": exit(main())