Commit 3f2863d4 by PLN (Algolia)

feat(corpus-viz): hover tooltips + all-dots collab view + copy de-slop

PLN review pass:
- tooltip layer: hover any mark for the tracks/examples behind it. cloud dots
  (track + bpm + date), tempo line dots (median + what it means), histogram
  bands, gig/sample columns, family segments (count + % + example members),
  idiom bars (plain-language gloss of each phrase), staple bars, collab dots
- collab story now plots EVERY track as a dot; tracks at the same BPM pile into
  one larger dot (radius ~ count); median drawn as a tick; sample-tell chips get
  lift tooltips. needs tide_eda collab_fingerprint to emit per-track {name,bpm}
- copy: removed cheap 'not X, Y' contrasts + em dashes per impeccable clarify;
  added 'hover anything' affordance hints
parent 33d49591
......@@ -153,8 +153,21 @@ footer .sig span{color:var(--magenta)}
background:#ff52520f;color:var(--ink);font-size:14px;line-height:1.7}
.banner b{color:#ff7a7a}
/* tooltip — the "what am I looking at" layer */
.tip{position:fixed;z-index:60;pointer-events:none;opacity:0;
transition:opacity .12s ease;max-width:280px;
background:#0c0c0ff2;-webkit-backdrop-filter:blur(9px);backdrop-filter:blur(9px);
border:1px solid var(--hairline);border-radius:10px;padding:9px 12px;
box-shadow:0 10px 30px #000b;font-size:12.5px;line-height:1.45;color:var(--ink)}
.tip b{display:block;font-weight:600;font-size:13px;margin-bottom:2px}
.tip b.mono{font-family:"Geist Mono",monospace;font-size:12.5px;color:#9fd0ff}
.tip .s{color:var(--mute);font-family:"Geist Mono",monospace;font-size:11.5px}
.tip .ex{color:var(--faint);font-size:11px;margin-top:4px;line-height:1.4}
svg [data-tip]{cursor:default;transition:filter .12s ease}
svg [data-tip]:hover{filter:brightness(1.35) saturate(1.1)}
@media(max-width:760px){.rail{display:none}.lens{top:auto;bottom:14px;right:14px}
section{padding:9vh 0}}
section{padding:9vh 0}.tip{display:none}}
</style></head>
<body>
......@@ -175,8 +188,8 @@ footer .sig span{color:var(--magenta)}
<div class="eyebrow">ParVagues</div>
<h1>By the numbers<em>.</em></h1>
<p class="lede">Five years of livecoded sets, <b>read straight back from the source</b>.
Every tempo, sample and phrase below is parsed from the <code>.tidal</code> files.
Nothing here was typed by hand.</p>
Every tempo, sample and phrase below comes from parsing the <code>.tidal</code>
files themselves. Hover anything to see the tracks behind it.</p>
<div class="stats" id="stats"></div>
<div class="scrollcue"><span class="arr"></span> scroll to read</div>
</div>
......@@ -205,6 +218,30 @@ const css=v=>getComputedStyle(document.documentElement).getPropertyValue(v).trim
let EDA=null, TOK=null, FAM={}, LENS="studio";
const CHARTS=[]; // [{el, draw}] for responsive re-render
/* ── tooltip layer ────────────────────────────────────────────────────────── */
const tipEl=document.createElement("div");tipEl.className="tip";document.body.appendChild(tipEl);
/* tag any SVG node: data-tip (title), data-sub (mono line), data-ex (example),
data-mono="1" (render title in mono). Values are escaped on display. */
function tipFor(el,title,sub,ex,mono){el.setAttribute("data-tip",title);
if(sub)el.setAttribute("data-sub",sub);if(ex)el.setAttribute("data-ex",ex);
if(mono)el.setAttribute("data-mono","1");return el;}
function onMove(e){
const t=e.target.closest?e.target.closest("[data-tip]"):null;
if(!t){tipEl.style.opacity="0";return;}
const ti=t.getAttribute("data-tip"),su=t.getAttribute("data-sub"),
ex=t.getAttribute("data-ex"),mono=t.getAttribute("data-mono");
tipEl.innerHTML=`<b class="${mono?"mono":""}">${esc(ti)}</b>`
+(su?`<span class="s">${esc(su)}</span>`:"")
+(ex?`<div class="ex">${esc(ex)}</div>`:"");
tipEl.style.opacity="1";
const r=tipEl.getBoundingClientRect();
let x=e.clientX+15,y=e.clientY+15;
if(x+r.width>innerWidth-8)x=e.clientX-r.width-15;
if(y+r.height>innerHeight-8)y=e.clientY-r.height-15;
tipEl.style.left=Math.max(8,x)+"px";tipEl.style.top=Math.max(8,y)+"px";
}
document.addEventListener("mousemove",onMove);
/* ── data load: inlined block first (deploy), else fetch (dev/serve) ──────── */
function embedded(id){const e=document.getElementById(id);
if(e&&e.textContent.trim()){try{return JSON.parse(e.textContent)}catch(_){}}return null;}
......@@ -302,8 +339,13 @@ function chartTempo(el,W,animate){
// scatter — each track by creation date (the studio cloud)
const cloud=E("g");
const dim = LENS==="club";
pts.forEach(p=>{cloud.appendChild(E("circle",{cx:Xd(p.created),cy:Y(p.bpm),r:3.4,
fill:"#ffffff",opacity:dim?0.10:0.22}));});
pts.forEach(p=>{
const cx=Xd(p.created),cy=Y(p.bpm);
cloud.appendChild(E("circle",{cx,cy,r:3.4,fill:"#ffffff",opacity:dim?0.10:0.22}));
// invisible larger hit target for easy hover
cloud.appendChild(tipFor(E("circle",{cx,cy,r:9,fill:"transparent"}),
p.name, `${p.bpm} BPM · written ${p.created}`));
});
svg.appendChild(cloud);
// the two median lines
......@@ -314,17 +356,24 @@ function chartTempo(el,W,animate){
const active = LENS===key;
const p=E("path",{d,fill:"none",stroke:color,"stroke-width":active?3.2:1.6,
"stroke-linecap":"round","stroke-linejoin":"round",opacity:active?1:0.4});
return {p,ys,map,color,active};
return {p,ys,map,color,active,key};
}
const ls=line(stage,css("--club")||"#ffb454","club");
const lt=line(studio,css("--studio")||"#7db3ff","studio");
[ls,lt].forEach(L=>{if(!L)return;
const where=L.key==="club"?"in the club":"in the studio";
const verb=L.key==="club"?"performed":"written";
svg.appendChild(L.p);
L.ys.forEach(y=>svg.appendChild(E("circle",{cx:X(y),cy:Y(L.map[y]),r:L.active?4:3,
fill:L.color,opacity:L.active?1:0.45})));
L.ys.forEach(y=>{
const d=E("circle",{cx:X(y),cy:Y(L.map[y]),r:L.active?4.5:3,
fill:L.color,opacity:L.active?1:0.45});
tipFor(d,`${where} · ${y}`,`${L.map[y]} BPM (median)`,
`median tempo of every track ${verb} in ${y}`);
svg.appendChild(d);
});
// end label
const ly=L.ys[L.ys.length-1];
const lab=TX(X(ly)+10,Y(L.map[ly])+4,L.color===(css("--club")||"#ffb454")?"in the club":"in the studio","dlabel");
const lab=TX(X(ly)+10,Y(L.map[ly])+4,where,"dlabel");
lab.setAttribute("fill",L.color);lab.setAttribute("opacity",L.active?1:0.5);
svg.appendChild(lab);
});
......@@ -349,6 +398,8 @@ function chartHist(el,W,animate){
const hot = k===120;
const r=E("rect",{x:x+2,y,width:bw-4,height:bh,rx:3,
fill:hot?"var(--ink)":"#ffffff2e","data-anim":"bary","data-ox":x,"data-oy":m.t+ih});
tipFor(r,`${k}${k+9} BPM`,`${h[k]} track${h[k]>1?"s":""}`,
hot?"the busiest band, most tracks sit at 120":"");
g.appendChild(r);
g.appendChild(TX(x+bw/2,m.t+ih+17,k,"atxt","middle"));
g.appendChild(TX(x+bw/2,y-5,h[k],"barv","middle")).setAttribute("font-size","10");
......@@ -391,8 +442,11 @@ function chartBreakout(el,W,animate){
const x0=Xy(Number(y)), x1=Xy(Number(y)+1), cw=(x1-x0)*0.62, cx=x0+(x1-x0-cw)/2;
const bh=Math.max(2,(n/gmx)*topH), yy=topY+topH-bh;
const hot=Number(y)===2024;
gTop.appendChild(E("rect",{x:cx,y:yy,width:cw,height:bh,rx:4,
fill:hot?"var(--magenta)":"#ffffff24","data-anim":"bary","data-ox":cx,"data-oy":topY+topH}));
gTop.appendChild(tipFor(E("rect",{x:cx,y:yy,width:cw,height:bh,rx:4,
fill:hot?"var(--magenta)":"#ffffff24","data-anim":"bary","data-ox":cx,"data-oy":topY+topH}),
`${y}`,`${n} gig${n>1?"s":""} played`,
hot?"the breakout year: gigs jumped from 4 to 14":
(Number(y)===2026?"2026 so far, the year is still running":"")));
gTop.appendChild(TX(x0+(x1-x0)/2,yy-6,n,"barv","middle"));
}
svg.appendChild(gTop);
......@@ -405,8 +459,12 @@ function chartBreakout(el,W,animate){
for(const[ym,n]of Object.entries(voc)){
const x=Xm(mi(ym)), bh=Math.max(1.5,(n/vmx)*botH), yy=botY+botH-bh;
const hot=ym.startsWith("2024-07");
const r=E("rect",{x:x-cw/2,y:yy,width:cw,height:bh,rx:2,
const r=E("rect",{x:x-Math.max(cw,5)/2,y:yy,width:Math.max(cw,5),height:bh,rx:2,
fill:hot?"var(--magenta)":"#ffffff2e","data-anim":"bary","data-ox":x,"data-oy":botY+botH});
const[yr,mo]=ym.split("-");
const moName=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][+mo-1];
tipFor(r,`${moName} ${yr}`,`${n} sample folder${n>1?"s":""} imported`,
hot?"the burst: 334 folders linked in one month, a wholesale palette change":"");
gBot.appendChild(r);
if(n>=80)gBot.appendChild(TX(x,yy-6,n,"barv","middle"));
}
......@@ -428,6 +486,10 @@ function chartFamilies(el,W,animate){
const uncl=Object.values(EDA.unclassified_top).reduce((a,b)=>a+b,0);
const entries=Object.entries(fam).sort((a,b)=>b[1]-a[1]);
const total=entries.reduce((a,[,n])=>a+n,0)+uncl;
// example members per family, from the top-sounds index
const members={};
for(const snd of Object.keys(EDA.palette_top||{})){const f=sampleFamily(snd);
if(f){(members[f.key]=members[f.key]||[]).push(snd);}}
const H=92, m={l:0,r:0};
const iw=W;
const svg=E("svg",{viewBox:`0 0 ${W} ${H}`});
......@@ -435,8 +497,13 @@ function chartFamilies(el,W,animate){
let x=0; const g=E("g");
const seg=(key,label,n,color,glyph)=>{
const w=Math.max(1,(n/total)*iw);
const r=E("rect",{x,y:barY,width:w,height:barH,fill:color,
"data-anim":"barx","data-ox":x,"data-oy":barY});
const pct=Math.round(n/total*100);
const eg=(members[key]||[]).slice(0,4).join(", ");
const r=tipFor(E("rect",{x,y:barY,width:w,height:barH,fill:color,
"data-anim":"barx","data-ox":x,"data-oy":barY}),
`${label} family`,`${n} uses · ${pct}% of the palette`,
key==="_u"?"rare one-off samples the classifier doesn't claim":
(eg?"e.g. "+eg:""));
g.appendChild(r);
if(w>34){ // glyph + count inside if room
g.appendChild(TX(x+w/2,barY+barH/2+1,glyph,"","middle")).setAttribute("style",
......@@ -455,6 +522,18 @@ function chartFamilies(el,W,animate){
/* ══════════════════════════════════════════════════════════════════════════
STORY 4 — The accent (signature idioms)
══════════════════════════════════════════════════════════════════════════ */
const IDIOM_GLOSS={
"f*16":"a 16-step on/off grid: the gMask/gMute boolean that gates a pattern",
"t . <f t f <f t>> <t f f <t f>>":"a cycling true/false mask: which steps play, rotating each bar",
"t(4,8,1)":"Euclidean rhythm: 4 hits across 8 steps, rotated by 1",
"[jazz,kick]":"a jazz break layered under a four-on-the-floor kick",
"k . ~ k ~ ~":"a kick figure with rests carved out",
"[0,12]":"a note stacked with its octave",
"<0 1 2 3>":"step through four values, one per cycle",
"f(4,8)":"a Euclidean boolean mask, 4 active of 8",
"<f!24 t!8>":"24 cycles muted, then 8 cycles on: a long breakdown gate",
"[snare,snare]":"a doubled snare hit",
};
function chartIdioms(el,W,animate){
const rows=EDA.idioms_top.slice(0,9);
const rh=46, m={l:0,r:54,t:8,b:8};
......@@ -467,8 +546,9 @@ function chartIdioms(el,W,animate){
rows.forEach((row,i)=>{
const y=m.t+i*rh, cy=y+rh/2;
const bw=Math.max(2,(row.n_tracks/mx)*iw);
g.appendChild(E("rect",{x:labelW,y:y+7,width:bw,height:rh-20,rx:4,
fill:i===0?"var(--ink)":"#ffffff26","data-anim":"barx","data-ox":labelW,"data-oy":y}));
g.appendChild(tipFor(E("rect",{x:labelW,y:y+7,width:bw,height:rh-20,rx:4,
fill:i===0?"var(--ink)":"#ffffff26","data-anim":"barx","data-ox":labelW,"data-oy":y}),
row.norm,`in ${row.n_tracks} of 73 tracks`,IDIOM_GLOSS[row.norm]||"",true));
// mini-notation label (mono), right-aligned to bar start
const lab=TX(labelW-12,cy+4,row.norm,"barlabel","end");
lab.setAttribute("fill",i===0?"var(--ink)":"var(--mute)");
......@@ -502,32 +582,46 @@ function chartCollab(el,W,animate){
svg.appendChild(TX(m.l,m.t-12,"BPM →","atxt b","start")).setAttribute("opacity","0");
const g=E("g");
rows.forEach((row,i)=>{
const[who,d]=row, y=m.t+i*rh, cy=y+24;
const[who,d]=row, y=m.t+i*rh, cy=y+22;
const solo=/solo/.test(who);
const c=solo?"#ffffff":(css("--club")||"#ffb454");
const label=who.replace("(solo)","").trim();
const me=solo?"solo work":label;
// name + count
const nm=TX(m.l-14,cy+1,who.replace("(solo)"," ·solo"),"dlabel","end");
const nm=TX(m.l-14,cy+1,solo?"solo":label,"dlabel","end");
nm.setAttribute("fill","var(--ink)");nm.setAttribute("font-size","14");
g.appendChild(nm);
g.appendChild(TX(m.l-14,cy+17,d.n+" track"+(d.n>1?"s":""),"atxt","end"));
// range strip min..max
// faint range strip min..max
g.appendChild(E("line",{x1:X(d.bpm_min),x2:X(d.bpm_max),y1:cy,y2:cy,
stroke:c,"stroke-width":3,"stroke-linecap":"round",opacity:0.32}));
g.appendChild(E("circle",{cx:X(d.bpm_min),cy,r:3,fill:c,opacity:0.5}));
g.appendChild(E("circle",{cx:X(d.bpm_max),cy,r:3,fill:c,opacity:0.5}));
// median marker
g.appendChild(E("circle",{cx:X(d.bpm_median),cy,r:6,fill:c}));
g.appendChild(TX(X(d.bpm_median),cy-12,d.bpm_median,"barv","middle")).setAttribute("fill",c);
// distinctive sample chips (tell)
stroke:c,"stroke-width":2,"stroke-linecap":"round",opacity:0.22}));
// every track as a dot; same-BPM tracks pile into one bigger dot
const byB={};(d.tracks||[]).forEach(tk=>{(byB[tk.bpm]=byB[tk.bpm]||[]).push(tk.name);});
Object.entries(byB).forEach(([bpm,names])=>{
const cnt=names.length, rad=3.5+Math.sqrt(cnt-1)*3.2, cx=X(+bpm);
const dot=E("circle",{cx,cy,r:rad,fill:c,opacity:0.84,stroke:"#0a0a0a","stroke-width":0.6});
tipFor(dot, cnt===1?names[0]:`${cnt} tracks at ${+bpm} BPM`, `${+bpm} BPM`,
cnt===1?`a ${me} track`:names.join(", "));
g.appendChild(dot);
});
// median tick + value
const mx2=X(d.bpm_median);
g.appendChild(tipFor(E("line",{x1:mx2,x2:mx2,y1:cy-14,y2:cy+14,stroke:c,"stroke-width":2}),
`${solo?"solo":label} · median`, `${d.bpm_median} BPM`,
`half their tracks fall below this tempo, half above`));
g.appendChild(TX(mx2,cy-19,d.bpm_median,"barv","middle")).setAttribute("fill",c);
// distinctive sample chips (the tell)
const tells=(d.distinctive_samples||[]).slice(0,3);
let tx=m.l;
tells.forEach(s=>{
const f=sampleFamily(s.sound);const col=f?f.base:"#8a8a90";
const txt=(f?f.glyph+" ":"")+s.sound;
const t=TX(tx,cy+30,txt,"");t.setAttribute("font-family","Geist Mono, monospace");
const t=TX(tx,cy+33,txt,"");t.setAttribute("font-family","Geist Mono, monospace");
t.setAttribute("font-size","11.5");t.setAttribute("fill",col);
tipFor(t, s.sound, ${s.lift} lift · in ${s.n} of their tracks`,
`turns up far more with ${me} than across the whole corpus`);
g.appendChild(t);
tx+=txt.length*7.0+18;
tx+=txt.length*7.0+20;
});
});
svg.appendChild(g);el.appendChild(svg);
......@@ -552,9 +646,12 @@ function chartStaples(el,W,animate){
const y=m.t+i*rh, cy=y+rh/2;
const bw=Math.max(2,(row.gigs/mx)*iw);
const isCafe=cafe.test(row.name);
g.appendChild(E("rect",{x:labelW,y:y+6,width:bw,height:rh-16,rx:4,
g.appendChild(tipFor(E("rect",{x:labelW,y:y+6,width:bw,height:rh-16,rx:4,
fill:i===0?"var(--ink)":(isCafe?cafeC:"#ffffff24"),
opacity:isCafe&&i!==0?0.8:1,"data-anim":"barx","data-ox":labelW,"data-oy":y}));
opacity:isCafe&&i!==0?0.8:1,"data-anim":"barx","data-ox":labelW,"data-oy":y}),
row.name,`played in ${row.gigs} set${row.gigs>1?"s":""}`,
i===0?"the most-played track in the catalog":
(isCafe?"part of the Café trilogy (Tiède · Glacé · Bouillant)":"")));
const lab=TX(labelW-12,cy+4,row.name,"barlabel","end");
lab.setAttribute("fill",i===0?"var(--ink)":"var(--mute)");lab.setAttribute("font-family","Geist, sans-serif");
g.appendChild(lab);
......@@ -567,9 +664,9 @@ function chartStaples(el,W,animate){
/* ── story scaffolding ────────────────────────────────────────────────────── */
const STORIES=[
{id:"creep",n:"01",t:"The slow climb",lens:true,
dek:`My tempo crept from <b>110 to 126 BPM</b> over five years. Not a decision, a drift.
The cloud is every track by when it was written; the lines are the yearly medians
<b>in the studio</b> (when I made it) and <b>in the club</b> (when I played it).`,
dek:`My tempo crept from <b>110 to 126 BPM</b> over five years, a slow drift I never set out
to make. The cloud is every track by when it was written; the lines are the yearly
medians <b>in the studio</b> (when I made it) and <b>in the club</b> (when I played it).`,
build:s=>{
const c=chartBox(s,chartTempo,true);
legend(s,[["in the studio","var(--studio)","—"],["in the club","var(--club)","—"],
......@@ -577,7 +674,7 @@ const STORIES=[
const sub=div(s,"sub");
const p1=panel(sub,"Where the tracks sit","73 tracks by tempo · the floor is 80, the ceiling 170");
chartBox(p1,chartHist,true);
const p2=panel(sub,"Felt vs written","tracks tagged at double or half the score tempo — half-time notation, not error");
const p2=panel(sub,"Felt vs written","tracks tagged at double or half the score tempo (half-time feel, counted two ways)");
twoX(p2,EDA.tempo.ac_delta.filter(d=>Math.abs(Math.abs(d.delta)-d.score_bpm)<1.5||Math.abs(d.delta)>=55).slice(0,4));
note(s,`Tempo is parsed from each track's <code>setcps</code>. A few sit at exactly half or
double their tagged BPM: that's half-time feel written one way and counted another, kept honest here.`);
......@@ -611,7 +708,8 @@ const STORIES=[
note(s,`From <b>${EDA.idioms_counts.total.toLocaleString()}</b> distinct phrases mined across the corpus,
<b>${EDA.idioms_counts.shared}</b> recur in two or more tracks. These nine are the most shared:
boolean masks (<code>f*16</code>, <code>t . &lt;f t f…&gt;</code>), euclids (<code>t(4,8,1)</code>)
and stacked drum hits. The signature isn't a sound, it's a way of writing rhythm.`);
and stacked drum hits. The signature lives in how the rhythm is written, more than in any
one sound. Hover a bar for what each phrase does.`);
}},
{id:"collab",n:"05",t:"Who I play with",lens:false,
dek:`Solo, I sit around <b>117 BPM</b>. Every collaborator pulls somewhere else, and
......@@ -629,8 +727,8 @@ const STORIES=[
chartBox(s,chartStaples,true);
legend(s,[["most-played","var(--ink)","■"],["the Café trilogy","var(--keys,#00abbd)","■"]]);
note(s,`Counted from the gig setlists: how many distinct sets each track has appeared in.
The long tail (one or two gigs) is most of the catalog. These are the ones that earned a
permanent slot.`);
Most of the catalog sits in the long tail at one or two gigs; these few hold a permanent
slot. Hover a bar for the count.`);
}},
];
......@@ -720,7 +818,7 @@ function observe(){
document.querySelectorAll(".rail a").forEach(a=>a.classList.toggle("on",a.dataset.id===id));
const aware=e.target.dataset.lens==="1";
lensEl.classList.toggle("off",!aware);
lensEl.title=aware?"toggle the tempo lens":"this chart is score-derived — one lens only";
lensEl.title=aware?"toggle the tempo lens":"this chart is score-derived, so it has just one lens";
lensLbl.textContent=aware?"lens":"one lens";
}});},{threshold:0.4});
secs.forEach(s=>so.observe(s));
......
......@@ -932,6 +932,16 @@
"bpm_median": 150,
"bpm_min": 140.0,
"bpm_max": 160.0,
"tracks": [
{
"name": "Blue Gold",
"bpm": 140.0
},
{
"name": "Ghosts in the T01l3ts",
"bpm": 160.0
}
],
"distinctive_samples": [
{
"sound": "moog",
......@@ -950,14 +960,24 @@
"bpm_median": 131,
"bpm_min": 120.0,
"bpm_max": 142.0,
"tracks": [
{
"name": "Love First",
"bpm": 120.0
},
{
"name": "So Good",
"bpm": 142.0
}
],
"distinctive_samples": [
{
"sound": "risers",
"sound": "moogBass",
"lift": 4.6,
"n": 2
},
{
"sound": "moogBass",
"sound": "risers",
"lift": 4.6,
"n": 2
},
......@@ -973,6 +993,56 @@
"bpm_median": 138,
"bpm_min": 102.0,
"bpm_max": 170.0,
"tracks": [
{
"name": "Long Way",
"bpm": 102.0
},
{
"name": "Des Efforts",
"bpm": 120.0
},
{
"name": "Piment Bresilien",
"bpm": 124.0
},
{
"name": "Biscuit Acide",
"bpm": 128.0
},
{
"name": "Desire",
"bpm": 129.0
},
{
"name": "Acidule",
"bpm": 135.0
},
{
"name": "Esperluette",
"bpm": 140.0
},
{
"name": "Jeudi Drill",
"bpm": 140.0
},
{
"name": "Permanence",
"bpm": 150.0
},
{
"name": "Nouveau Punk",
"bpm": 155.0
},
{
"name": "Aria Sans Serif",
"bpm": 160.0
},
{
"name": "PunkAChien",
"bpm": 170.0
}
],
"distinctive_samples": [
{
"sound": "clubkick",
......@@ -1011,6 +1081,188 @@
"bpm_median": 117,
"bpm_min": 80.0,
"bpm_max": 165.0,
"tracks": [
{
"name": "Sessions Break",
"bpm": 80.0
},
{
"name": "Green Land",
"bpm": 80.0
},
{
"name": "Paris",
"bpm": 80.0
},
{
"name": "'Plosive",
"bpm": 80.0
},
{
"name": "Premiere Grillade",
"bpm": 80.0
},
{
"name": "Reboot",
"bpm": 80.0
},
{
"name": "Venons Ensemble",
"bpm": 85.0
},
{
"name": "Contre visite",
"bpm": 90.0
},
{
"name": "Fabuleux",
"bpm": 93.0
},
{
"name": "L'Or Bleu",
"bpm": 94.0
},
{
"name": "Lendemain Divin",
"bpm": 95.0
},
{
"name": "Ton Numero",
"bpm": 99.0
},
{
"name": "CBOW",
"bpm": 100.0
},
{
"name": "Orage",
"bpm": 104.0
},
{
"name": "Empreinte du numerique",
"bpm": 110.0
},
{
"name": "It's About Time",
"bpm": 110.0
},
{
"name": "Lunar",
"bpm": 110.0
},
{
"name": "Solar",
"bpm": 110.0
},
{
"name": "Nouveau Soleil",
"bpm": 110.0
},
{
"name": "Ere de Jeu",
"bpm": 110.0
},
{
"name": "La Révolution Sera Samplée",
"bpm": 114.0
},
{
"name": "Invoque l'ete",
"bpm": 115.0
},
{
"name": "Oct4 Glitch Sauvages",
"bpm": 117.0
},
{
"name": "Quand on Décolle",
"bpm": 120.0
},
{
"name": "RAISE",
"bpm": 120.0
},
{
"name": "La fin de l'insouciance",
"bpm": 120.0
},
{
"name": "Michael",
"bpm": 120.0
},
{
"name": "Sunny Side Up",
"bpm": 120.0
},
{
"name": "Café Bouillant",
"bpm": 120.0
},
{
"name": "Café Glacé",
"bpm": 120.0
},
{
"name": "Salut Nu",
"bpm": 120.0
},
{
"name": "L'été à Mauerpark",
"bpm": 120.0
},
{
"name": "Take 5 Drops",
"bpm": 124.0
},
{
"name": "Force Motrice",
"bpm": 125.0
},
{
"name": "Café Tiède",
"bpm": 125.0
},
{
"name": "Bain électrique",
"bpm": 128.0
},
{
"name": "WAP",
"bpm": 133.0
},
{
"name": "Prestance",
"bpm": 134.0
},
{
"name": "Lady Perplexity",
"bpm": 138.0
},
{
"name": "You My Sunshine",
"bpm": 144.0
},
{
"name": "Nuit Agitée",
"bpm": 160.0
},
{
"name": "Alerte Verte",
"bpm": 160.0
},
{
"name": "Something about Drums",
"bpm": 160.0
},
{
"name": "VelociTeuf",
"bpm": 165.0
},
{
"name": "Break the Loop",
"bpm": 165.0
}
],
"distinctive_samples": [
{
"sound": "electro1",
......
......@@ -181,6 +181,9 @@ def collab_fingerprint(T, tempo):
"bpm_median": round(st.median(bpms)) if bpms else None,
"bpm_min": min(bpms) if bpms else None,
"bpm_max": max(bpms) if bpms else None,
"tracks": sorted(({"name": t["name"], "bpm": tempo[t["track"]]["bpm"]}
for t in tracks if t["track"] in tempo),
key=lambda x: x["bpm"]),
"distinctive_samples": [{"sound": s, "lift": round(l, 1), "n": c}
for s, l, c in distinctive],
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment