|
import librosa |
|
import numpy as np |
|
from scipy import signal |
|
from collections import Counter |
|
import warnings |
|
warnings.filterwarnings('ignore') |
|
try: |
|
import matplotlib.pyplot as plt |
|
except ImportError: |
|
plt = None |
|
|
|
class MusicAnalyzer: |
|
def __init__(self): |
|
|
|
self.emotion_classes = { |
|
'happy': {'valence': 0.96, 'arousal': 0.72}, |
|
'excited': {'valence': 0.88, 'arousal': 0.96}, |
|
'tender': {'valence': 0.70, 'arousal': 0.39}, |
|
'calm': {'valence': 0.58, 'arousal': 0.18}, |
|
'sad': {'valence': 0.18, 'arousal': 0.19}, |
|
'depressed': {'valence': 0.09, 'arousal': 0.06}, |
|
'angry': {'valence': 0.11, 'arousal': 0.80}, |
|
'fearful': {'valence': 0.13, 'arousal': 0.99} |
|
} |
|
|
|
self.theme_classes = { |
|
'love': ['happy', 'excited', 'tender'], |
|
'triumph': ['excited', 'happy', 'angry'], |
|
'loss': ['sad', 'depressed'], |
|
'adventure': ['excited', 'fearful'], |
|
'reflection': ['calm', 'tender', 'sad'], |
|
'conflict': ['angry', 'fearful'] |
|
} |
|
|
|
self.feature_weights = { |
|
'mode': 0.34, |
|
'tempo': 0.32, |
|
'energy': 0.16, |
|
'brightness': 0.14, |
|
'rhythm_complexity': 0.04 |
|
} |
|
self.key_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] |
|
|
|
def load_audio(self, file_path, sr=22050, duration=None): |
|
try: |
|
y, sr = librosa.load(file_path, sr=sr, duration=duration) |
|
return y, sr |
|
except Exception as e: |
|
print(f"Error loading audio file: {e}") |
|
return None, None |
|
|
|
def analyze_rhythm(self, y, sr): |
|
onset_env = librosa.onset.onset_strength(y=y, sr=sr) |
|
tempo, beat_frames = librosa.beat.beat_track(onset_envelope=onset_env, sr=sr) |
|
beat_times = librosa.frames_to_time(beat_frames, sr=sr) |
|
beat_intervals = np.diff(beat_times) if len(beat_times) > 1 else np.array([0]) |
|
beat_regularity = 1.0 / np.std(beat_intervals) if len(beat_intervals) > 0 and np.std(beat_intervals) > 0 else 0 |
|
ac = librosa.autocorrelate(onset_env, max_size=sr // 2) |
|
ac = librosa.util.normalize(ac, norm=np.inf) |
|
rhythm_intensity = np.mean(onset_env) / np.max(onset_env) if np.max(onset_env) > 0 else 0 |
|
rhythm_complexity = np.std(onset_env) / np.mean(onset_env) if np.mean(onset_env) > 0 else 0 |
|
beat_times_list = [float(t) for t in beat_times.tolist()] |
|
beat_intervals_list = [float(i) for i in beat_intervals.tolist()] |
|
return { |
|
"tempo": float(tempo), |
|
"beat_times": beat_times_list, |
|
"beat_intervals": beat_intervals_list, |
|
"beat_regularity": float(beat_regularity), |
|
"rhythm_intensity": float(rhythm_intensity), |
|
"rhythm_complexity": float(rhythm_complexity) |
|
} |
|
|
|
def analyze_tonality(self, y, sr): |
|
chroma = librosa.feature.chroma_cqt(y=y, sr=sr) |
|
major_profile = np.array([6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88]) |
|
minor_profile = np.array([6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]) |
|
chroma_avg = np.mean(chroma, axis=1) |
|
major_corr = np.zeros(12) |
|
minor_corr = np.zeros(12) |
|
for i in range(12): |
|
major_corr[i] = np.corrcoef(np.roll(chroma_avg, i), major_profile)[0, 1] |
|
minor_corr[i] = np.corrcoef(np.roll(chroma_avg, i), minor_profile)[0, 1] |
|
max_major_idx = np.argmax(major_corr) |
|
max_minor_idx = np.argmax(minor_corr) |
|
if major_corr[max_major_idx] > minor_corr[max_minor_idx]: |
|
mode = "major" |
|
key = self.key_names[max_major_idx] |
|
else: |
|
mode = "minor" |
|
key = self.key_names[max_minor_idx] |
|
harmony_complexity = np.std(chroma) / np.mean(chroma) if np.mean(chroma) > 0 else 0 |
|
tonal_stability = 1.0 / (np.std(chroma_avg) + 0.001) |
|
spectral_centroid = librosa.feature.spectral_centroid(y=y, sr=sr)[0] |
|
brightness = np.mean(spectral_centroid) / (sr / 2) |
|
spectral_contrast = librosa.feature.spectral_contrast(y=y, sr=sr) |
|
dissonance = np.mean(spectral_contrast[0]) |
|
return { |
|
"key": key, |
|
"mode": mode, |
|
"is_major": mode == "major", |
|
"harmony_complexity": float(harmony_complexity), |
|
"tonal_stability": float(tonal_stability), |
|
"brightness": float(brightness), |
|
"dissonance": float(dissonance) |
|
} |
|
|
|
def analyze_energy(self, y, sr): |
|
rms = librosa.feature.rms(y=y)[0] |
|
mean_energy = np.mean(rms) |
|
energy_std = np.std(rms) |
|
energy_dynamic_range = np.max(rms) - np.min(rms) if len(rms) > 0 else 0 |
|
spec = np.abs(librosa.stft(y)) |
|
freq_bins = spec.shape[0] |
|
low_freq_energy = np.mean(spec[:int(freq_bins * 0.2), :]) |
|
mid_freq_energy = np.mean(spec[int(freq_bins * 0.2):int(freq_bins * 0.8), :]) |
|
high_freq_energy = np.mean(spec[int(freq_bins * 0.8):, :]) |
|
total_energy = low_freq_energy + mid_freq_energy + high_freq_energy |
|
if total_energy > 0: |
|
low_freq_ratio = low_freq_energy / total_energy |
|
mid_freq_ratio = mid_freq_energy / total_energy |
|
high_freq_ratio = high_freq_energy / total_energy |
|
else: |
|
low_freq_ratio = mid_freq_ratio = high_freq_ratio = 1 / 3 |
|
return { |
|
"mean_energy": float(mean_energy), |
|
"energy_std": float(energy_std), |
|
"energy_dynamic_range": float(energy_dynamic_range), |
|
"frequency_distribution": { |
|
"low_freq": float(low_freq_ratio), |
|
"mid_freq": float(mid_freq_ratio), |
|
"high_freq": float(high_freq_ratio) |
|
} |
|
} |
|
|
|
def feature_to_valence_arousal(self, features): |
|
|
|
|
|
tempo_norm = np.clip((features['tempo'] - 70) / (170 - 70), 0, 1) |
|
energy_norm = np.clip((features['energy'] - 0.08) / (0.5 - 0.08), 0, 1) |
|
brightness_norm = np.clip((features['brightness'] - 0.25) / (0.7 - 0.25), 0, 1) |
|
rhythm_complexity_norm = np.clip((features['rhythm_complexity'] - 0.1) / (0.8 - 0.1), 0, 1) |
|
|
|
valence = ( |
|
self.feature_weights['mode'] * (1.0 if features['is_major'] else 0.0) + |
|
self.feature_weights['tempo'] * tempo_norm + |
|
self.feature_weights['energy'] * energy_norm + |
|
self.feature_weights['brightness'] * brightness_norm |
|
) |
|
arousal = ( |
|
self.feature_weights['tempo'] * tempo_norm + |
|
self.feature_weights['energy'] * energy_norm + |
|
self.feature_weights['brightness'] * brightness_norm + |
|
self.feature_weights['rhythm_complexity'] * rhythm_complexity_norm |
|
) |
|
|
|
|
|
if features['is_major'] and features['tempo'] > 100 and features['brightness'] > 0.5: |
|
valence = max(valence, 0.85) |
|
arousal = max(arousal, 0.7) |
|
|
|
return float(np.clip(valence, 0, 1)), float(np.clip(arousal, 0, 1)) |
|
|
|
def analyze_emotion(self, rhythm_data, tonal_data, energy_data): |
|
features = { |
|
'tempo': rhythm_data['tempo'], |
|
'energy': energy_data['mean_energy'], |
|
'is_major': tonal_data['is_major'], |
|
'brightness': tonal_data['brightness'], |
|
'rhythm_complexity': rhythm_data['rhythm_complexity'] |
|
} |
|
valence, arousal = self.feature_to_valence_arousal(features) |
|
emotion_scores = {} |
|
for emotion, va in self.emotion_classes.items(): |
|
dist = np.sqrt((valence - va['valence']) ** 2 + (arousal - va['arousal']) ** 2) |
|
emotion_scores[emotion] = 1.0 - dist |
|
primary_emotion = max(emotion_scores.items(), key=lambda x: x[1]) |
|
sorted_emotions = sorted(emotion_scores.items(), key=lambda x: x[1], reverse=True) |
|
secondary_emotion = sorted_emotions[1][0] if len(sorted_emotions) > 1 else None |
|
return { |
|
"primary_emotion": primary_emotion[0], |
|
"confidence": float(primary_emotion[1]), |
|
"emotion_scores": {k: float(v) for k, v in emotion_scores.items()}, |
|
"valence": valence, |
|
"arousal": arousal, |
|
"secondary_emotion": secondary_emotion |
|
} |
|
|
|
def analyze_theme(self, rhythm_data, tonal_data, emotion_data): |
|
primary_emotion = emotion_data['primary_emotion'] |
|
secondary_emotion = emotion_data.get('secondary_emotion') |
|
theme_scores = {} |
|
for theme, emolist in self.theme_classes.items(): |
|
score = 0.0 |
|
if primary_emotion in emolist: |
|
score += 0.7 |
|
if secondary_emotion in emolist: |
|
score += 0.3 |
|
harmony_complexity = tonal_data.get('harmony_complexity', 0.5) |
|
if theme in ['adventure', 'conflict']: |
|
score += 0.3 * np.clip((harmony_complexity - 0.4) / 0.6, 0, 1) |
|
elif theme in ['love', 'reflection']: |
|
score += 0.3 * np.clip((0.6 - harmony_complexity) / 0.6, 0, 1) |
|
theme_scores[theme] = float(np.clip(score, 0, 1)) |
|
primary_theme = max(theme_scores.items(), key=lambda x: x[1]) |
|
secondary_themes = [k for k, v in sorted(theme_scores.items(), key=lambda x: x[1], reverse=True) |
|
if k != primary_theme[0] and v > 0.5] |
|
return { |
|
"primary_theme": primary_theme[0], |
|
"confidence": primary_theme[1], |
|
"secondary_themes": secondary_themes[:2], |
|
"theme_scores": theme_scores |
|
} |
|
|
|
def analyze_music(self, file_path): |
|
y, sr = self.load_audio(file_path) |
|
if y is None: |
|
return {"error": "Failed to load audio file"} |
|
rhythm_data = self.analyze_rhythm(y, sr) |
|
tonal_data = self.analyze_tonality(y, sr) |
|
energy_data = self.analyze_energy(y, sr) |
|
emotion_data = self.analyze_emotion(rhythm_data, tonal_data, energy_data) |
|
theme_data = self.analyze_theme(rhythm_data, tonal_data, emotion_data) |
|
def convert_numpy_to_python(obj): |
|
if isinstance(obj, dict): |
|
return {k: convert_numpy_to_python(v) for k, v in obj.items()} |
|
elif isinstance(obj, list): |
|
return [convert_numpy_to_python(item) for item in obj] |
|
elif isinstance(obj, np.ndarray): |
|
return obj.tolist() |
|
elif isinstance(obj, np.number): |
|
return float(obj) |
|
else: |
|
return obj |
|
rhythm_data = convert_numpy_to_python(rhythm_data) |
|
tonal_data = convert_numpy_to_python(tonal_data) |
|
energy_data = convert_numpy_to_python(energy_data) |
|
emotion_data = convert_numpy_to_python(emotion_data) |
|
theme_data = convert_numpy_to_python(theme_data) |
|
return { |
|
"file": file_path, |
|
"rhythm_analysis": rhythm_data, |
|
"tonal_analysis": tonal_data, |
|
"energy_analysis": energy_data, |
|
"emotion_analysis": emotion_data, |
|
"theme_analysis": theme_data, |
|
"summary": { |
|
"tempo": float(rhythm_data["tempo"]), |
|
"primary_emotion": emotion_data["primary_emotion"], |
|
"primary_theme": theme_data["primary_theme"] |
|
} |
|
} |
|
|
|
|
|
analyzer = MusicAnalyzer() |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
demo_file = "path/to/your/audio/file.mp3" |
|
|
|
|
|
results = analyzer.analyze_music(demo_file) |
|
|
|
|
|
print("\n=== MUSIC ANALYSIS SUMMARY ===") |
|
print(f"Tempo: {results['summary']['tempo']:.1f} BPM") |
|
print(f"Primary Emotion: {results['summary']['primary_emotion']}") |
|
print(f"Primary Theme: {results['summary']['primary_theme']}") |
|
|
|
|
|
import json |
|
print("\n=== DETAILED ANALYSIS ===") |
|
print(json.dumps(results, indent=2)) |
|
|
|
|
|
|