177 lines
5.7 KiB
Python

# /// 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))