Skip to content

Features, corpus statistics & categories

Per-track features

features.extract_collection computes an interpretable descriptor set per track (cached in features.json, extracted in parallel):

Feature Musical reading
onset_rate note density—plucked events per second
centroid_hz brightness (spectral centroid)
flatness inharmonic texture—buzz, bowls, breath
zcr, flux surface noisiness, spectral change
perc_ratio percussive share (harmonic/percussive separation)
dyn_range_db loud-to-quiet span within the track
chroma_entropy pitch-class spread
key, key_conf Krumhansl–Schmuckler estimate + correlation
pulse_R, pulse_bpm circular pulse clarity and its period
tempo_bpm perceptually-weighted tempo (what cards display)
fifths_center, fifths_R tonal centre and focus on the circle of fifths
tartyp duration-shares of Schaeffer object types

The set is deliberately small enough to explain, and it is the interpretable counterpart to embedding models. The trade is weaker raw similarity, but every axis has a musical name.

Very long tracks

Analysing a track costs memory in proportion to its length: the spectrogram, the harmonic/percussive separation and the constant-Q chroma each hold the whole signal. Three quarters of an hour of audio needs several gigabytes in one worker, and if the operating system kills that worker the process is simply gone — no exception is raised, and the pool it was running in breaks.

extract_collection survives this. It records what completed, then retries the rest one at a time in their own process with the analysis window capped to retry_cap_s (default 600 s), so a track that has already proved fatal is analysed on a window that fits. Capped results carry analysis_capped_s, so a shortened window is visible in the output rather than looking like a short track. Nothing is capped on the first attempt, so ordinary collections are unaffected; pass retry_cap_s=None to retry at full length instead.

Tempo, honestly

Beat trackers fail on rubato material, so two numbers are kept apart: pulse_R measures metric lock (circular concentration of onset phases; 0 = free, 1 = metronomic), while tempo_bpm is librosa's perceptually-weighted estimate targeting the felt beat. Cards display the latter, ~-prefixed when pulse_R < 0.1.

Corpus statistics

corpus.album_stats(f)    # mean/std/min/max per feature, keys, minor share
corpus.similarity(f)     # cosine matrix + album affinity & consistency
corpus.landscape(f)      # PCA coords, explained variance, loadings
corpus.tonal_spread(f)   # circular concentration of tonal centres per album

The affinity diagonal is each album's internal consistency. An album with one instrument and one mood scores high, while an eclectic album scores near zero. tonal_spread answers a question with no linear equivalent, since key centres have no meaningful mean: a repertoire in neighbouring keys scores R near 1, and one that wanders the whole circle near 0.

Categories

categorize.cluster(f, k=None) runs k-means in the standardised feature space (k chosen by silhouette unless given) and describes every cluster by its three most distinguishing features as signed z-scores:

category 2 (15): centroid_hz +1.4, zcr +1.2, flatness +1.0

That reads as "bright, noisy, inharmonic": the textural tracks, wherever their album membership put them. A category is never just "cluster 3".