diff --git a/.gitignore b/.gitignore index 4609e3de5..9636ba503 100644 --- a/.gitignore +++ b/.gitignore @@ -199,6 +199,8 @@ __pycache__/ /reports/jas-nzxt-fleet-use-report-2026-07-22.out /slab/blueberry-cabinet/build/ /pop/nullabye/release/special-sign/special-sign-3d-spatial-graph.mp4 +/pop/nullabye/release/special-sign/special-sign-3d-spatial-overview.mp4 +/pop/nullabye/release/special-sign/special-sign-3d-score-cylinder-tour-hd.mp4 # Ignore lockfile for ac-site /tmp/ac-site.lock diff --git a/pop/nullabye/c/build-spatial.sh b/pop/nullabye/c/build-spatial.sh index b06421552..3f3d5b6f5 100644 --- a/pop/nullabye/c/build-spatial.sh +++ b/pop/nullabye/c/build-spatial.sh @@ -1,5 +1,11 @@ #!/bin/sh set -eu HERE="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -cc -O3 -std=c11 -Wall -Wextra -o "$HERE/spatial-sineabye" "$HERE/spatial-sineabye.c" -lm -echo "built $HERE/spatial-sineabye" +if [ "${1:-}" = "--hd" ]; then + OUT="$HERE/spatial-sineabye-hd" + cc -O3 -std=c11 -Wall -Wextra -DW=1440 -DH=1440 -o "$OUT" "$HERE/spatial-sineabye.c" -lm +else + OUT="$HERE/spatial-sineabye" + cc -O3 -std=c11 -Wall -Wextra -o "$OUT" "$HERE/spatial-sineabye.c" -lm +fi +echo "built $OUT" diff --git a/pop/nullabye/c/spatial-sineabye.c b/pop/nullabye/c/spatial-sineabye.c index e6cdf3427..a5a887cbb 100644 --- a/pop/nullabye/c/spatial-sineabye.c +++ b/pop/nullabye/c/spatial-sineabye.c @@ -27,8 +27,12 @@ static double tempoBpm=ROOM_BPM; // control-rate stepping before their states are interpolated at 48 kHz. #define CTRL 240 #define FPS 24 +#ifndef W #define W 720 +#endif +#ifndef H #define H 720 +#endif #define MAXE 4096 #define NSRC 12 #define NFRAMES ((int)(DUR*FPS)) @@ -75,6 +79,8 @@ static int voiceCount=12; static int duetMode=0; static int cosmosMode=0; static int cameraMode=0; // 0 global overview, 1 ship/listener POV +static int presentationMode=0; // closer orbiting overview for room-scale playback +static int cylinderMode=0; // high-resolution light score tube, viewed side-on static int acousticsView=0; // score-wide acoustic telemetry / signal-chain view static void resolve_render_theme(void){if(themeExplicit)return;const char*env=getenv("AC_RENDER_THEME");if(env&&*env){if(!strcmp(env,"light"))brightMode=1;else if(!strcmp(env,"dark"))brightMode=0;else if(!strcmp(env,"custom"))return;else env=NULL;if(env)return;}FILE*f=popen("defaults read -g AppleInterfaceStyle 2>/dev/null","r");if(!f){brightMode=0;return;}char b[32]={0};size_t n=fread(b,1,sizeof(b)-1,f);pclose(f);brightMode=!(n&&strstr(b,"Dark"));} static const StarSystem SYSTEMS[2]={ @@ -840,10 +846,20 @@ static void spectral_volume(unsigned char*p,int src,int fr,double t,V3 c,V3 cam, line3(p,pt[b][SPEC_RIBS-1],pt[b][0],cam,target,col,alpha*.72); } } +// Presentation view score tubes. Upcoming events rise from below each source, +// cross its spectral body at onset, then accelerate upward as emitted notes. +// Ring radius is logarithmic pitch and the axial tail is authored duration. +static void note_ring3(unsigned char*p,V3 c,double r,V3 cam,V3 target,uint32_t color,double alpha){V3 first={c.x+r,c.y,c.z},prev=first;for(int i=1;i<=12;i++){double a=TAU*i/12;V3 q={c.x+cos(a)*r,c.y+sin(a)*r,c.z};line3(p,prev,q,cam,target,color,alpha);prev=q;} } +static void note_stream(unsigned char*p,int src,double t,V3 c,V3 cam,V3 target){double lookAhead=cylinderMode?12.0:4.2,after=cylinderMode?9.0:2.25,below=cylinderMode?14.0:3.6,above=cylinderMode?14.0:3.4,tubeRadius=cylinderMode?.18:.24;uint32_t base=S[src].color; + for(int k=0;k<3;k++){double a=TAU*k/3+src*.19,x=cos(a)*tubeRadius,y=sin(a)*tubeRadius;line3(p,(V3){c.x+x,c.y+y,c.z-below},(V3){c.x+x,c.y+y,c.z+above},cam,target,base,.075);} + for(int j=0;jsrc!=src||e->g<.004)continue;double dt=e->t-t,flow,fade;if(dt>0&&dt<=lookAhead){flow=dt/lookAhead;flow=flow*flow*(3-2*flow);flow=-below*flow;fade=.22+.62*(1-dt/lookAhead);}else if(dt<=0&&dt>=-after){double u=-dt/after;flow=above*(1-pow(1-u,3));fade=.78*(1-u)*(1-u);}else continue; + double f=sqrt(fmax(45,e->f0)*fmax(45,e->f1)),pitch=fmax(0,fmin(1,log(f/45.0)/log(14000.0/45.0))),r=.10+.18*pitch,tail=.10+.30*fmin(2.0,e->dur),amp=fmin(1.0,.24+1.55*sqrt(fabs(e->g)));uint32_t col=shade_color(base,.68+.48*pitch);V3 q={c.x,c.y,c.z+flow};note_ring3(p,q,r,cam,target,col,fade*amp);line3(p,(V3){q.x,q.y,q.z-tail},q,cam,target,col,fade*amp*.82);P2 qp=project(q,cam,target);if(qp.ok){int rr=dt<=0?3:2;glow(p,qp.x,qp.y,rr*3,col,.05+fade*amp*.09);dot(p,qp.x,qp.y,rr,col);}if(fabs(dt)<.055){P2 cp=project(c,cam,target);if(cp.ok)glow(p,cp.x,cp.y,20,col,.10*amp);} + } +} // Acoustic shells are emitted from each body's historical world position. A // shell exists only when that source's measured post-HRTF output existed, so // the waves remain signal visualization rather than decorative animation. -static void sound_shells(unsigned char*p,int src,int fr,V3 cam,V3 target){for(int q=1;q<=6;q++){int pf=fr-q*5;if(pf<0)continue;double ml,mr;source_meter(pf,src,&ml,&mr);double e=hypot(ml,mr);if(e<.00008)continue;double age=(fr-pf)/(double)FPS,et=pf/(double)FPS,fade=(1-age/1.35);fade*=fade;Source so=source_at(src,et);V3 c={so.x,so.y,so.z};double r=.16+age*2.35,a=(.025+fmin(.20,e*15))*fade;uint32_t col=shade_color(S[src].color,.72+q*.055);const float*wave=&sourceWave[(pf*NSRC+src)*WAVE_POINTS];double peak=0;for(int i=0;i=4&&!strcmp(outp+ol-4,".mov");const char*clean=globeMode?"-af 'highpass=f=30,equalizer=f=7600:t=q:w=.75:g=-4,lowpass=f=11800,alimiter=limit=.90:attack=6:release=110,volume=.88'":"",*pixels=(globeMode&&!acousticsView)?"-vf 'scale=240:240:flags=neighbor,scale=720:720:flags=neighbor'":"";if(lossless) snprintf(cmd,sizeof cmd,"ffmpeg -hide_banner -y -loglevel error -f rawvideo -pixel_format rgb24 -video_size %dx%d -framerate %d -i - -i '%s' -c:v libx265 -preset medium -x265-params lossless=1:log-level=error -pix_fmt yuv444p -tag:v hvc1 %s -c:a pcm_s24le -ar 48000 -shortest '%s'",W,H,FPS,wavp,clean,outp); else snprintf(cmd,sizeof cmd,"ffmpeg -hide_banner -y -loglevel error -f rawvideo -pixel_format rgb24 -video_size %dx%d -framerate %d -i - -i '%s' %s -c:v libx264 -preset slow -pix_fmt yuv420p -crf 14 %s -c:a aac -b:a 320k -shortest '%s'",W,H,FPS,wavp,pixels,clean,outp);FILE*ff=popen(cmd,"w");if(!ff)return;unsigned char*p=malloc(W*H*3); - int videoFrames=(int)floor(videoDuration*FPS+.5);for(int outFr=0;outFrM_PI)side-=TAU;while(side-2.25<-M_PI)side+=TAU;u=smooth01(fmin(1,(elapsed-76)/10));orbit=2.25+(side-2.25)*u;zoff=5.8-u;}double radius=fmax(6.5,fmin(13.2,displayExtent*1.45+2.4));cam=(V3){cx+radius*cos(orbit),cy+radius*sin(orbit),cz+zoff};target=(V3){cx,cy,cz};}}else{double orbitTime=ending_motion_time(t),orbit=-.72+.16*sin(orbitTime/19.0);cam=(V3){cx+19*cos(orbit),cy+19*sin(orbit),cz+12.5};target=(V3){cx,cy,cz*.55};}} // Faceted receiver-world horizon. Large triangles scroll gently with travel, // providing scale without pretending to be another sound source. if(cameraMode){int horizon=brightMode?300:330,shift=(int)(l.x*9+l.y*5);for(int q=-1;q<10;q++){int x0=q*92-(shift%92),x1=x0+46,x2=x0+92,y1=horizon-28-(int)(38*(.5+.5*sin(q*2.31+l.heading)));uint32_t c1=brightMode?(q&1?0xc9d2cf:0xbcc8c6):(q&1?0x20282b:0x182124);tri2(p,x0,horizon,x1,y1,x2,horizon,c1,.82);tri2(p,x0,horizon,x2,horizon,x2,H-205,brightMode?0xd9ded8:0x111719,.52);}line2(p,0,horizon,W,horizon,brightMode?0x7f9292:0x52656a,.55);} - // Perspective floor grid makes depth and the final pull-away legible. - for(int q=-8;q<=8;q++){line3(p,(V3){q,-8,0},(V3){q,8,0},cam,target,brightMode?0x9eafb2:0x536069,brightMode?.42:.28);line3(p,(V3){-8,q,0},(V3){8,q,0},cam,target,brightMode?0x9eafb2:0x536069,brightMode?.42:.28);} + // The ordinary overview retains its spatial scaffolding. Presentation mode + // lets the authored bodies, score tubes, and measured waves carry the view. + if(!presentationMode)for(int q=-8;q<=8;q++){line3(p,(V3){q,-8,0},(V3){q,8,0},cam,target,brightMode?0x9eafb2:0x536069,brightMode?.42:.28);line3(p,(V3){-8,q,0},(V3){8,q,0},cam,target,brightMode?0x9eafb2:0x536069,brightMode?.42:.28);} // The same propagation world the radio hears: brick occluder and wind-raked // rain streaks. They are geometry/fields, never decorative AI additions. - for(int q=0;q<=5;q++)line3(p,(V3){1.2,-3,q*.48},(V3){1.2,2.5,q*.48},cam,target,0x806f68,.62); - for(int q=0;q<=6;q++)line3(p,(V3){1.2,-3+q*.92,0},(V3){1.2,-3+q*.92,2.4},cam,target,0x806f68,.62); - for(int q=0;q<42;q++){double rx=-8+fmod(q*3.71+t*.7,16),ry=-8+fmod(q*6.13+t*.43,16),rz=.3+fmod(q*1.77-t*1.9,5.5);line3(p,(V3){rx,ry,rz},(V3){rx-.12,ry-.08,rz-.38},cam,target,0x8faab5,.18);} + if(!presentationMode){for(int q=0;q<=5;q++)line3(p,(V3){1.2,-3,q*.48},(V3){1.2,2.5,q*.48},cam,target,0x806f68,.62);for(int q=0;q<=6;q++)line3(p,(V3){1.2,-3+q*.92,0},(V3){1.2,-3+q*.92,2.4},cam,target,0x806f68,.62);for(int q=0;q<42;q++){double rx=-8+fmod(q*3.71+t*.7,16),ry=-8+fmod(q*6.13+t*.43,16),rz=.3+fmod(q*1.77-t*1.9,5.5);line3(p,(V3){rx,ry,rz},(V3){rx-.12,ry-.08,rz-.38},cam,target,0x8faab5,.18);}} V3 lv={l.x,l.y,1.6}; // Six-second fading breadcrumb: the path that the two follower voices chase. - for(int q=1;q<=36;q++){int ia=li-(q-1)*CTRL/6,ib=li-q*CTRL/6;if(ib<0)break;V3 a={L[ia].x,L[ia].y,1.58},b={L[ib].x,L[ib].y,1.58};line3(p,a,b,cam,target,0xff6b9d,.42*(1-q/37.0));} - for(int s=0;s.00005)line3(p,lv,sv,cam,target,S[s].color,.03+fmin(.84,heard*28)); + {double trail=.18+fmin(.9,dd/12);V3 prev=sv;for(int q=1;q<=16;q++){double pt=fmax(0,t-q*trail/16),fade=(1-q/17.0)*(.08+fmin(.55,heard*18));Source old=source_at(s,pt);V3 ov={visualCx+(old.x-visualCx)*visualSpread,visualCy+(old.y-visualCy)*visualSpread,old.z};line3(p,prev,ov,cam,target,S[s].color,fade);prev=ov;}} + if(!presentationMode&&heard>.00005)line3(p,lv,sv,cam,target,S[s].color,.03+fmin(.84,heard*28)); if(sp.ok){double sc=W/360.0;int core=(int)fmax(3,fmin(11,52*sc/sp.z));int halo=(int)fmax(core*2,fmin(62,(4+heard*1500)*sc*10/sp.z));P2 sh=project((V3){so.x,so.y,.02},cam,target);if(sh.ok)glow(p,sh.x,sh.y,(int)fmax(5,halo*.8),S[s].color,.06+fmin(.4,heard*14));if(s==ts&&t<104)glow(p,sp.x,sp.y,halo+18,0xffffff,.16);glow(p,sp.x,sp.y,halo,S[s].color,.10+fmin(.36,heard*10));} - spectral_volume(p,s,fr,t,sv,cam,target,s==ts);if(!cameraMode&&sp.ok)text3(p,sp.x+9,sp.y-7,S[s].name,1,brightMode?0x33484c:0xd5e5e2); + spectral_volume(p,s,fr,t,sv,cam,target,s==ts);if(!cameraMode&&!presentationMode&&sp.ok)text3(p,sp.x+9,sp.y-7,S[s].name,1,brightMode?0x33484c:0xd5e5e2); } if(cameraMode){ stereo_windshield(p,t); @@ -1043,12 +1060,12 @@ static void video(const char*wavp,const char*outp,double videoStart,double video line2(p,rcx,rcy-7,rcx-6,rcy+6,0xf6c915,.95);line2(p,rcx-6,rcy+6,rcx+6,rcy+6,0xf6c915,.95);line2(p,rcx+6,rcy+6,rcx,rcy-7,0xf6c915,.95); double sumL=0,sumR=0;for(int s=0;s10.5){rx*=10.5/rr;ry*=10.5/rr;}int bx=rcx+(int)(ry*scale),base=rcy-(int)(rx*scale*.5),by=base-(int)((so.z-1.6)*10),weak=hypot(ml,mr)<.0015;double vis=weak?(.16+.24*(sin(t*31+s*7)>0)):.9;line2(p,bx,base,bx,by,S[s].color,vis*.7);if(s==ts)glow(p,bx,by,12,0xffffff,.28);dot(p,bx,by,s==ts?4:3,S[s].color);} int lm=(int)fmin(110,sumL*850),rm=(int)fmin(110,sumR*850);fill2(p,70,H-55,70+lm,H-45,0x4ecdc4,.9);fill2(p,W-70-rm,H-55,W-70,H-45,0xf8a5c2,.9); - }else{P2 lp=project(lv,cam,target),hd=project((V3){l.x+cos(l.heading)*.75,l.y+sin(l.heading)*.75,1.6},cam,target);if(lp.ok){glow(p,lp.x,lp.y,26,0xffd54f,.28);dot(p,lp.x,lp.y,7,0xffffff);if(hd.ok)line2(p,lp.x,lp.y,hd.x,hd.y,0xff6b9d,.9);}text3(p,24,22,"GLOBAL ARRANGEMENT",2,brightMode?0x324a4d:0xe2efed);text3(p,24,42,"SOUND BODIES - GRAVITY - WAVES",1,brightMode?0x536e73:0x75aeb2);} + }else{P2 lp=project(lv,cam,target),hd=project((V3){l.x+cos(l.heading)*.75,l.y+sin(l.heading)*.75,1.6},cam,target);if(lp.ok){glow(p,lp.x,lp.y,presentationMode?38:26,0xffd54f,.28);dot(p,lp.x,lp.y,presentationMode?10:7,cylinderMode?0x37474a:0xffffff);if(hd.ok)line2(p,lp.x,lp.y,hd.x,hd.y,0xff6b9d,.9);if(presentationMode&&!cylinderMode)text3(p,lp.x+14,lp.y-9,"LISTENER",2,brightMode?0x324a4d:0xffffff);}if(!presentationMode){text3(p,24,22,"GLOBAL ARRANGEMENT",2,brightMode?0x324a4d:0xe2efed);text3(p,24,42,"SOUND BODIES - GRAVITY - WAVES",1,brightMode?0x536e73:0x75aeb2);}} fwrite(p,1,W*H*3,ff); } free(p);pclose(ff); } - int main(int argc,char**argv){const char*w="../out/spatial-sineabye.wav",*v="../out/spatial-sineabye.mp4",*videoAudio=NULL,*mp3=NULL,*ptStill=NULL,*sceneOut=NULL,*scoreOut=NULL;double ptTime=32,videoStart=0,videoDuration=DUR;int ptSpp=32,sceneFrames=0,tempoExplicit=0,scoreOnly=0;for(int i=1;i