[object Object]

← back to Dead Agentabrams

Sync ROOM 01: add optional audio-reactive mode (live archive.org stream drives the visuals on the beat)

d42c6950d1c2d4f46fc9965cb33bf1e909120a16 · 2026-09-01 18:15:11 -0700 · Steve Abrams

Files touched

Diff

commit d42c6950d1c2d4f46fc9965cb33bf1e909120a16
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 18:15:11 2026 -0700

    Sync ROOM 01: add optional audio-reactive mode (live archive.org stream drives the visuals on the beat)
---
 room/index.html | 55 +++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 49 insertions(+), 6 deletions(-)

diff --git a/room/index.html b/room/index.html
index c849d94..c2e78aa 100644
--- a/room/index.html
+++ b/room/index.html
@@ -255,6 +255,7 @@
       <div class="toggle">
         <button class="btn" id="reduceBtn" aria-pressed="false">◐ Reduced Motion</button>
         <button class="btn" id="trailBtn" aria-pressed="true">✧ Motion Trails</button>
+        <button class="btn" id="reactBtn" aria-pressed="false">♫ React to Music</button>
       </div>
     </div>
     <div class="group">
@@ -282,7 +283,7 @@
     <h2>Scarlet Riverboat Masquerade</h2>
     <div class="meta">Generative Canvas · Live Music Archive · Authored Visual Rhythm</div>
     <p>A fantastical paddlewheel riverboat drifts through a hand-painted crimson canyon while masked revelers fill three decks. This is a <strong>listening room</strong>: press play in the Music Room and a Grateful Dead concert streams from the Internet Archive's <strong>Live Music Archive</strong> while the boat turns around you.</p>
-    <p><strong>The motion is composed, not measured.</strong> The choreography is an <em>authored visual cadence</em> written into the drawing loop itself — it is deliberately <em>not</em> audio analysis and does not react to the waveform. Set the visual tempo to whatever feels right against the night's playing; the two rhythms — heard and drawn — keep company without one driving the other.</p>
+    <p><strong>The motion is composed, not measured.</strong> The choreography is an <em>authored visual cadence</em> written into the drawing loop itself — it is deliberately <em>not</em> audio analysis and does not react to the waveform. Set the visual tempo to whatever feels right against the night's playing; the two rhythms — heard and drawn — keep company without one driving the other. <em>(Flip “React to Music” and that changes on purpose: the lanterns, fireflies, and dancers then bloom to a live analysis of the streaming audio.)</em></p>
     <p><strong>The recordings are not copied or re-hosted here.</strong> They are community tapes made available through the Grateful Dead collection at the Internet Archive; playback is user-initiated and every set links back to its source item on archive.org.</p>
     <p>The <strong>original visual language</strong> reduces each dancer to a weighted silhouette: couples pivot in shared arcs, a linked circle turns about the smokestack, soloists trail scarves, and musicians appear only as abstract shadow-forms — a communal, exploratory spirit rendered without any borrowed logo, emblem, or album art.</p>
     <p>On the shore, four creatures each carry a distinct loop with believable weight — a <strong>trotting fox</strong>, a <strong>slow-stepping heron</strong>, a <strong>squash-and-stretch rabbit</strong>, and a <strong>bear conducting fireflies</strong>. Palette <em>and</em> choreography change across the three chapters as the boat travels from dusk (Departure) through moonrise (Moon Bend) into sunrise (Dawn Landing).</p>
@@ -456,7 +457,7 @@ function canyonSilhouette(baseY, amp, seed, color, offset){
 // ---------- Boat transform for the active camera ----------
 function boatTransform(){
   const W=S.W,H=S.H;
-  const bob = S.reduced?0:Math.sin(S.t*0.6)*6;
+  const bob = S.reduced?0:Math.sin(S.t*0.6)*6*(1+(S.amp||0)*1.1);
   const sway = S.reduced?0:Math.sin(S.t*0.33)*0.012;
   let cx=W*0.5, cy=H*0.60+bob, scale=Math.min(W,H)/620, rot=sway;
   if(S.cam==='deck'){ cx=W*0.5; cy=H*0.66+bob; scale=Math.min(W,H)/300; rot=sway*0.6; }
@@ -492,7 +493,7 @@ function drawFigure(x,y,s,phase,type,accent){
     armSwing = 0.5+0.3*Math.sin(t*2+ph);
   }
   ctx.rotate(lean);
-  ctx.translate(0,bob);
+  ctx.translate(0,bob*(1+(S.amp||0)*0.7));   // dancers bounce harder on louder passages when reactive
 
   // scarf trail for soloists — length/opacity swell across chapters
   if(type==='solo' && scarf){
@@ -563,7 +564,7 @@ function lantern(x,y,s,glow,color){
 function drawBoat(){
   const {cx,cy,scale,rot}=boatTransform();
   const accent=pal('accent',0), lan=pal('lantern',0);
-  const glow=S.lantern;
+  const glow=S.lantern*(1+(S.amp||0)*0.9);   // lanterns bloom on the beat when reactive
   ctx.save();
   ctx.translate(cx,cy); ctx.rotate(rot); ctx.scale(scale,scale);
 
@@ -859,7 +860,7 @@ function bearAndFireflies(x,y,s,t,accent){
     const a=t*0.7*(0.6+ (i%3)*0.2) + ph;
     const fx=x + Math.cos(a)*orbit + Math.sin(t*0.7)*14;
     const fy=y-40 + Math.sin(a*1.3)*orbit*0.5 - (i%4)*6;
-    const blink=0.35+0.65*Math.abs(Math.sin(t*3+ph));
+    const blink=Math.min(1,(0.35+0.65*Math.abs(Math.sin(t*3+ph)))*(1+(S.amp||0)*1.3));
     const g=ctx.createRadialGradient(fx,fy,0,fx,fy,7);
     g.addColorStop(0,`rgba(255,230,140,${blink})`); g.addColorStop(1,'rgba(255,200,90,0)');
     ctx.fillStyle=g; ctx.beginPath(); ctx.arc(fx,fy,7,0,7); ctx.fill();
@@ -894,7 +895,7 @@ function drawRiver(){
   if(S.lantern>0.05 && S.cam!=='over'){
     const {cx}=boatTransform();
     const rg=ctx.createRadialGradient(cx,top+30,4,cx,top+30,120);
-    rg.addColorStop(0,`rgba(255,190,90,${0.22*S.lantern})`); rg.addColorStop(1,'rgba(0,0,0,0)');
+    rg.addColorStop(0,`rgba(255,190,90,${Math.min(0.6,0.22*S.lantern*(1+(S.amp||0)))})`); rg.addColorStop(1,'rgba(0,0,0,0)');
     ctx.fillStyle=rg; ctx.beginPath(); ctx.ellipse(cx,top+40,120,40,0,0,7); ctx.fill();
   }
 }
@@ -904,6 +905,9 @@ let last=performance.now();
 function frame(now){
   const dt=Math.min(0.05,(now-last)/1000); last=now;
 
+  // live audio energy (0..1) when "React to Music" is on — otherwise motion stays authored
+  S.amp = (window.SRM && window.SRM.react) ? (window.SRM.level||0) : 0;
+
   if(S.playing){
     const spd = S.reduced ? 0.28 : 1.0;
     S.t += dt * S.tempo * spd * 1.4;
@@ -1055,6 +1059,7 @@ window.addEventListener('keydown',e=>{
     case 'r': document.getElementById('restartBtn').click(); break;
     case 'm': reduceBtn.click(); break;
     case 't': trailBtn.click(); break;
+    case 'a': { const rb=document.getElementById('reactBtn'); if(rb) rb.click(); } break;
     case '1': document.querySelector('[data-cam="wide"]').click(); break;
     case '2': document.querySelector('[data-cam="deck"]').click(); break;
     case '3': document.querySelector('[data-cam="over"]').click(); break;
@@ -1176,6 +1181,44 @@ function setPlay(on){
   b.setAttribute('aria-pressed',String(on)); b.setAttribute('aria-label',on?'Pause':'Play');
 }
 
+// --- optional audio-reactive bridge: analyzes the LIVE archive.org stream (CORS-open) ---
+// Default OFF — visuals stay authored until the visitor flips "React to Music".
+audio.crossOrigin='anonymous';                 // set before any src load so the CDN stream is analyzable
+window.SRM = window.SRM || { level:0, react:false };
+let _actx, _analyser, _freq, _looping=false;
+function ensureGraph(){
+  if(_actx) return true;
+  try{
+    _actx = new (window.AudioContext||window.webkitAudioContext)();
+    const src=_actx.createMediaElementSource(audio);   // once per element
+    _analyser=_actx.createAnalyser(); _analyser.fftSize=256; _analyser.smoothingTimeConstant=0.82;
+    src.connect(_analyser); _analyser.connect(_actx.destination);
+    _freq=new Uint8Array(_analyser.frequencyBinCount);
+    return true;
+  }catch(e){ return false; }
+}
+function sampleLevel(){
+  if(_analyser){
+    _analyser.getByteFrequencyData(_freq);
+    let sum=0, n=Math.max(1,Math.floor(_freq.length/6));  // low-end (bass) energy
+    for(let i=0;i<n;i++) sum+=_freq[i];
+    window.SRM.level = window.SRM.level*0.6 + ((sum/n)/255)*0.4;  // smoothed 0..1
+  }
+  requestAnimationFrame(sampleLevel);
+}
+const reactBtn=$('reactBtn');
+if(reactBtn) reactBtn.addEventListener('click',()=>{
+  const on=!window.SRM.react;
+  if(on){
+    if(!ensureGraph()){ announce('Live audio analysis is not available in this browser.'); return; }
+    if(_actx.state==='suspended') _actx.resume();
+    if(!_looping){ _looping=true; sampleLevel(); }
+  }
+  window.SRM.react=on;
+  reactBtn.setAttribute('aria-pressed',String(on));
+  announce(on?'Reacting to the music — lanterns, fireflies, and dancers now move to the live audio.':'Back to authored motion.');
+});
+
 audio.addEventListener('play',()=>setPlay(true));
 audio.addEventListener('pause',()=>setPlay(false));
 audio.addEventListener('ended',()=>{ if(trackIdx<tracks.length-1) playTrack(trackIdx+1); else setPlay(false); });

← f9a40c3 Sync ROOM 01 with codex-hardened riverboat room (race guard,  ·  back to Dead Agentabrams  ·  Sync ROOM 01: pulsating lamps + beat/click fireworks ba4b4cb →