# /// script # requires-python = ">=3.10" # dependencies = [ # "librosa>=0.10.0", # "numpy", # "scipy", # "soundfile", # ] # /// import librosa import numpy as np import sys import json import os from pathlib import Path def analyze_track(audio_path): """Analyze a single track and return features.""" print(f" Analyzing: {os.path.basename(audio_path)}") y, sr = librosa.load(audio_path, sr=22050, duration=180) # Tempo tempo, _ = librosa.beat.beat_track(y=y, sr=sr) tempo = float(tempo) if not hasattr(tempo, '__len__') else float(tempo[0]) # Energy rms = librosa.feature.rms(y=y)[0] energy = float(np.mean(rms)) energy_std = float(np.std(rms)) # Spectral features spectral_centroid = librosa.feature.spectral_centroid(y=y, sr=sr)[0] brightness = float(np.mean(spectral_centroid)) # Harmonic/Percussive y_harmonic, y_percussive = librosa.effects.hpss(y) harmonic_ratio = float(np.sum(np.abs(y_harmonic)) / (np.sum(np.abs(y)) + 1e-6)) # Chroma chroma = librosa.feature.chroma_stft(y=y, sr=sr) chroma_mean = float(np.mean(chroma)) # Onset strength onset_env = librosa.onset.onset_strength(y=y, sr=sr) rhythmic_density = float(np.mean(onset_env)) # Zero crossing rate zcr = librosa.feature.zero_crossing_rate(y)[0] percussiveness = float(np.mean(zcr)) return { "tempo_bpm": tempo, "energy_rms": energy, "energy_std": energy_std, "brightness_hz": brightness, "harmonic_ratio": harmonic_ratio, "chroma_mean": chroma_mean, "rhythmic_density": rhythmic_density, "percussiveness": percussiveness, } def normalize_features(raw): """Convert raw features to 0-1 Genre Universe scales.""" # Tempo: 60-180 BPM tempo_norm = np.clip((raw["tempo_bpm"] - 60) / 120, 0, 1) # Energy: RMS 0.01-0.25 energy_norm = np.clip(raw["energy_rms"] / 0.2, 0, 1) # Brightness: 1000-4000 Hz brightness_norm = np.clip((raw["brightness_hz"] - 1000) / 3000, 0, 1) # Organic score organic_norm = np.clip(raw["harmonic_ratio"] * 1.2 - brightness_norm * 0.2, 0, 1) # Valence estimate (rough) valence_norm = np.clip(0.25 + brightness_norm * 0.25 + raw["chroma_mean"] * 0.3, 0, 1) # Danceability dance_tempo = 1 - abs(raw["tempo_bpm"] - 120) / 60 danceability = np.clip(dance_tempo * 0.4 + raw["rhythmic_density"] * 0.35 + energy_norm * 0.25, 0, 1) # Acousticness (related to organic but different) acousticness = np.clip(raw["harmonic_ratio"] * 0.7 + (1 - brightness_norm) * 0.3, 0, 1) # Production density prod_density = np.clip((1 - raw["harmonic_ratio"]) * 0.5 + raw["energy_std"] * 8 + energy_norm * 0.3, 0, 1) return { "position": { "valence": round(float(valence_norm), 2), "tempo": round(float(tempo_norm), 2), "organic": round(float(organic_norm), 2), }, "spikes": { "energy": round(float(energy_norm), 2), "acousticness": round(float(acousticness), 2), "danceability": round(float(danceability), 2), "production_density": round(float(prod_density), 2), } } if __name__ == "__main__": tracks_dir = Path("surya_tracks") mp3_files = list(tracks_dir.glob("*.mp3")) if not mp3_files: print("No MP3 files found!") sys.exit(1) print(f"\nšŸŽµ ANALYZING {len(mp3_files)} TRACKS FROM SURYA ALBUM\n") print("="*60) all_raw = [] track_results = [] for mp3 in sorted(mp3_files): raw = analyze_track(str(mp3)) all_raw.append(raw) normalized = normalize_features(raw) track_results.append({ "track": mp3.stem, "raw": raw, "normalized": normalized }) print(f" Tempo: {raw['tempo_bpm']:.1f} BPM | Energy: {raw['energy_rms']:.3f} | Harmonic: {raw['harmonic_ratio']:.2f}") # Average all tracks print("\n" + "="*60) print("šŸ“Š ALBUM AVERAGES") print("="*60) avg_raw = { key: np.mean([t[key] for t in all_raw]) for key in all_raw[0].keys() } print(f"\n Average Tempo: {avg_raw['tempo_bpm']:.1f} BPM") print(f" Average Energy: {avg_raw['energy_rms']:.4f}") print(f" Average Brightness: {avg_raw['brightness_hz']:.1f} Hz") print(f" Average Harmonic Ratio: {avg_raw['harmonic_ratio']:.3f}") avg_normalized = normalize_features(avg_raw) print("\n" + "="*60) print("šŸŽÆ RECOMMENDED DAS POSITION (Album Average)") print("="*60) pos = avg_normalized["position"] spikes = avg_normalized["spikes"] print(f"\n Position in Genre Universe:") print(f" valence: {pos['valence']} (mood: {'sad' if pos['valence'] < 0.4 else 'happy' if pos['valence'] > 0.6 else 'bittersweet'})") print(f" tempo: {pos['tempo']} ({'slow' if pos['tempo'] < 0.4 else 'fast' if pos['tempo'] > 0.6 else 'medium'})") print(f" organic: {pos['organic']} ({'electronic' if pos['organic'] < 0.4 else 'organic' if pos['organic'] > 0.6 else 'balanced'})") print(f"\n Spike Values:") for key, val in spikes.items(): bar = "ā–ˆ" * int(val * 20) + "ā–‘" * (20 - int(val * 20)) print(f" {key:20} [{bar}] {val}") # Output JSON for programmatic use result = { "tracks_analyzed": len(mp3_files), "track_names": [mp3.stem for mp3 in sorted(mp3_files)], "raw_averages": {k: round(v, 4) for k, v in avg_raw.items()}, "recommended_position": pos, "recommended_spikes": spikes, } print("\n" + "="*60) print("šŸ“„ JSON OUTPUT") print("="*60) print(json.dumps(result, indent=2))