← back to Rose City Rooftop Solstice
Make music LIVE AUDIO-REACTIVE: decode archive.org tracks through a Web Audio analyser and drive the whole scene (beat/bass/level) — the entire gathering now moves to the music
9c4c7bf475cc41eb16cbd3c3b06e9e89ec3bd557 · 2026-09-01 17:24:48 -0700 · Steve Abrams
Files touched
Diff
commit 9c4c7bf475cc41eb16cbd3c3b06e9e89ec3bd557
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 1 17:24:48 2026 -0700
Make music LIVE AUDIO-REACTIVE: decode archive.org tracks through a Web Audio analyser and drive the whole scene (beat/bass/level) — the entire gathering now moves to the music
---
README.md | 37 ++++++++---
index.html | 217 ++++++++++++++++++++++++++++++++++++++++++++-----------------
2 files changed, 185 insertions(+), 69 deletions(-)
diff --git a/README.md b/README.md
index d20b500..859b1c1 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ or you can pin one.
Keyboard: `Space` pause · `1 2 3` cameras · `R` restart · `N` names ·
`T` trails · `D` diagrams · `M` reduced motion · `C` hide panel.
-## Music (optional, from the archive)
+## Music — live and audio-reactive (from the archive)
The control dock has a **Music · from the archive** section that streams live
**Grateful Dead** recordings from the [Internet Archive](https://archive.org)'s
@@ -48,14 +48,33 @@ The control dock has a **Music · from the archive** section that streams live
taping-friendly, and these recordings are shared with their blessing; each
now-playing show links back to its archive.org item for credit.
-- **Play music** starts a curated, verified show (Cornell '77 by default).
-- **Shuffle show** queries the archive live and loads a random popular show.
-- **⏮ / ⏭** step tracks; the volume slider is independent of visual tempo.
-
-This is the *only* part of the piece that touches the network, and it's **audio
-only** — it starts on a click (respecting autoplay rules), and if you're offline
-it simply says so. The visuals stay fully self-contained and, per the honest
-note, **never** react to the audio.
+**When music plays, the whole scene reacts to it — live.** Each track is fetched
+and decoded, then played through a Web Audio graph
+(`BufferSource → Analyser → Gain`). Every frame the analyser is read for
+**bass / mid / treble / overall level** plus a **beat onset**, and those features
+drive *everything*:
+
+- the master clock speeds up with musical energy (the whole gathering moves faster when it's loud),
+- **dancers** grow their moves with loudness and **lift on every beat**,
+- the **rose sculpture** pulses and blooms with the bass and beat,
+- **lanterns** and the **city glow** breathe with the music,
+- **bears** sway harder on bass, **cats** leap higher, **pigeons** get extra lift on beats, **moths** flutter with the treble.
+
+Controls: **Play/Pause**, **Shuffle show** (queries the archive live for a random
+popular show), **⏮ / ⏭** track stepping, and a volume slider (independent of the
+analysis, so quiet music still drives the visuals).
+
+Why buffer-and-decode rather than a plain `<audio>` element? Reading a stream's
+spectrum through Web Audio needs the decoded samples in the graph; the
+buffer-source path guarantees that reactivity works on every browser (it's the
+same synthesis path as an oscillator), which a `MediaElementSource` does not.
+Tradeoff: each track is fetched in full first (the light 64 kbps derivative,
+~2–3 MB, ~2 s), and the next track is prefetched while the current one plays.
+
+This music is the **only** part of the piece that touches the network, and it's
+**audio only** — it starts on a click (respecting autoplay rules) and reports
+plainly if you're offline. With the music **off**, the visuals are fully
+self-contained and run on their authored rhythm.
## An honest note
diff --git a/index.html b/index.html
index 9d79b0a..502a127 100644
--- a/index.html
+++ b/index.html
@@ -231,7 +231,7 @@
<div class="grp" role="group" aria-label="Music from the Internet Archive">
<h2 style="margin-top:0">Music · from the archive</h2>
- <p class="hint" id="npLine">Optional: live Grateful Dead recordings streamed from the Internet Archive. Even while it plays, the visuals never sync to the audio — their rhythm stays authored.</p>
+ <p class="hint" id="npLine">Press play: live Grateful Dead recordings stream from the Internet Archive, and the whole scene moves to them — dancers step on the beat, the rose pulses with the bass, lanterns and glow breathe with the music.</p>
<div class="actions">
<button class="btn" id="mPlay" aria-pressed="false">Play music</button>
<button class="btn" id="mShuffle">Shuffle show</button>
@@ -263,7 +263,7 @@
<button class="btn dock-toggle" id="dockToggle" aria-expanded="true" aria-controls="controls">Hide panel</button>
<div class="toolbar">
- <p class="note"><b>An honest note.</b> This piece keeps time on its own. Its rhythm, swells and blooms are <b>authored</b> — a composed visual tempo — <em>not</em> reactive to any music or microphone. Optional music streams live from the <b>Internet Archive</b>’s Grateful Dead collection; even with it playing, the visuals never sync to the audio. Turn on <b>Reduced motion</b> for a calm, still composition.</p>
+ <p class="note"><b>An honest note.</b> With the music <b>off</b>, this piece keeps its own time — an <b>authored</b> visual tempo. Press <b>Play music</b> and it becomes <b>live audio-reactive</b>: the scene analyzes the streaming Grateful Dead set (from the <b>Internet Archive</b>) in real time, so the beat, bass and brightness drive the whole gathering. Change the show and the whole scene changes with it. <b>Reduced motion</b> holds a calm, still composition.</p>
</div>
<script>
@@ -314,8 +314,10 @@ const S = {
cam:'rooftop', targetScene:0, autoScene:true, sceneT:0,
tempo:1, density:1, isolate:'all', paused:false, reduced:false,
names:false, trails:true, diagrams:false,
- t:0, // animation clock (scaled by tempo)
+ t:0, // animation clock (scaled by tempo AND by live audio energy)
cfrac:SCENES[0].complexity,
+ // live audio-reactivity features (all 0 / inert when no music is playing)
+ audio:{ level:0, bass:0, mid:0, treble:0, beat:0, active:false, _prev:0, _avg:0, _refr:0 },
};
/* current, continuously-lerped palette + numeric fields */
@@ -602,8 +604,9 @@ function drawCityGlow(){
// luminous band where the city meets the rooftops
const [,y]=project(0, 320, 1);
const g=ctx.createLinearGradient(0,y-60,0,y+120);
+ const gA=0.18*(S.audio.active?(1+0.8*S.audio.level):1);
g.addColorStop(0, rgb(P.glow,0));
- g.addColorStop(0.5, rgb(P.glow,0.18));
+ g.addColorStop(0.5, rgb(P.glow,gA));
g.addColorStop(1, rgb(P.glow,0));
ctx.fillStyle=g; ctx.fillRect(0,y-60,VW,180);
}
@@ -740,7 +743,7 @@ const BODY_COLORS_RGB = BODY_COLORS.map(hx);
function drawLantern(L,i){
const [x,y]=project(L.x,L.y);
const s=cam.scale;
- const pulse=0.75+0.25*Math.sin(S.t*1.5+i);
+ const pulse=0.75+0.25*Math.sin(S.t*1.5+i) + (S.audio.active?(S.audio.bass*0.5+S.audio.beat*0.5):0);
const rad=14*s*pulse*(0.7+0.6*P.star);
const g=ctx.createRadialGradient(x,y,1,x,y,rad*2.6);
g.addColorStop(0, rgb(P.lantern, 0.9));
@@ -761,9 +764,12 @@ function figure(x,y,sc,ph,col,style,lean,alpha){
ctx.globalAlpha=(alpha==null?1:alpha); ctx.fillStyle=col;
ctx.beginPath(); ctx.arc(x,y-6*s,2.4*s,0,TAU); ctx.fill(); ctx.globalAlpha=1; return;
}
- const bob = S.reduced?0:Math.sin(ph)*2*s;
- const swing = S.reduced?0.25:Math.sin(ph);
- lean = (lean||0);
+ const AA=S.audio;
+ const amp = AA.active ? (0.55 + 1.15*AA.level) : 1; // bigger moves when louder
+ const pop = AA.active ? AA.beat*7*s : 0; // every dancer lifts on the beat
+ const bob = (S.reduced?0:Math.sin(ph)*2*s*amp) + pop;
+ const swing = (S.reduced?0.25:Math.sin(ph))*amp;
+ lean = (lean||0)*(AA.active?(0.7+0.8*amp*0.6):1);
ctx.save();
ctx.globalAlpha=(alpha==null?1:alpha);
ctx.translate(x, y-bob);
@@ -800,10 +806,11 @@ function figure(x,y,sc,ph,col,style,lean,alpha){
function drawRose(){
const [x,y]=project(ROSE.x, ROSE.y);
const s=cam.scale;
- const bloom = clamp((S.cfrac-0.4)/0.6, 0, 1); // fuller in later scenes
+ const AA=S.audio;
+ const bloom = clamp((S.cfrac-0.4)/0.6, 0, 1) * (AA.active?(1+0.35*AA.level):1); // fuller in later scenes + with music
const rot = S.reduced?0.4:S.t*0.35;
const layers=5, petalsPer=8;
- const baseR=ROSE.R*s*(0.42+0.28*bloom);
+ const baseR=ROSE.R*s*(0.42+0.28*bloom) * (AA.active?(1 + 0.28*AA.bass + 0.16*AA.beat):1);
// ground glow
const g=ctx.createRadialGradient(x,y,2,x,y,baseR*2.6);
g.addColorStop(0, rgb(P.glow,0.85));
@@ -914,7 +921,7 @@ function drawBears(){
BEARS.forEach(b=>{
const [x,y]=project(b.x,b.y); const s=b.s*cam.scale;
if(s<0.5) return;
- const sway=S.reduced?0:Math.sin(S.t*1.1+b.ph)*0.10;
+ const sway=S.reduced?0:Math.sin(S.t*1.1+b.ph)*0.10*(S.audio.active?(1+1.3*S.audio.bass):1);
ctx.save(); ctx.translate(x,y); ctx.rotate(sway);
const col=mix(P.roof,[60,44,38],0.5);
ctx.fillStyle=rgb(col);
@@ -957,7 +964,7 @@ function drawCats(){
CATS.forEach(c=>{
const t=(Math.sin(S.t*c.sp+c.ph)*0.5+0.5); // 0..1 leap progress
const wx=lerp(c.p0[0],c.p1[0],t);
- const arc=Math.sin(t*Math.PI)*34;
+ const arc=Math.sin(t*Math.PI)*34*(S.audio.active?(1+0.6*S.audio.level):1);
const wy=lerp(c.p0[1],c.p1[1],t)-arc-4;
const [x,y]=project(wx,wy); const s=cam.scale;
if(s<0.5) return;
@@ -983,7 +990,8 @@ function drawCats(){
function drawPigeons(){
const t=S.t*0.5*S.tempo0 + PIGEONS.cycle;
const phase=(t%8)/8; // 0..1 launch/rise/settle
- const rise = phase<0.7? (phase/0.7): (1-(phase-0.7)/0.3);
+ let rise = phase<0.7? (phase/0.7): (1-(phase-0.7)/0.3);
+ if(S.audio.active) rise = clamp(rise + S.audio.beat*0.35 + S.audio.treble*0.12, 0, 1.2);
const [hx0,hy0]=PIGEONS.home;
PIGEONS.birds.forEach(bd=>{
const spiral = t*2 + bd.a;
@@ -1005,8 +1013,9 @@ function drawMoths(){
MOTHS.forEach(m=>{
const L=LANTERNS[m.L];
const a=m.a + S.t*m.sp;
- const wx=L.x + Math.cos(a)*m.r + Math.sin(S.t*3+m.ph)*3;
- const wy=L.y + Math.sin(a*1.3)*m.r*0.7 + Math.cos(S.t*3.4+m.ph)*3;
+ const jit=3+(S.audio.active?S.audio.treble*9:0);
+ const wx=L.x + Math.cos(a)*m.r + Math.sin(S.t*3+m.ph)*jit;
+ const wy=L.y + Math.sin(a*1.3)*m.r*0.7 + Math.cos(S.t*3.4+m.ph)*jit;
const [x,y]=project(wx,wy); const s=cam.scale;
const flap=Math.sin(S.t*18+m.ph);
ctx.save(); ctx.translate(x,y);
@@ -1153,9 +1162,12 @@ let last=performance.now();
S.tempo0=1; // smoothed tempo used inside figure phase math
function frame(now){
const rawdt=Math.min(0.05,(now-last)/1000); last=now;
+ sampleAudio(); // read the live spectrum every frame
if(!S.paused && !S.reduced){
S.tempo0=lerp(S.tempo0,S.tempo,0.1);
- S.t += rawdt*S.tempo;
+ // the master clock speeds up with musical energy → the whole scene moves to the music
+ const boost = S.audio.active ? (S.audio.level*1.3 + S.audio.beat*0.25) : 0;
+ S.t += rawdt*S.tempo*(1+boost);
// scene drifting
if(S.autoScene){
S.sceneT += rawdt;
@@ -1274,11 +1286,14 @@ window.addEventListener('keydown',e=>{
});
/* ===================================================================
- Optional live music — Grateful Dead recordings streamed from the
- Internet Archive (archive.org). Public, keyless API. The band was
- famously taping-friendly; these are shared with their blessing.
- NOTE: this adds a runtime network dependency for AUDIO ONLY. The
- visuals stay fully self-contained/offline and never react to sound.
+ Optional LIVE-REACTIVE music — Grateful Dead sets streamed from the
+ Internet Archive (archive.org; public keyless API — the band was
+ taping-friendly, shared with their blessing). Each track is fetched
+ and decoded into an AudioBuffer, then played through a Web Audio
+ graph (BufferSource → Analyser → Gain → speakers), so the live
+ spectrum drives every element on screen. This is the ONLY part that
+ touches the network, and it's audio-only; the visuals stay
+ self-contained and react ONLY while a set is playing.
=================================================================== */
const MUSIC = {
// curated, verified-real item ids — filenames are always fetched at runtime, never hardcoded
@@ -1290,67 +1305,149 @@ const MUSIC = {
{id:'gd73-02-15.sbd.hall.1580.sbeok.shnf', label:'Dane County Coliseum · Feb 15, 1973'},
{id:'gd71-08-06.aud.bertrando.yerys.129.sbeok.shnf',label:'Hollywood Palladium · Aug 6, 1971'},
],
- audio:new Audio(), playlist:[], idx:0, showId:'', showLabel:'', loading:false, wantPlay:false,
+ playlist:[], idx:0, showId:'', showLabel:'', loading:false, wantPlay:false,
+ ctx:null, analyser:null, gain:null, freq:null,
+ srcNode:null, buffers:new Map(), playing:false, _stopping:false, _loadToken:0,
};
-MUSIC.audio.preload='none';
-MUSIC.audio.volume=0.55;
const iaItem = id => 'https://archive.org/details/'+id;
const iaFile = (id,name) => 'https://archive.org/download/'+id+'/'+encodeURIComponent(name);
-const npEl=()=>document.getElementById('npLine');
-function setNP(t){ const e=npEl(); if(e) e.textContent=t; }
+function setNP(t){ const e=document.getElementById('npLine'); if(e) e.textContent=t; }
function setCredit(id){
const c=document.getElementById('mCredit'); if(!c) return;
- if(!id){ c.textContent=''; return; }
- c.innerHTML='Source: <a href="'+iaItem(id)+'" target="_blank" rel="noopener noreferrer">archive.org ↗</a> · taping-friendly recording, shared with the band’s blessing.';
+ c.innerHTML = id ? 'Source: <a href="'+iaItem(id)+'" target="_blank" rel="noopener noreferrer">archive.org ↗</a> · taping-friendly recording, shared with the band’s blessing.' : '';
}
function updateMusicBtn(playing){
const b=document.getElementById('mPlay'); if(!b) return;
b.textContent=playing?'Pause music':'Play music'; b.setAttribute('aria-pressed',String(playing));
}
+// Build the Web Audio graph once (on a user gesture). Degrades to non-reactive on failure.
+function ensureCtx(){
+ if(MUSIC.ctx){ if(MUSIC.ctx.state==='suspended') MUSIC.ctx.resume(); return true; }
+ try{
+ const AC=window.AudioContext||window.webkitAudioContext;
+ MUSIC.ctx=new AC();
+ MUSIC.analyser=MUSIC.ctx.createAnalyser();
+ MUSIC.analyser.fftSize=1024; MUSIC.analyser.smoothingTimeConstant=0.82;
+ MUSIC.gain=MUSIC.ctx.createGain(); MUSIC.gain.gain.value=0.55;
+ MUSIC.analyser.connect(MUSIC.gain); MUSIC.gain.connect(MUSIC.ctx.destination); // analyser BEFORE gain → full spectrum at any volume
+ MUSIC.freq=new Uint8Array(MUSIC.analyser.frequencyBinCount);
+ MUSIC.ctx.resume();
+ return true;
+ }catch(e){ MUSIC.ctx=null; return false; }
+}
+
+// one mp3 per track; prefer the light 64Kbps derivative so loads are fast (~2–3MB / ~2s)
+function pickFiles(files){
+ const mp3=files.filter(f=>/\.mp3$/i.test(f.name||''));
+ const low=mp3.filter(f=>/64Kbps/i.test(String(f.format||'')));
+ const vbr=mp3.filter(f=>/VBR/i.test(String(f.format||'')));
+ const use=(low.length?low:(vbr.length?vbr:mp3));
+ use.sort((a,b)=>(a.name<b.name?-1:a.name>b.name?1:0));
+ return use;
+}
+
async function loadShow(show, autoplay, silent){
MUSIC.wantPlay = MUSIC.wantPlay || !!autoplay;
- if(MUSIC.loading) return;
- MUSIC.loading=true;
+ const token=++MUSIC._loadToken;
if(!silent) setNP('Loading '+show.label+' …');
try{
const r=await fetch('https://archive.org/metadata/'+show.id);
const d=await r.json();
- let files=(d.files||[]).filter(f=>/\.mp3$/i.test(f.name||''));
- const vbr=files.filter(f=>String(f.format||'').toUpperCase().indexOf('VBR')>=0);
- if(vbr.length) files=vbr; // one derivative per track
- files.sort((a,b)=> (a.name<b.name?-1:a.name>b.name?1:0));
- if(!files.length) throw new Error('no mp3 files');
+ const files=pickFiles(d.files||[]);
+ if(!files.length) throw new Error('no mp3');
+ if(token!==MUSIC._loadToken) return; // superseded by a newer load
MUSIC.showId=show.id; MUSIC.showLabel=show.label; MUSIC.idx=0;
MUSIC.playlist=files.map(f=>({url:iaFile(show.id,f.name), title:(f.title||f.name.replace(/\.mp3$/i,'')).replace(/^gd[\d\-]+[a-z]*-?/i,'').trim()||f.name}));
- MUSIC.loading=false;
setCredit(show.id);
- if(MUSIC.wantPlay) playTrack(0);
+ if(MUSIC.wantPlay) startTrack(0);
else if(!silent) setNP('Ready — '+show.label);
}catch(e){
- MUSIC.loading=false;
if(!silent) setNP('Couldn’t reach the Internet Archive — check your connection, then try again.');
}
}
-function playTrack(i){
- if(!MUSIC.playlist.length){ MUSIC.wantPlay=true; loadShow(MUSIC.shows[0],true); return; }
+
+async function getBuffer(url){
+ if(MUSIC.buffers.has(url)) return MUSIC.buffers.get(url);
+ const r=await fetch(url); // CORS on archive.org fileserver is open (ACAO:*)
+ const ab=await r.arrayBuffer();
+ const buf=await MUSIC.ctx.decodeAudioData(ab);
+ MUSIC.buffers.set(url,buf);
+ if(MUSIC.buffers.size>4){ const k=MUSIC.buffers.keys().next().value; MUSIC.buffers.delete(k); } // bound memory
+ return buf;
+}
+
+function stopSource(){
+ if(MUSIC.srcNode){
+ MUSIC._stopping=true;
+ try{ MUSIC.srcNode.onended=null; MUSIC.srcNode.stop(); }catch(e){}
+ try{ MUSIC.srcNode.disconnect(); }catch(e){}
+ MUSIC.srcNode=null; MUSIC._stopping=false;
+ }
+}
+
+async function startTrack(i){
+ if(!ensureCtx() || !MUSIC.playlist.length) return;
MUSIC.idx=((i%MUSIC.playlist.length)+MUSIC.playlist.length)%MUSIC.playlist.length;
- const t=MUSIC.playlist[MUSIC.idx];
+ const track=MUSIC.playlist[MUSIC.idx];
+ const token=++MUSIC._loadToken;
MUSIC.wantPlay=true;
- MUSIC.audio.src=t.url;
- setNP('♪ '+t.title+' — '+MUSIC.showLabel);
- const p=MUSIC.audio.play();
- if(p&&p.catch) p.catch(()=>{ updateMusicBtn(false); setNP('Tap “Play music” once more to allow audio.'); });
+ setNP('Buffering ♪ '+track.title+' …');
+ try{
+ const buf=await getBuffer(track.url);
+ if(token!==MUSIC._loadToken) return; // a newer start superseded us
+ stopSource();
+ const sn=MUSIC.ctx.createBufferSource();
+ sn.buffer=buf; sn.connect(MUSIC.analyser);
+ sn.onended=()=>{ if(!MUSIC._stopping && MUSIC.srcNode===sn) startTrack(MUSIC.idx+1); };
+ if(MUSIC.ctx.state==='suspended') await MUSIC.ctx.resume();
+ sn.start(0);
+ MUSIC.srcNode=sn; MUSIC.playing=true;
+ updateMusicBtn(true);
+ setNP('♪ '+track.title+' — '+MUSIC.showLabel);
+ prefetchNext();
+ }catch(e){
+ if(MUSIC.idx<MUSIC.playlist.length-1) startTrack(MUSIC.idx+1); // skip a bad track
+ else setNP('Couldn’t play this show — try “Shuffle show”.');
+ }
+}
+function prefetchNext(){
+ const n=MUSIC.playlist[(MUSIC.idx+1)%MUSIC.playlist.length];
+ if(n) getBuffer(n.url).catch(()=>{});
+}
+function pauseMusic(){ if(MUSIC.ctx && MUSIC.ctx.state==='running'){ MUSIC.ctx.suspend(); MUSIC.playing=false; updateMusicBtn(false); } }
+function resumeMusic(){ if(MUSIC.ctx){ MUSIC.ctx.resume(); MUSIC.playing=true; updateMusicBtn(true); } }
+
+// Per-frame: read the live spectrum → bass/mid/treble/level + a beat onset (used by the whole scene).
+function sampleAudio(){
+ const a=S.audio;
+ if(MUSIC.analyser && MUSIC.ctx && MUSIC.ctx.state==='running' && MUSIC.playing){
+ MUSIC.analyser.getByteFrequencyData(MUSIC.freq);
+ const f=MUSIC.freq, n=f.length;
+ const bE=Math.max(2,Math.floor(n*0.06)), mE=Math.floor(n*0.30), tE=Math.floor(n*0.70);
+ let bass=0,mid=0,tre=0,all=0;
+ for(let i=0;i<bE;i++) bass+=f[i];
+ for(let i=bE;i<mE;i++) mid +=f[i];
+ for(let i=mE;i<tE;i++) tre +=f[i];
+ for(let i=0;i<tE;i++) all +=f[i];
+ bass/=(bE*255); mid/=((mE-bE)*255); tre/=((tE-mE)*255); all/=(tE*255);
+ a.bass=lerp(a.bass,bass,0.4); a.mid=lerp(a.mid,mid,0.4); a.treble=lerp(a.treble,tre,0.4);
+ a.level=clamp(lerp(a.level,all*1.7,0.3),0,1);
+ const flux=bass-a._prev; a._prev=bass;
+ a._avg=lerp(a._avg,bass,0.08);
+ if(bass>a._avg*1.32 && flux>0.02 && a._refr<=0){ a.beat=1; a._refr=7; }
+ a._refr--; a.beat*=0.80;
+ a.active=true;
+ } else {
+ a.active=false; a.beat*=0.8;
+ a.level=lerp(a.level,0,0.12); a.bass=lerp(a.bass,0,0.12); a.mid=lerp(a.mid,0,0.12); a.treble=lerp(a.treble,0,0.12);
+ }
}
-MUSIC.audio.addEventListener('ended', ()=>playTrack(MUSIC.idx+1));
-MUSIC.audio.addEventListener('error', ()=>{ if(MUSIC.playlist.length && MUSIC.idx<MUSIC.playlist.length-1) playTrack(MUSIC.idx+1); });
-MUSIC.audio.addEventListener('play', ()=>updateMusicBtn(true));
-MUSIC.audio.addEventListener('pause', ()=>updateMusicBtn(false));
async function liveShuffle(){
- setNP('Finding a show in the archive …');
- MUSIC.wantPlay=true;
+ ensureCtx();
+ setNP('Finding a show in the archive …'); MUSIC.wantPlay=true;
try{
const q=encodeURIComponent('collection:GratefulDead AND format:(VBR MP3)');
const r=await fetch('https://archive.org/advancedsearch.php?q='+q+'&fl[]=identifier&fl[]=title&sort[]=downloads+desc&rows=60&output=json');
@@ -1359,25 +1456,25 @@ async function liveShuffle(){
if(!docs.length) throw new Error('empty');
const pick=docs[Math.floor(rnd()*docs.length)];
const label=String(pick.title||'Grateful Dead').replace(/^Grateful Dead Live at\s*/i,'').replace(/\s+on\s+/,' · ');
- MUSIC.loading=false; loadShow({id:pick.identifier,label}, true);
+ loadShow({id:pick.identifier,label}, true);
}catch(e){
- MUSIC.loading=false;
loadShow(MUSIC.shows[Math.floor(rnd()*MUSIC.shows.length)], true); // fallback: curated
}
}
// wiring
document.getElementById('mPlay').addEventListener('click',()=>{
- if(MUSIC.audio.src && !MUSIC.audio.paused){ MUSIC.audio.pause(); return; }
- if(MUSIC.audio.src){ const p=MUSIC.audio.play(); if(p&&p.catch)p.catch(()=>{}); return; }
- if(MUSIC.playlist.length) playTrack(MUSIC.idx);
+ ensureCtx();
+ if(MUSIC.playing){ pauseMusic(); return; }
+ if(MUSIC.srcNode && MUSIC.ctx && MUSIC.ctx.state==='suspended'){ resumeMusic(); return; }
+ if(MUSIC.playlist.length) startTrack(MUSIC.idx);
else { MUSIC.wantPlay=true; loadShow(MUSIC.shows[0], true); }
});
-document.getElementById('mNext').addEventListener('click',()=>playTrack(MUSIC.idx+1));
-document.getElementById('mPrev').addEventListener('click',()=>playTrack(MUSIC.idx-1));
+document.getElementById('mNext').addEventListener('click',()=>{ if(MUSIC.playlist.length) startTrack(MUSIC.idx+1); });
+document.getElementById('mPrev').addEventListener('click',()=>{ if(MUSIC.playlist.length) startTrack(MUSIC.idx-1); });
document.getElementById('mShuffle').addEventListener('click',liveShuffle);
-document.getElementById('mVol').addEventListener('input',e=>{ MUSIC.audio.volume=+e.target.value; });
-// silent metadata prefetch so the first Play is instant (no audio, no autoplay, leaves the note text intact)
+document.getElementById('mVol').addEventListener('input',e=>{ if(MUSIC.gain) MUSIC.gain.gain.value=+e.target.value; });
+// silent metadata prefetch so the first Play only needs to fetch the audio
setTimeout(()=>{ if(!MUSIC.playlist.length && !MUSIC.loading) loadShow(MUSIC.shows[0], false, true); }, 1800);
/* ============================ BOOT ============================ */
← 632cd63 Add optional live Grateful Dead music streamed from the Inte
·
back to Rose City Rooftop Solstice
·
(newest)