"""Lists the simulated clips most similar to a chosen simulated clip under the
perceptual log-mel distance of match.py, and writes a listening page.
python3 ml/similar.py QUERY_ID [--data dataset] [--top 5] [--controls 50,500,2500]
The log-mel features of every clip are cached in DATA/ml/mel.npz. The page
DATA/similar-QUERY_ID.html has the query, the top matches, and a few control
clips at given ranks, so the measure can be judged by ear against the ranks.
"""
import argparse
import json
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from match import read_wav_any, segment, melspec # noqa: E402
def mel_features(data):
cache = os.path.join(data, "ml", "mel.npz")
rows = {json.loads(l)["id"]: json.loads(l) for l in open(os.path.join(data, "index.jsonl")) if l.strip()}
ids = sorted(i for i in rows if os.path.exists(os.path.join(data, "clips", i + ".wav")))
if os.path.exists(cache):
z = np.load(cache, allow_pickle=True)
if list(z["ids"]) == ids:
return rows, ids, z["mel"]
os.makedirs(os.path.join(data, "ml"), exist_ok=True)
mel = np.stack([melspec(segment(read_wav_any(os.path.join(data, "clips", i + ".wav")), 0)) for i in ids]).astype(np.float32)
np.savez(cache, ids=np.array(ids), mel=mel)
return rows, ids, mel
def cells(r):
return (f"
{r['E'] / 1e9:.0f}
{r['rho']:.0f}
{r['h'] * 1e3:.2f}
{r['Lx'] / 0.0254:.1f}×{r['Ly'] / 0.0254:.1f}
"
f"
{r['strikeVelocity']:.1f}
{r['strikeFracX']:.2f}, {r['strikeFracY']:.2f}
{r['sigma0']:.2f}
{r['sigma1'] * 1e4:.1f}
"
f"
{r['peakToPeak'] * 1e3:.1f}
{r['features']}
")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("query")
ap.add_argument("--data", default="dataset")
ap.add_argument("--top", type=int, default=5)
ap.add_argument("--controls", default="50,500,2500")
args = ap.parse_args()
rows, ids, mel = mel_features(args.data)
q = ids.index(args.query)
d = np.abs(mel - mel[q]).mean((1, 2))
order = np.argsort(d)
assert order[0] == q
ranked = [(k, ids[i], d[i]) for k, i in enumerate(order)]
sel = [("query", *ranked[0][1:])] + [(f"rank {k}", i, dd) for k, i, dd in ranked[1:args.top + 1]]
for c in [int(x) for x in args.controls.split(",") if x]:
if c < len(ranked):
sel.append((f"control, rank {c}", *ranked[c][1:]))
print(f"query {args.query}; median distance over all clips {np.median(d):.2f} dB")
for label, i, dd in sel:
r = rows[i]
print(f"{label:>18} {i} {dd:5.2f} dB | E {r['E'] / 1e9:.0f} rho {r['rho']:.0f} h {r['h'] * 1e3:.2f} {r['Lx'] / 0.0254:.1f}x{r['Ly'] / 0.0254:.1f} in v {r['strikeVelocity']:.1f} sig0 {r['sigma0']:.2f} sig1 {r['sigma1'] * 1e4:.1f} p2p {r['peakToPeak'] * 1e3:.1f}")
gal = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "scripts", "gallery.html")).read()
style = gal[gal.index("")]
js = gal[gal.index("// Spectrogram:"):gal.index("$(\"reload\").onclick")]
body = "".join(
f"
{label}
{i}
{dd:.2f}
{cells(rows[i])}
"
f"
"
for label, i, dd in sel)
html = f"""clips similar to {args.query}
Simulated clips most similar to {args.query}
Distance: mean absolute dB difference between level-normalized log-mel spectrograms (48 bands, 60 Hz to 6 kHz, 100 ms frames, floored at −60 dB). Median distance over all {len(ids)} clips: {np.median(d):.2f} dB. The control rows are clips at the given ranks, for comparison by ear.
role
id
dist dB
audio
E GPa
ρ
h mm
W×H in
v
strike
σ0
σ1·10⁴
p-p mm
features
{body}
"""
out = os.path.join(args.data, f"similar-{args.query}.html")
open(out, "w").write(html)
print("wrote", out)
if __name__ == "__main__":
main()