Think of each musical scale as a set of points on a clock. Instead of hours, the clock marks musical distances ("cents") around the circle. Two scales look similar if their points land in similar places — even if one is slightly shifted or stretched. We compare scales by:
We then rank scales by similarity and show "nearby" relatives. This works across tunings and different numbers of notes.
[0, P) where P is the period (usually 1200).(ratio_num, ratio_den) is retained.[0, P/2] with configurable bin width (e.g., 50¢).import math
def wrap_cents(c, period=1200.0):
x = c % period
return x + period if x < 0 else x
def interval_histogram(cents, period=1200.0, bin_width=50.0):
vals = sorted(wrap_cents(c, period) for c in cents)
k = len(vals)
if k < 2:
bins = int(math.ceil((period/2)/bin_width))
return [0.0] * bins
diffs = []
for i in range(k):
for j in range(i+1, k):
d = abs(vals[j] - vals[i])
diffs.append(min(d, period - d))
bins = [0.0] * int(math.ceil((period/2)/bin_width))
for d in diffs:
idx = min(int(d // bin_width), len(bins)-1)
bins[idx] += 1.0
s = sum(bins) or 1.0
return [b/s for b in bins]
def jensen_shannon_distance(p, q):
def _norm(v):
s = sum(v) or 1.0
return [x/s for x in v]
def _kl(a, b):
eps = 1e-12
s = 0.0
for ai, bi in zip(a, b):
ai = max(ai, eps); bi = max(bi, eps)
s += ai * math.log(ai/bi)
return s
p = _norm(p); q = _norm(q); m = [(pi+qi)*0.5 for pi,qi in zip(p,q)]
return math.sqrt(0.5*_kl(p,m) + 0.5*_kl(q,m))
Create a Python venv and run the analysis script to index assets/source/scl, compute features, and print nearest neighbors per scale (uses only Python standard library).
python scripts/scale_analyze.py --scl-dir assets/source/scl --bin-width 50 --topk 8 --limit 200