File size: 2,763 Bytes
cde4ff8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import librosa
import numpy as np
from scipy import stats
import warnings

# Suppress librosa's PySoundFile warnings for mp3s
warnings.filterwarnings('ignore')


def compute_statistics(feature_matrix):
    """
    Calculates the 7 FMA statistics across a feature matrix.
    Order: kurtosis, max, mean, median, min, skew, std.
    """
    kurtosis = stats.kurtosis(feature_matrix, axis=1)
    maximum = np.max(feature_matrix, axis=1)
    mean = np.mean(feature_matrix, axis=1)
    median = np.median(feature_matrix, axis=1)
    minimum = np.min(feature_matrix, axis=1)
    skewness = stats.skew(feature_matrix, axis=1)
    std = np.std(feature_matrix, axis=1)

    return np.concatenate([kurtosis, maximum, mean, median, minimum, skewness, std])


def extract_audio_features(file_path):
    """
    Loads audio, extracts 518 features, and returns frontend metrics.
    """
    # 1. Load exactly 30 seconds at 22050 Hz
    y, sr = librosa.load(file_path, sr=22050, duration=30.0)

    # 2. Extract Base Features
    stft = np.abs(librosa.stft(y, n_fft=2048, hop_length=512))

    chroma_stft = librosa.feature.chroma_stft(S=stft, sr=sr)
    chroma_cqt = librosa.feature.chroma_cqt(y=y, sr=sr)
    chroma_cens = librosa.feature.chroma_cens(y=y, sr=sr)
    mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20)
    rmse = librosa.feature.rms(S=stft)
    spec_cent = librosa.feature.spectral_centroid(S=stft, sr=sr)
    spec_bw = librosa.feature.spectral_bandwidth(S=stft, sr=sr)
    spec_contrast = librosa.feature.spectral_contrast(S=stft, sr=sr)
    spec_rolloff = librosa.feature.spectral_rolloff(S=stft, sr=sr)
    tonnetz = librosa.feature.tonnetz(y=librosa.effects.harmonic(y), sr=sr)
    zcr = librosa.feature.zero_crossing_rate(y)

    # 3. Compute Statistics (Strict Order Required)
    features = []
    feature_list = [chroma_cens, chroma_cqt, chroma_stft, mfcc, rmse,
                    spec_bw, spec_cent, spec_contrast, spec_rolloff, tonnetz, zcr]

    for feature in feature_list:
        features.append(compute_statistics(feature))

    final_feature_vector = np.concatenate(features)

    # 4. Extract clean frontend metrics for the p5.js Mandala
    # 4. Extract clean frontend metrics for the p5.js Mandala
    bpm, _ = librosa.beat.beat_track(y=y, sr=sr)

    # SAFELY handle the Librosa version difference (extracts the number if it's trapped in an array)
    bpm_value = bpm[0] if isinstance(bpm, np.ndarray) else bpm

    frontend_metrics = {
        "bpm": round(float(bpm_value), 2),
        "energy_rmse": round(float(np.mean(rmse)), 4),
        "brightness_centroid": round(float(np.mean(spec_cent)), 2),
        "mfcc_vector": [round(float(x), 2) for x in np.mean(mfcc, axis=1)[:3]]
    }

    return final_feature_vector, frontend_metrics