diff --git a/pop/nullabye/README.md b/pop/nullabye/README.md --- a/pop/nullabye/README.md +++ b/pop/nullabye/README.md @@ -68,14 +68,25 @@ Its listening law is intentionally selective: distance falls steeply after a few room units, so wandering actively remixes the piece. Faint grey tethers in the video show physical source distance; colored halos and lines show the post-distance energy that actually reaches the stereo listener. -The opening is four voice-free bars of sustained resonators: the complete room -starts near 30 rotations per second, completing 96 turns as it decelerates to a -stop. At that speed the HRTF and proximity motion fuse into a spatial hum before -the written lullaby arrives. From 0:50–1:18 the complete source constellation -eases through two more physical rotations, including an eight-turn centrifuge -from 1:02–1:10. The audio and gravity use the rotating coordinates, so these -passages produce real proximity, Doppler, and stereo movement rather than a -camera spin. +The opening starts near 30 rotations per second, completing 96 turns as it +decelerates to a stop. Sustained resonators make that HRTF/proximity motion fuse +into a spatial hum, but the track no longer waits inside atmosphere: the +lullaby melody, quarter-note low pulse, and alternating air answers begin in +bar one. From 0:50–1:18 the complete source constellation eases through two +more physical rotations, including an eight-turn centrifuge from 1:02–1:10. +The audio and gravity use the rotating coordinates, so these passages produce +real proximity, Doppler, and stereo movement rather than a camera spin. + +In the first-person receiver view, emitters are twelve-slice spectrographic +volumes instead of small source dots. Frequency rises vertically, live band +energy controls each cross-section, and longitudinal ribs join the slices into +one body. Six historical post-HRTF measurements launch expanding three-plane +shells from each body's position at the moment its sound was emitted; captured +per-source waveform samples deform those shells instead of leaving them as +smooth decorative rings. The cockpit windshield is a 55 ms perspective surface +of the exact final stereo master decoded back into C. Source-colored waveform +particles strike it at the emitter's measured L/R position and spectral height, +producing impact ripples on the calculated sound plane. The echo and air sources are follower bodies: they occupy the listener's position 2.2 and 5.5 seconds in the past, respectively. Their sound and gravity therefore chase the player; a fading breadcrumb in the video exposes the same @@ -107,8 +118,8 @@ ### Jeffrey choir release candidate `bin/render-spatial-jeffrey.mjs` layers the existing project-owned, pitch-locked Jeffrey `mm/oo/oh/ah/eh` takes over a room render. It leaves the opening spin -voice-free, then stretches compact harmonies around the takes' natural B2 -register. During the 1:02–1:10 super-spin, the choir's orbit follows the exact +free of Jeffrey's choir, then stretches compact harmonies around the takes' +natural B2 register. During the 1:02–1:10 super-spin, the choir's orbit follows the exact eight-turn C trajectory. The renderer makes no network or voice-generation API calls and records every source and placement in a provenance sidecar. @@ -124,6 +135,12 @@ --video ../out/review/spatial-room-spin-jeffrey.mp4 \ --video-audio ../out/review/spatial-room-spin-jeffrey-MASTER.wav \ --spatial-wet 0.58 --noise-level 0.55 ``` + +Accepted listening/video snapshot: + +- `release/spatial-sineabye/spatial-sineabye.mp3` — 320 kbps listening copy. +- `release/spatial-sineabye/spatial-sineabye.mp4` — 720×720, 24 fps, + Git-safe H.264 delivery encode with the calculated stereo windshield. ## nuellaby — the complexity-arch cut diff --git a/pop/nullabye/bin/render-spatial-jeffrey.mjs b/pop/nullabye/bin/render-spatial-jeffrey.mjs --- a/pop/nullabye/bin/render-spatial-jeffrey.mjs +++ b/pop/nullabye/bin/render-spatial-jeffrey.mjs @@ -2,8 +2,9 @@ #!/usr/bin/env node // render-spatial-jeffrey.mjs — Jeffrey's stretched phoneme choir for the // two-minute spatial sineabye room mix. // -// The first four bars remain voice-free so the decelerating whole-room spin -// can fuse into its own hum. The choir enters as mm/oo, opens through oh, +// The first four bars remain Jeffrey-choir-free while the C bed's melody and +// beat move through the decelerating whole-room spin. The choir enters as +// mm/oo, opens through oh, // blooms to ah/eh through the 62–70 s eight-turn centrifuge, then closes back // toward oo/mm. All sources are the project-owned, pitch-locked Jeffrey takes // already used by momabobasheep; this renderer makes no network/API calls. @@ -284,7 +285,7 @@ const provenancePath = outPath.replace(/\.[^.]+$/, ".provenance.json"); writeFileSync(provenancePath, JSON.stringify({ title: "spatial sineabye — Jeffrey choir candidate", bed: bedPath.replace(REPO + "/", ""), bpm: BPM, duration: TOTAL_SEC, - opening: "bars 0–3 voice-free; 96-turn decelerating room spin is rendered in C bed", + opening: "bars 0–3 Jeffrey-choir-free; melody, beat, and 96-turn decelerating room spin are rendered in C bed", superSpin: { start: 62, end: 70, turns: 8, vocalOrbitMatched: true }, voiceManifest: manifestPath.replace(REPO + "/", ""), sources: manifest.map(({ file, flatFile, vowel, midi }) => ({ file, flatFile, vowel, midi })), diff --git a/pop/nullabye/c/spatial-sineabye.c b/pop/nullabye/c/spatial-sineabye.c --- a/pop/nullabye/c/spatial-sineabye.c +++ b/pop/nullabye/c/spatial-sineabye.c @@ -49,6 +49,10 @@ static Listener *L; static float *busL,*busR,*fieldGain; // Measured post-HRTF contribution of every source in every video frame. The // visualizer reads this telemetry instead of guessing loudness from envelopes. static double *meterL,*meterR; +#define WAVE_POINTS 48 +// Per-emitter post-HRTF waveform thumbnails, accumulated at the same retarded +// arrival time as the audio bus. Expanding shells carry these actual samples. +static float *sourceWave; static double spatialWet=.32; // The noise voices are part of the instrument, not recording dirt. Keep a // permanent breath floor while letting the release mix sit behind the tones. @@ -254,10 +258,15 @@ return; } for(int b=0;b<38;b++){ double t=b*BAR, tr=(b>=20&&b<28)?4.0/3:1; - // Opening: no beat and no written melody, only four resonators carried by - // the decelerating room spin. A faint continuous air voice prevents the - // release from ever becoming clinically noiseless. - if(b<4){if(b==0){ev(t,4*BAR+.3,36,.055,0,2.8,2.6);ev(t,4*BAR+.3,43,.038,2,3.4,2.8);ev(t,4*BAR+.3,48,.030,3,4.0,3.0);ev(t,4*BAR+.3,55,.018,4,4.8,3.4);noisev(t,4*BAR+.3,2100,7200,.0065*noiseLevel,11);}continue;} + // Opening: no choir yet, but the twisting room is already the song. Four + // sustained resonators make the rotational hum; a quarter-note low wave, + // alternating air answers, and a lighter statement of the lullaby give the + // visible bodies clear emissions throughout the 96-turn deceleration. + if(b<4){ + if(b==0){ev(t,4*BAR+.3,36,.055,0,2.8,2.6);ev(t,4*BAR+.3,43,.038,2,3.4,2.8);ev(t,4*BAR+.3,48,.030,3,4.0,3.0);ev(t,4*BAR+.3,55,.018,4,4.8,3.4);noisev(t,4*BAR+.3,2100,7200,.0065*noiseLevel,11);} + for(int q=0;q<4;q++){glide(t+q*BEAT,.36,72,38,(q==0?.090:.066),1);noisev(t+(q+.5)*BEAT,.12,2400,6800,.025*noiseLevel,q&1?7:6);ev(t+q*BEAT,BEAT*.72,mel[b%4][q]-12,.050,4,.035,.25);} + continue; + } for(int k=0;k<4;k++)ev(t,BAR+.1,69+12*log2(hz(chord[b%4][k])*tr/440),.034,k<2?2:3,.7,1.0); // Continuous independently filtered noise voices. double noiseScale=noiseLevel; @@ -345,7 +354,7 @@ // Brick barrier at x=1.2, y=-3..2.5, height 2.4. Only an actual ray // intersection occludes; high sources and paths around its ends remain clear. if((x-1.2)*(so.x-1.2)<0){double q=(1.2-x)/(so.x-x),iy=y+(so.y-y)*q,iz=1.6+(so.z-1.6)*q;if(iy>-3&&iy<2.5&&iz<2.4){*wall=.46;*cutoff=1450;}} double wx=.7*cos(t*.11),wy=.7*sin(t*.083+.8);*wind=.0045*(wx*dx+wy*dy)/(d+1);*rain=1-.075*fmin(1,d/8.0)*fabs(sin(t*19.7+src*2.13));} -static void render(void){long n=(long)(DUR*SR);busL=calloc(n,4);busR=calloc(n,4);meterL=calloc(NFRAMES*NSRC,sizeof(*meterL));meterR=calloc(NFRAMES*NSRC,sizeof(*meterR)); +static void render(void){long n=(long)(DUR*SR);busL=calloc(n,4);busR=calloc(n,4);meterL=calloc(NFRAMES*NSRC,sizeof(*meterL));meterR=calloc(NFRAMES*NSRC,sizeof(*meterR));sourceWave=calloc(NFRAMES*NSRC*WAVE_POINTS,sizeof(*sourceWave)); for(int j=0;jt*SR),nn=(long)(e->dur*SR);double ph=0,lp=0,hpLP=0,envLP=0,saz=0,sel=0,sd=0,mode1[3]={0},mode2[3]={0};int spatialInit=0;uint32_t rs=e->seed;ACHrtf hs;memset(&hs,0,sizeof hs); for(long k=0;ksrc,t,&az,&el,&d);if(!spatialInit){saz=az;sel=el;sd=d;spatialInit=1;}else{double smooth=1-exp(-1.0/(SR*.018)),da=atan2(sin(az-saz),cos(az-saz));saz+=da*smooth;sel+=(el-sel)*smooth;sd+=(d-sd)*smooth;}az=saz;el=sel;d=sd;propagation(e->src,t,&wall,&cutoff,&wind,&rain);double spaceD=d+(globeMode?(1-a)*18.0:0),microA=fmin(1,u/.006),microR=fmin(1,(e->dur-u)/.028),clickGate=sin(microA*M_PI/2)*sin(microA*M_PI/2)*sin(microR*M_PI/2)*sin(microR*M_PI/2); double v;if(e->type==0||e->type==3){double base=e->type==3?e->f0:(cosmosMode?cosmos_frequency(e->src,t):(duetMode?duet_frequency(e->src,t):e->f0*pow(e->f1/e->f0,u/e->dur))),f=base*(1+wind*spatialWet);ph+=TAU*f/SR;v=sin(ph);} @@ -360,7 +369,7 @@ double arrival=s0+k+spaceD*SR/343.0;long at=(long)floor(arrival);double af=arrival-at;if(at>=0&&atsrc)*SR);if(!duetMode&&!cosmosMode&&echo=0&&frsrc;meterL[mi]+=cl*cl;meterR[mi]+=cr*cr;}} + int fr=(int)(at*FPS/(double)SR);if(fr>=0&&frsrc;meterL[mi]+=cl*cl;meterR[mi]+=cr*cr;long frameStart=(long)fr*SR/FPS;int wi=(int)((at-frameStart)*WAVE_POINTS*FPS/SR);if(wi<0)wi=0;if(wi>=WAVE_POINTS)wi=WAVE_POINTS-1;sourceWave[(mi*WAVE_POINTS)+wi]+=(float)((cl+cr)*.5*(WAVE_POINTS*FPS/(double)SR));}} } } // Cross-room early reflections. @@ -370,6 +379,12 @@ for(long i=0;in-SR?((n-i)/(double)SR):1;busL[i]*=g*fo;busR[i]*=g*fo;} } static int wav(const char*p){FILE*f=fopen(p,"wb");if(!f)return 0;long n=(long)(DUR*SR);uint32_t ds=n*8,sz=36+ds,sr=SR,br=SR*8,fs=16;uint16_t fm=3,ch=2,ba=8,bi=32; fwrite("RIFF",1,4,f);fwrite(&sz,4,1,f);fwrite("WAVEfmt ",1,8,f);fwrite(&fs,4,1,f);fwrite(&fm,2,1,f);fwrite(&ch,2,1,f);fwrite(&sr,4,1,f);fwrite(&br,4,1,f);fwrite(&ba,2,1,f);fwrite(&bi,2,1,f);fwrite("data",1,4,f);fwrite(&ds,4,1,f);for(long i=0;i=0&&X=0&&Y>16;p[o+1]=c>>8;p[o+2]=c;}}} static void glow(unsigned char*p,int x,int y,int r,uint32_t c,double strength){int rr=c>>16,gg=(c>>8)&255,bb=c&255;for(int yy=-r;yy<=r;yy++)for(int xx=-r;xx<=r;xx++){double d=sqrt(xx*xx+yy*yy)/(double)r;if(d>1)continue;int X=x+xx,Y=y+yy;if(X<0||X>=W||Y<0||Y>=H)continue;double a=strength*(1-d)*(1-d);int o=(Y*W+X)*3;p[o]=p[o]*(1-a)+rr*a;p[o+1]=p[o+1]*(1-a)+gg*a;p[o+2]=p[o+2]*(1-a)+bb*a;}} typedef struct{double x,y,z;} V3;typedef struct{int x,y;double z;int ok;} P2; @@ -406,6 +421,45 @@ // A short symmetric window removes 24 fps sparkle without inventing energy. double sl=0,sr=0;int count=0;for(int q=-2;q<=2;q++){int f=fr+q;if(f<0||f>=NFRAMES)continue;double w=q?1.0/(1+abs(q)):1.0;sl+=meterL[f*NSRC+src]*w;sr+=meterR[f*NSRC+src]*w;count+=(int)(SR/(double)FPS*w);} *l=count?sqrt(sl/count):0;*r=count?sqrt(sr/count):0; } +// A source is drawn as a spectrum with volume, not as a status dot. Frequency +// climbs through twelve logarithmic slices; current event energy controls each +// slice's cross-section. Ribs connect the slices into one translucent body. +#define SPEC_BANDS 12 +#define SPEC_RIBS 10 +static void source_spectrum(int src,double t,double band[SPEC_BANDS]){ + for(int b=0;bsrc!=src)continue;double a=env(e,t)*e->g;if(a<=0)continue;double u=fmax(0,fmin(1,(t-e->t)/e->dur)),f0=fmin(e->f0,e->f1),f1=fmax(e->f0,e->f1); + if(e->type==1||e->type==2||e->type==4||e->type==5){for(int b=0;b=f0*.82&&f<=f1*1.18)band[b]+=a*(.55+.45*sin(M_PI*(b+.5)/SPEC_BANDS));}} + else{double f=e->type==3?e->f0:(cosmosMode?cosmos_frequency(src,t):(duetMode?duet_frequency(src,t):e->f0*pow(e->f1/e->f0,u)));for(int b=0;b1e-8?.30*wave[wi]/peak:0,rr=fmax(.04,r+displacement),a=TAU*i/28,x=cos(a)*rr,y=sin(a)*rr;V3 q=plane==0?(V3){c.x+x,c.y+y,c.z}:plane==1?(V3){c.x+x,c.y,c.z+y}:(V3){c.x,c.y+x,c.z+y};if(i)line3(p,prev,q,cam,target,color,alpha);else first=q;prev=q;}line3(p,prev,first,cam,target,color,alpha);} +static void spectral_volume(unsigned char*p,int src,int fr,double t,V3 c,V3 cam,V3 target,int focus){double band[SPEC_BANDS],peak=0,ml,mr;source_spectrum(src,t,band);source_meter(fr,src,&ml,&mr);double heard=hypot(ml,mr);for(int b=0;bpeak)peak=band[b];if(peak<1e-9)peak=1; + V3 pt[SPEC_BANDS][SPEC_RIBS];double bodyScale=focus?1.22:1.0,alpha=.10+fmin(.42,heard*19)+(focus?.08:0); + for(int b=0;b=(long)(DUR*SR))si=(long)(DUR*SR)-1;double sample=busL[si]*(1-u)+busR[si]*u,x=left+(right-left)*u,y=baseY-sample*(52+40*v);uint32_t c=stereo_color(pan);if(col)line2(p,lastX,lastY,(int)x,(int)y,c,.10+fmin(.18,fabs(sample)*.9));if(row)line2(p,prevX[col],prevY[col],(int)x,(int)y,c,.055+fmin(.12,fabs(sample)*.55));prevX[col]=(int)x;prevY[col]=(int)y;lastX=(int)x;lastY=(int)y;}} + // A bright current-time trace makes the plane's instantaneous stereo wave + // legible against its own short perspective history. + int lastX=0,lastY=0;for(int col=0;col<=cols;col++){double u=col/(double)cols,pan=u*2-1;long si=now+col*3;if(si>=(long)(DUR*SR))si=(long)(DUR*SR)-1;double sample=busL[si]*(1-u)+busR[si]*u,x=178+(542-178)*u,y=105-sample*62;if(col)line2(p,lastX,lastY,(int)x,(int)y,stereo_color(pan),.42);lastX=(int)x;lastY=(int)y;}} +// Source packets travel from their projected spectral body to the windshield. +// Existence/size comes from captured per-source waveform samples; impact x is +// the source's measured post-HRTF stereo balance and impact y its live spectral +// centroid. The collision splash therefore says what arrived and where. +static void windshield_particles(unsigned char*p,int fr,double t,V3 cam,V3 target){for(int s=0;s.90){double u=(phase-.90)/.10;int rr=3+(int)(25*u);ellipse2(p,ix,iy,rr,(int)(rr*.46),S[s].color,(1-u)*(.18+.32*fabs(sample)));for(int ray=0;ray<5;ray++){double a=TAU*(ray/5.0+s*.071),len=5+14*u;line2(p,ix+(int)(cos(a)*3),iy+(int)(sin(a)*2),ix+(int)(cos(a)*len),iy+(int)(sin(a)*len*.46),S[s].color,(1-u)*.34);}}}}} static P2 chart_point(double x,double y){return(P2){226+(int)(x*13.0),260-(int)(y*13.0),0,1};} static void acoustics_frame(unsigned char*p,int fr,double t,Listener l){ uint32_t ink=brightMode?0x24383c:0xc7e5e5,sub=brightMode?0x718b8e:0x527a7e,panel=brightMode?0xe4ebe8:0x101c20; @@ -561,15 +615,19 @@ 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)); - 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,.18+fmin(.7,heard*16));stereo_lobes(p,sp,ml,mr,core,S[s].color);marble(p,sp.x,sp.y,core+2,S[s].color,heard);} + 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); } + stereo_windshield(p,t); + windshield_particles(p,fr,t,cam,target); P2 lp=project(lv,cam,target),hd=project((V3){l.x+cos(l.heading)*.55,l.y+sin(l.heading)*.55,1.6},cam,target); if(lp.ok){int rr=(int)fmax(4,fmin(20,130/lp.z));glow(p,lp.x,lp.y,rr*3,0xf6c915,.35);dot(p,lp.x,lp.y,rr,0xffd54f);if(hd.ok)line2(p,lp.x,lp.y,hd.x,hd.y,0xff6b9d,.95);} // Receiver cockpit. The canopy frames the view while the lower scanner maps @@ -593,4 +651,4 @@ 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;double ptTime=32;int ptSpp=32,sceneFrames=0,tempoExplicit=0;for(int i=1;i