diff --git a/pop/nullabye/c/ac_mesh_acoustics.h b/pop/nullabye/c/ac_mesh_acoustics.h new file mode 100644 index 0000000000..f01d105d89 --- /dev/null +++ b/pop/nullabye/c/ac_mesh_acoustics.h @@ -0,0 +1,28 @@ +// ac_mesh_acoustics.h — reduced-order material acoustics for prompt-rock meshes. +#ifndef AC_MESH_ACOUSTICS_H +#define AC_MESH_ACOUSTICS_H +#include "ac_prompt_rock.h" + +typedef struct { double density,young,loss,absorption,radiation; } ACMaterial; +typedef struct { double volume,area,inertia[3],mode[6],decay[6],directivity; } ACMeshAcoustics; +static const ACMaterial AC_MAT_WOOD={690,10.5e9,.034,.22,.72}; +static const ACMaterial AC_MAT_ALUMINUM={2700,69e9,.006,.05,.91}; +static const ACMaterial AC_MAT_GLASS={2500,70e9,.012,.08,.84}; +static const ACMaterial AC_MAT_STONE={2700,45e9,.022,.16,.63}; + +static ACRockV ac_avsub(ACRockV a,ACRockV b){return ac_rv(a.x-b.x,a.y-b.y,a.z-b.z);} +static ACRockV ac_avcross(ACRockV a,ACRockV b){return ac_rv(a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x);} +static double ac_avdot(ACRockV a,ACRockV b){return a.x*b.x+a.y*b.y+a.z*b.z;} +static void ac_mesh_analyze(const ACRock*r,ACMaterial m,ACMeshAcoustics*a){ + memset(a,0,sizeof(*a));for(int i=0;inf;i++){ACRockFace f=r->f[i];ACRockV p=r->v[f.a],q=r->v[f.b],s=r->v[f.c],n=ac_avcross(ac_avsub(q,p),ac_avsub(s,p));a->area+=.5*sqrt(ac_avdot(n,n));a->volume+=ac_avdot(p,ac_avcross(q,s))/6;} + a->volume=fabs(a->volume);double mass=fmax(1e-6,a->volume*m.density),ex=r->extent[0],ey=r->extent[1],ez=r->extent[2];a->inertia[0]=mass*(ey*ey+ez*ez)/12;a->inertia[1]=mass*(ex*ex+ez*ez)/12;a->inertia[2]=mass*(ex*ex+ey*ey)/12; + double wave=sqrt(m.young/m.density),length[6]={ex,ey,ez,sqrt(ex*ey),sqrt(ey*ez),sqrt(ex*ez)},factor[6]={.42,.47,.53,.71,.89,1.17};for(int k=0;k<6;k++){a->mode[k]=fmax(22,wave/(6.283185307179586*fmax(.08,length[k]))*factor[k]);a->decay[k]=1/(m.loss*6.283185307179586*a->mode[k]+1e-6);} + double longest=fmax(ex,fmax(ey,ez)),shortest=fmin(ex,fmin(ey,ez));a->directivity=m.radiation*fmin(.92,fmax(.08,1-shortest/(longest+1e-6))); +} +// Cosine-power blend between diffuse enclosure radiation and the dominant +// speaker plane. `facingDot` is the oriented mesh-forward · receiver direction. +static double ac_mesh_radiation(const ACMeshAcoustics*a,ACMaterial m,double facingDot){double front=pow(fmax(0,facingDot),1+5*a->directivity),back=.08+.22*(1-a->directivity);return m.radiation*((1-a->directivity)+a->directivity*(front+(facingDot<0?back:0)))*(1-m.absorption);} +// Area-weighted radiation from every polygon center/normal. The mesh's local +// +Z axis is aimed at the receiver; phase supplies the secondary skeletal rock. +static double ac_mesh_facet_radiation(const ACRock*r,ACMaterial m,double phase){double sum=0,area=0,tx=.16*sin(phase*.37),ty=.12*cos(phase*.29);for(int i=0;inf;i++){ACRockFace f=r->f[i];ACRockV a=r->v[f.a],b=r->v[f.b],c=r->v[f.c],n=ac_avcross(ac_avsub(b,a),ac_avsub(c,a));double ar=.5*sqrt(ac_avdot(n,n));if(ar<1e-9)continue;n=ac_rv(n.x/(2*ar),n.y/(2*ar),n.z/(2*ar));double facing=n.z+tx*n.x+ty*n.y,front=pow(fmax(0,facing),2.4),back=facing<0?.055:0;sum+=ar*(front+back);area+=ar;}double diaphragm=.90+.10*sin(phase);return m.radiation*(1-m.absorption)*diaphragm*(.32+.95*sum/(area+1e-9));} +#endif diff --git a/pop/nullabye/c/ac_prompt_rock.h b/pop/nullabye/c/ac_prompt_rock.h new file mode 100644 index 0000000000..0e3be62fe0 --- /dev/null +++ b/pop/nullabye/c/ac_prompt_rock.h @@ -0,0 +1,34 @@ +// ac_prompt_rock.h — C port of Slab's SigilMesh prompt-rock generator. +// Same SplitMix64 stream, icosahedron, hashed subdivision, radial displacement, +// and anisotropic scale. Geometry also supplies deterministic acoustic modes. +#ifndef AC_PROMPT_ROCK_H +#define AC_PROMPT_ROCK_H +#include +#include +#include + +typedef struct { double x,y,z; } ACRockV; +typedef struct { int a,b,c; } ACRockFace; +typedef struct { ACRockV v[42]; ACRockFace f[80]; int nv,nf; double roughness,scale[3],extent[3]; } ACRock; +typedef struct { uint64_t s; } ACRockRng; +static uint64_t ac_rock_next(ACRockRng*r){r->s+=UINT64_C(0x9e3779b97f4a7c15);uint64_t z=r->s;z=(z^(z>>30))*UINT64_C(0xbf58476d1ce4e5b9);z=(z^(z>>27))*UINT64_C(0x94d049bb133111eb);return z^(z>>31);} +static double ac_rock_unit(ACRockRng*r){return (ac_rock_next(r)>>40)/(double)(1u<<24);} +static ACRockV ac_rv(double x,double y,double z){ACRockV v={x,y,z};return v;} +static ACRockV ac_rnorm(ACRockV v){double n=sqrt(v.x*v.x+v.y*v.y+v.z*v.z);return ac_rv(v.x/n,v.y/n,v.z/n);} +static int ac_rock_mid(ACRock*r,int a,int b,int ea[30],int eb[30],int em[30],int*ne){if(a>b){int q=a;a=b;b=q;}for(int i=0;i<*ne;i++)if(ea[i]==a&&eb[i]==b)return em[i];ACRockV p=ac_rnorm(ac_rv((r->v[a].x+r->v[b].x)*.5,(r->v[a].y+r->v[b].y)*.5,(r->v[a].z+r->v[b].z)*.5));int m=r->nv;r->v[r->nv++]=p;ea[*ne]=a;eb[*ne]=b;em[*ne]=m;(*ne)++;return m;} +static void ac_rock_generate(uint64_t seed,ACRock*r){ + memset(r,0,sizeof(*r));ACRockRng rng={seed?seed:UINT64_C(0x9e3779b97f4a7c15)};double t=(1+sqrt(5.0))/2; + ACRockV raw[12]={ac_rv(-1,t,0),ac_rv(1,t,0),ac_rv(-1,-t,0),ac_rv(1,-t,0),ac_rv(0,-1,t),ac_rv(0,1,t),ac_rv(0,-1,-t),ac_rv(0,1,-t),ac_rv(t,0,-1),ac_rv(t,0,1),ac_rv(-t,0,-1),ac_rv(-t,0,1)}; + for(int i=0;i<12;i++)r->v[i]=ac_rnorm(raw[i]);r->nv=12; + ACRockFace base[20]={{0,11,5},{0,5,1},{0,1,7},{0,7,10},{0,10,11},{1,5,9},{5,11,4},{11,10,2},{10,7,6},{7,1,8},{3,9,4},{3,4,2},{3,2,6},{3,6,8},{3,8,9},{4,9,5},{2,4,11},{6,2,10},{8,6,7},{9,8,1}};memcpy(r->f,base,sizeof(base));r->nf=20; + int subdivide=ac_rock_unit(&rng)>=.34;if(subdivide){ACRockFace next[80];int ea[30],eb[30],em[30],ne=0,nf=0;for(int i=0;i<20;i++){int a=r->f[i].a,b=r->f[i].b,c=r->f[i].c,ab=ac_rock_mid(r,a,b,ea,eb,em,&ne),bc=ac_rock_mid(r,b,c,ea,eb,em,&ne),ca=ac_rock_mid(r,c,a,ea,eb,em,&ne);next[nf++]=(ACRockFace){a,ab,ca};next[nf++]=(ACRockFace){b,bc,ab};next[nf++]=(ACRockFace){c,ca,bc};next[nf++]=(ACRockFace){ab,bc,ca};}memcpy(r->f,next,sizeof(next));r->nf=80;} + double amp=.10+.30*ac_rock_unit(&rng);r->roughness=amp;for(int k=0;k<3;k++)r->scale[k]=.66+.64*ac_rock_unit(&rng); + double lo[3]={1e9,1e9,1e9},hi[3]={-1e9,-1e9,-1e9};for(int i=0;inv;i++){double d=1+(ac_rock_unit(&rng)-.5)*2*amp;r->v[i].x*=d*r->scale[0];r->v[i].y*=d*r->scale[1];r->v[i].z*=d*r->scale[2];double p[3]={r->v[i].x,r->v[i].y,r->v[i].z};for(int k=0;k<3;k++){if(p[k]hi[k])hi[k]=p[k];}} + for(int k=0;k<3;k++)r->extent[k]=hi[k]-lo[k]; +} +static void ac_rock_modes(const ACRock*r,double modes[5],double gains[5]){ + double ex=r->extent[0],ey=r->extent[1],ez=r->extent[2],body=4500/(ex+ey+ez),facet=r->nf==20?1.18:1.0,rough=1+r->roughness*.65; + modes[0]=body*sqrt(1/(ex*ex)+1/(ey*ey))*facet;modes[1]=body*sqrt(1/(ey*ey)+1/(ez*ez))*1.47;modes[2]=body*sqrt(1/(ex*ex)+1/(ez*ez))*2.13;modes[3]=(modes[0]+modes[1])*1.73*rough;modes[4]=(modes[1]+modes[2])*2.08*rough; + double g[5]={.46,.34,.26,.19,.13};memcpy(gains,g,sizeof(g)); +} +#endif diff --git a/pop/nullabye/c/ac_terrarium_pt.h b/pop/nullabye/c/ac_terrarium_pt.h new file mode 100644 index 0000000000..e9e2c937db --- /dev/null +++ b/pop/nullabye/c/ac_terrarium_pt.h @@ -0,0 +1,72 @@ +#ifndef AC_TERRARIUM_PT_H +#define AC_TERRARIUM_PT_H + +// Small allocation-free CPU path tracer for spatial-sineabye's luminous world. +// It traces diffuse/emissive bodies, a dielectric listening shell, and colored +// participating media. The caller owns animation, audio, and graphic overlays. + +typedef struct { double x,y,z; } PTVec; +typedef struct { PTVec center,color; double radius,emission,roughness; } PTBody; +typedef struct { uint64_t state; } PTRng; +#ifdef __CUDACC__ +#define PT_HD __host__ __device__ +#else +#define PT_HD +#endif + +static PT_HD inline PTVec ptv(double x,double y,double z){return(PTVec){x,y,z};} +static PT_HD inline PTVec pt_add(PTVec a,PTVec b){return ptv(a.x+b.x,a.y+b.y,a.z+b.z);} +static PT_HD inline PTVec pt_sub(PTVec a,PTVec b){return ptv(a.x-b.x,a.y-b.y,a.z-b.z);} +static PT_HD inline PTVec pt_mul(PTVec a,double b){return ptv(a.x*b,a.y*b,a.z*b);} +static PT_HD inline PTVec pt_had(PTVec a,PTVec b){return ptv(a.x*b.x,a.y*b.y,a.z*b.z);} +static PT_HD inline double pt_dot(PTVec a,PTVec b){return a.x*b.x+a.y*b.y+a.z*b.z;} +static PT_HD inline PTVec pt_cross(PTVec a,PTVec b){return ptv(a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x);} +static PT_HD inline PTVec pt_norm(PTVec a){double n=sqrt(pt_dot(a,a));return n?pt_mul(a,1/n):a;} +static PT_HD inline uint64_t pt_hash(uint64_t x){x^=x>>30;x*=0xbf58476d1ce4e5b9ULL;x^=x>>27;x*=0x94d049bb133111ebULL;return x^(x>>31);} +static PT_HD inline double pt_rand(PTRng*r){r->state=pt_hash(r->state+0x9e3779b97f4a7c15ULL);return(r->state>>11)*(1.0/9007199254740992.0);} +static PT_HD inline PTVec pt_cosine(PTRng*r,PTVec n){double a=TAU*pt_rand(r),z=sqrt(pt_rand(r)),q=sqrt(1-z*z);PTVec h=fabs(n.z)<.9?ptv(0,0,1):ptv(1,0,0),u=pt_norm(pt_cross(h,n)),v=pt_cross(n,u);return pt_norm(pt_add(pt_add(pt_mul(u,cos(a)*q),pt_mul(v,sin(a)*q)),pt_mul(n,z)));} +static PT_HD inline double pt_sphere(PTVec ro,PTVec rd,PTVec c,double radius){PTVec oc=pt_sub(ro,c);double b=pt_dot(oc,rd),h=b*b-pt_dot(oc,oc)+radius*radius;if(h<0)return 1e30;h=sqrt(h);double t=-b-h;return t>1e-4?t:(-b+h>1e-4?-b+h:1e30);} +static PT_HD inline PTVec pt_sky(PTVec d){double h=.5+.5*d.z;return ptv(.006+.018*h,.009+.028*h,.018+.065*h);} + +static PT_HD PTVec pt_trace(PTVec ro,PTVec rd,const PTBody*b,int count,PTRng*r,double time){ + PTVec light=ptv(0,0,0),through=ptv(1,1,1); +#ifdef PT_NO_SHELL + int inside=1; +#else + int inside=pt_dot(ro,ro)<6.8*6.8; +#endif + for(int bounce=0;bounce<7;bounce++){ + double nearest=1e30;int hit=-1;for(int i=0;i1){PTVec to=pt_sub(b[i].center,ro);double d2=pt_dot(to,to),d=sqrt(d2),vis=1;PTVec ld=pt_mul(to,1/d);for(int o=0;o0){light=pt_add(light,pt_mul(pt_had(through,b[hit].color),b[hit].emission));break;} + // Direct emitter sampling on matte bodies reveals colored indirect fill. + for(int i=0;i1&&i!=hit){PTVec to=pt_sub(b[i].center,hp);double d2=pt_dot(to,to),d=sqrt(d2),nl=fmax(0,pt_dot(n,pt_mul(to,1/d)));if(nl>0)light=pt_add(light,pt_mul(pt_had(through,pt_had(b[hit].color,b[i].color)),b[i].emission*nl/(1.5+d2)));} + through=pt_had(through,b[hit].color);ro=pt_add(hp,pt_mul(n,1e-3));PTVec diffuse=pt_cosine(r,n),refl=pt_sub(rd,pt_mul(n,2*pt_dot(rd,n)));rd=pt_norm(pt_add(pt_mul(refl,1-b[hit].roughness),pt_mul(diffuse,b[hit].roughness))); + if(bounce>3){double survive=fmin(.94,fmax(through.x,fmax(through.y,through.z)));if(pt_rand(r)>survive)break;through=pt_mul(through,1/survive);} + } + return light; +} + +static void pt_render_rgb(unsigned char*out,int w,int h,PTVec cam,PTVec target,const PTBody*b,int count,int spp,int frame,double time){ + PTVec f=pt_norm(pt_sub(target,cam)),right=pt_norm(pt_cross(f,ptv(0,0,1))),up=pt_cross(right,f);double aspect=w/(double)h,tanHalf=.72; + for(int y=0;y #include #include "ac_hrtf.h" +#include "ac_prompt_rock.h" #ifndef M_PI #define M_PI 3.14159265358979323846 #endif +#include "ac_mesh_acoustics.h" #define TAU (2*M_PI) #define SR 48000 -#define BPM 76.0 +#define BPM 104.0 #define BEAT (60.0/BPM) #define BAR (4*BEAT) #define DUR (38*BAR) @@ -24,10 +26,13 @@ #define MAXE 4096 #define NSRC 12 #define NFRAMES ((int)(DUR*FPS)) +#include "ac_terrarium_pt.h" typedef struct { double x,y,z,mass; const char *name; uint32_t color; } Source; typedef struct { double t,dur,f0,f1,g,atk,rel; int src,type; uint32_t seed; } Event; -typedef struct { double x,y,vx,vy,heading; } Listener; +typedef struct { double x,y,vx,vy,heading,z,vz,pitch,roll; } Listener; +typedef struct { double x,y,z,radius,tempo,phase; const char *name; uint32_t color; } StarSystem; +typedef struct { int system,role,wave; double orbit,rate,eccentricity,height,phase,gain,midi; } EmitterDef; static Source S[NSRC]={ {-4.2,-2.8,1.0,1.3,"bass",0x4ecdc4},{.4,-1.2,.4,2.0,"boom",0xff6b6b}, {-2.3,1.2,1.2,.8,"pad-l",0x63cdda},{4.8,2.4,1.5,.8,"pad-r",0x778beb}, @@ -42,17 +47,126 @@ static Listener *L; static float *busL,*busR,*fieldGain; // visualizer reads this telemetry instead of guessing loudness from envelopes. static double *meterL,*meterR; static double spatialWet=.32; +static int brightMode=0; +static int themeExplicit=0; +static int globeMode=0; +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 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]={ + {-7.2,-1.8,1.1,4.2,104,.15,"percussion sun",0xff6b6b}, + { 7.4, 2.2,1.8,5.0,78,.61,"harmony sun",0x63cdda} +}; +static const EmitterDef COSMOS[NSRC]={ + {0,0,0,2.3,1.00,.82,.5,.00,.105,36},{0,1,0,3.1,.50,.44,1.1,.19,.052,36}, + {0,2,1,1.8,2.00,.70,1.8,.43,.010,0},{0,3,0,3.7,.25,.25,2.0,.71,.026,48}, + {1,4,0,2.5,.75,.30,1.3,.08,.052,60},{1,5,0,3.2,.50,.22,2.0,.31,.030,64}, + {1,6,0,3.8,.50,.18,2.5,.54,.028,67},{1,7,1,2.0,1.50,.58,2.9,.77,.007,0}, + {0,8,0,4.1,.20,.20,2.4,.26,.018,43},{1,9,0,4.5,.25,.16,1.0,.64,.018,72}, + {0,10,1,2.8,.75,.62,3.0,.88,.006,0},{1,11,0,4.8,.125,.12,3.2,.92,.014,76} +}; +static ACRock BODY_MESH[NSRC+3];static ACMeshAcoustics BODY_ACOUSTICS[NSRC+3];static ACMaterial BODY_MATERIAL[NSRC+3]; +static Source source_at(int s,double t); +static ACMaterial cosmos_material(int i){if(i%5==0)return AC_MAT_ALUMINUM;if(i%5==1)return AC_MAT_WOOD;if(i%5==2)return AC_MAT_GLASS;return AC_MAT_STONE;} +static void init_cosmos_bodies(void){for(int i=0;i0&&u<1){double sh=sin(M_PI*u);z+=sh*(.72*sin(t*11+k*2.1)+.34*sin(t*23+k));x+=sh*.24*sin(t*17+k);y+=sh*.20*cos(t*19+k*.7);}} + Source q={x+.35*sin(t*.037+k),y+.28*cos(t*.031+k*.7),z,4,d.name,d.color};return q;} +static double cosmos_plane_gain(int src,double t,double az,double el){EmitterDef e=COSMOS[src];double phase=TAU*(SYSTEMS[e.system].tempo/60.0)*t*e.rate+e.phase*TAU; + static long facetTick[NSRC]={0};static double facetEmitter[NSRC],facetRelay[NSRC];long tick=(long)(t*1000)+1;ACMaterial em=BODY_MATERIAL[src],sm=BODY_MATERIAL[NSRC+e.system],hm=BODY_MATERIAL[NSRC+2];if(facetTick[src]!=tick){facetTick[src]=tick;facetEmitter[src]=ac_mesh_facet_radiation(&BODY_MESH[src],em,phase)/(em.radiation*(1-em.absorption)+1e-6);facetRelay[src]=ac_mesh_facet_radiation(&BODY_MESH[NSRC+e.system],sm,phase*.23)/(sm.radiation*(1-sm.absorption)+1e-6);}double emitter=facetEmitter[src],relay=facetRelay[src],shipFacing=cos(az)*cos(el),receiver=ac_mesh_radiation(&BODY_ACOUSTICS[NSRC+2],hm,shipFacing)/(hm.radiation*(1-hm.absorption)+1e-6); + // Direction and material remain audible, but enclosure losses are normalized + // before composition so three shaped stages do not mute one another. + double shaped=cbrt(fmax(0,emitter*relay*receiver));return .56+.72*fmin(1.05,shaped);} +static double shell_arrival(int s,double t){ + // One global audible field: every body begins outside and enters in score order. + static const double enter[NSRC]={6.0,9.5,2.0,4.0,0.8,13.0,16.0,18.0,11.0,12.0,20.0,23.0}; + double u=(t-enter[s])/3.2;if(u<=0)return 0;if(u>=1)return 1;return u*u*(3-2*u); +} +static double shell_listen(int s,double t){double u=(shell_arrival(s,t)-.36)/.24;if(u<=0)return 0;if(u>=1)return 1;return u*u*(3-2*u);} +static int voice_enabled(int s){if(cosmosMode)return 1;if(duetMode)return s==0||s==1||s==4||s==6;static const int order[4]={4,0,2,3};if(voiceCount>=12)return 1;for(int i=0;i=12)return;int w=0;for(int i=0;i=50&&t<=78){double u=(t-50)/28;u=u*u*(3-2*u);double a=TAU*2*u; +static Source source_at(int s,double t){Source q=S[s]; + if(cosmosMode){ + EmitterDef e=COSMOS[s];Source sun=system_sun_at(e.system,t);StarSystem sys=SYSTEMS[e.system]; + double parent=TAU*(sys.tempo/60.0)*t+sys.phase*TAU; + double mean=parent*e.rate+e.phase*TAU,child=eccentric_phase(mean,e.eccentricity); + // system sun -> dominant orbital plane -> eccentric child endpoint + double precess=.22*sin(parent*.17+s*.81),bone=parent+precess; + double ux=cos(bone),uy=sin(bone),vx=-uy,vy=ux; + double radial=e.orbit*(1-e.eccentricity*cos(child)); + q.x=sun.x+ux*(radial*cos(child))+vx*(e.orbit*.42*sin(child)); + q.y=sun.y+uy*(radial*cos(child))+vy*(e.orbit*.42*sin(child)); + q.z=sun.z+e.height+.48*sin(child+s*.31);q.color=e.system?0x63cdda:S[s].color; + // Orchestration by travel: each body begins outside its sun's useful local + // field and physically joins the system in score order. + static const double enter[NSRC]={0,6,14,22,10,18,26,34,30,38,46,54};double u=(t-enter[s])/7;if(u<1){u=fmax(0,u);u=u*u*(3-2*u);double far=13+fmod(s*2.7,4),ox=cos(e.phase*TAU)*far,oy=sin(e.phase*TAU)*far;q.x=sun.x+ox+(q.x-sun.x-ox)*u;q.y=sun.y+oy+(q.y-sun.y-oy)*u;q.z=sun.z+5.5+(q.z-sun.z-5.5)*u;} + return q; + } + if(globeMode){ + // Lattice score: listener -> flock -> three clusters -> local resonators. + // Every level has its own phase and breathing radius; distance is the pulse. + Listener anchor={0,-.5,0,0,0};if(L){double at=fmax(0,t-.06)*CTRL;int i=(int)at,max=(int)(DUR*CTRL)-1;if(i>max)i=max;double f=at-i;Listener a=L[i],b=L[i+1];anchor.x=a.x+(b.x-a.x)*f;anchor.y=a.y+(b.y-a.y)*f;anchor.vx=a.vx+(b.vx-a.vx)*f;anchor.vy=a.vy+(b.vy-a.vy)*f;anchor.heading=a.heading+(b.heading-a.heading)*f;} + int group=s/4,lane=s%4; + // Integral of an exponentially slowing angular velocity: the composition + // begins at 520 RPM and continuously settles to its 76 BPM revolution. + double globalTurns=dominant_turns(t); + double ga=TAU*globalTurns+group*TAU/3,clusterR=(2.1+.62*group)*(1+.18*sin(TAU*t/(5.7+group*1.3)+group)); + double cx=anchor.x+cos(ga)*clusterR,cy=anchor.y+sin(ga)*clusterR; + double localRate=.13+.037*lane+.019*group,la=TAU*(localRate*t)+lane*TAU/4+group*.41; + double localR=(.55+.18*lane)*(1+.28*sin(TAU*t/(2.3+.31*s)+s*.77)); + double schooling=.22*sin(TAU*t/(1.1+.07*s)+s*1.19); + q.x=cx+cos(la)*localR-cos(ga)*schooling; + q.y=cy+sin(la)*localR-sin(ga)*schooling; + q.z=.55+.42*group+.28*lane+.46*sin(TAU*t/(1.7+.11*s)+s*.53)+.18*sin(ga+la); + if(duetMode&&(s==0||s==1||s==4||s==6)){ + // A continuous eccentric orbit crosses the listening field. There is no + // amplitude envelope or radial reset: proximity alone makes each pulse. + // Skeleton: listener anchor -> rotating flock bone -> precessing shoulder + // -> eccentric child orbit -> audible endpoint. + double precess=.24*sin(la*.37+s*.9),bone=ga+precess; + double rate=s==6?2.0:(s==1?.5:1.0),offset=s==4?M_PI*.58:(s==1?M_PI*.27:(s==6?M_PI*.83:0)); + double mean=TAU*(t/BEAT)*rate+offset,ecc=s==0?.84:(s==6?.72:(s==1?.42:.28)); + double orbit=eccentric_phase(mean,ecc),flex=.16*sin(la*1.7+s); + double along=cos(orbit+flex),across=sin(orbit); + double axis=s==0?3.05:(s==1?3.5:(s==6?2.35:2.65)),side=s==0?.92:(s==1?1.25:(s==6?.68:1.08)); + double ux=cos(bone),uy=sin(bone),vx=-uy,vy=ux; + // The dominant bone turns around an offset world pivot. If it were centered + // on the listener, spin would change only azimuth and could not pulse range. + double pivotX=anchor.x+1.45,pivotY=anchor.y-.82; + double shoulderX=pivotX+ux*axis,shoulderY=pivotY+uy*axis; + q.x=shoulderX-ux*(axis*along)+vx*(side*across); + q.y=shoulderY-uy*(axis*along)+vy*(side*across); + q.z=(s==0?.72:(s==1?.95:(s==6?2.45:1.72)))+(s==0?.18:(s==6?.22:.48))*sin(orbit+(s==4?.7:0)); + } + return q; + } + if(t>=50&&t<=78){double u=(t-50)/28;u=u*u*(3-2*u);double a=TAU*2*u; // Eight-turn centrifuge from 62–70 s. Smoothstep gives it a physical ramp; // eight whole turns land at the original orientation with no visual/audio cut. if(t>=62){double x=fmin(1,(t-62)/8);x=x*x*(3-2*x);a+=TAU*8*x;} double x=q.x,y=q.y;q.x=x*cos(a)-y*sin(a);q.y=x*sin(a)+y*cos(a);q.z+=.22*sin(a+s*.7);} + // In the visual/listening-field cut, the whole composition physically winds + // up after its opening assembly. This rotation feeds gravity, Doppler, HRTF, + // telemetry, and light transport; the camera does not manufacture the spin. // Echo and air are memory-bodies: they occupy where the listener used to be. // During simulation these indices are always in the already-computed past. if(L&&(s==5||s==11)){double lag=s==5?2.2:5.5;int i=(int)(fmax(0,t-lag)*CTRL),max=(int)(DUR*CTRL);if(i>max)i=max;Listener p=L[i];double side=s==5?.55:-.8;q.x=p.x-cos(p.heading)*.45-sin(p.heading)*side;q.y=p.y-sin(p.heading)*.45+cos(p.heading)*side;q.z=s==5?2.35:1.85;} @@ -69,6 +183,23 @@ static void glide(double t,double d,double f0,double f1,double g,int src){ static void noisev(double t,double d,double lo,double hi,double g,int src){ if(NEt;if(u<0||u>=e->dur)retu static const int chord[4][4]={{48,55,64,74},{45,52,60,71},{53,60,69,67},{55,62,71,69}}; static const int root[4]={36,33,41,43}; static const int mel[4][4]={{76,79,81,81},{79,76,74,74},{72,74,76,79},{74,76,76,72}}; +static double duet_frequency(int s,double t){ + if(s==0)return 36; + if(s==1){static const double roots[8]={36,36,33,33,29,31,33,28};double turns=dominant_turns(t)*.25,whole=floor(turns),phase=turns-whole;int i=((long)whole)%8;if(i<0)i+=8;double x=fmax(0,fmin(1,(phase-.86)/.14));x=x*x*(3-2*x);return hz(roots[i]+(roots[(i+1)%8]-roots[i])*x);} + static const double phrase[16]={64,67,69,71,69,67,64,62,60,64,67,72,71,67,69,64}; + double turns=dominant_turns(t),whole=floor(turns),phase=turns-whole; + int i=((long)whole)%16;if(i<0)i+=16; + // Phase remains continuous, but frequency changes cleanly at the angular + // boundary. Removing the double portamento avoids the rubbery squeak. + return hz(phrase[i]-12); +} +static double cosmos_frequency(int s,double t){ + EmitterDef e=COSMOS[s];if(e.wave)return 0; + int bar=(int)(t/BAR),ci=(bar/2)&3;static const int rootMidi[4]={36,32,39,34};static const int melody[16]={0,3,5,7,10,7,5,3,0,5,7,12,10,7,5,3};int root=rootMidi[ci],role=e.role,midi; + if(role==0)midi=36;else if(role==1)midi=root;else if(role==3)midi=root+7;else if(role==8)midi=root+12; + else if(role==4){int step=(int)floor(dominant_turns(t));midi=root+24+melody[step&15];} + else if(role==5)midi=root+(ci==1?16:15);else if(role==6)midi=root+19;else if(role==9)midi=root+(ci==2?23:22);else midi=root+31; + return hz(midi); +} static void score(void){ + if(cosmosMode){init_cosmos_bodies();for(int s=0;s=5?.78:1),s,.006,.035);}for(int b=0;b<38;b++){double t=b*BAR;for(int q=0;q<8;q++)if(!(b%8==7&&q>=6))chipv(t+q*BEAT*.5,(q==0||q==4)?.0215:.0121,(q&1)?7:10);}for(int b=2;b<38;b+=4){int a=(b/2)&1?10:7,o=a==10?7:10;double t=b*BAR,main=b<14?.068:(b<30?.087:.060);blastv(t,main,a);if(b>=14&&b<32)blastv(t+2.5*BEAT,main*.52,o);if(b>=22&&b<30)blastv(t+BAR+3.25*BEAT,main*.42,o);}return;} + if(duetMode){ev(0,DUR+.2,36,.082,0,.006,.035);ev(0,DUR+.2,36,.064,1,.006,.035);ev(0,DUR+.2,64,.078,4,.006,.035);noisev(0,DUR+.2,1400,3200,.012,6);return;} + if(globeMode){ + // A resonator is a place, not a note event: one fixed-frequency oscillator + // with continuous phase for the entire work. Composition comes exclusively + // from listener/source motion, Doppler, interference, and spatial filtering. + static const double pitch[NSRC]={36,48,55,64,72,79,84,88,60,67,43,76}; + static const double gain[NSRC]={.060,.032,.040,.038,.048,.022,.010,.009,.025,.023,.034,.014}; + for(int s=0;s=20&&b<28)?4.0/3:1; + if(globeMode&&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,.03,3,4.0,3.0);ev(t,4*BAR+.3,55,.018,4,4.8,3.4);}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. - for(int q=0;q<4;q++)noisev(t+(q+.5)*BEAT,.085,4200,10200,.065,q&1?7:6); - noisev(t,BAR,1700,5700,.013,11); - for(int q=1;q<4;q+=2)noisev(t+q*BEAT,.15,620,2100,.052,q==1?8:9); + double noiseScale=globeMode?.16:1.0; + for(int q=0;q<4;q++)noisev(t+(q+.5)*BEAT,.085,4200,10200,.065*noiseScale,q&1?7:6); + noisev(t,BAR,1700,5700,.013*noiseScale,11); + for(int q=1;q<4;q+=2)noisev(t+q*BEAT,.15,620,2100,.052*noiseScale,q==1?8:9); if(b>=4&&b<35){ev(t,BAR,root[b%4],.055,0,.08,.5);glide(t,.34,66,40,.13,1);glide(t,.58,46,31,.105,1);glide(t+2*BEAT,.34,66,40,.12,1);glide(t+2*BEAT,.58,46,31,.095,1);} // Immediate melodic line; small rhythmic cells stay consonant in the bridge. for(int q=0;q<4;q++){double raw=mel[b%4][q]-12+12*log2(tr),m=fmin(69,raw);ev(t+q*BEAT,BEAT*.88,m,.075,4,.045,.34);if(b>=12&&b<32)ev(t+(q+.5)*BEAT,BEAT*.48,m+12,.014,5,.06,.28);} @@ -99,6 +261,18 @@ static void score(void){ static void simulate(void){ int n=(int)(DUR*CTRL)+1;L=calloc(n,sizeof(*L));L[0]=(Listener){0,-.5,0,0,0};double dt=1.0/CTRL; + if(cosmosMode){ + // Six-DOF goal-seeking ship. It flies a sequence of world-space objectives; + // yaw/pitch follow velocity and roll banks continuously into each turn. + static const Source goals[]={{-10,-5,1},{-7,-1,4},{0,6,-1},{7,2,0},{10,-4,4},{0,0,7},{-7,-2,-2},{0,-6,2},{7,2,5},{0,1,0}}; + L[0]=(Listener){-10,-5,0,0,0,1,0,0,0}; + for(int i=1;i=n)i=n-1;double f=u-i;Listener a=L[i],b=L[i+1]; double x=a.x+(b.x-a.x)*f,y=a.y+(b.y-a.y)*f,h=a.heading+(b.heading-a.heading)*f;Source so=source_at(src,t); + if(cosmosMode){double z=a.z+(b.z-a.z)*f,pitch=a.pitch+(b.pitch-a.pitch)*f,roll=a.roll+(b.roll-a.roll)*f;Source sun=system_sun_at(COSMOS[src].system,t);double local=hypot(hypot(so.x-sun.x,so.y-sun.y),so.z-sun.z),dx=sun.x-x,dy=sun.y-y,dz=sun.z-z; + double cy=cos(h),sy=sin(h),cp=cos(pitch),sp=sin(pitch),cr=cos(roll),sr=sin(roll),forward=cy*dx+sy*dy,right=-sy*dx+cy*dy,up=dz; + double pf=cp*forward+sp*up,pu=-sp*forward+cp*up,rr=cr*right+sr*pu,uu=-sr*right+cr*pu;*dist=local+sqrt(dx*dx+dy*dy+dz*dz);*az=atan2(rr,pf);*el=atan2(uu,hypot(pf,rr));return;} double dx=so.x-x,dy=so.y-y,dz=so.z-1.6,horiz=hypot(dx,dy);*dist=hypot(horiz,dz);*az=atan2(dy,dx)-h;*el=atan2(dz,horiz); } -static void propagation(int src,double t,double *wall,double *cutoff,double *wind,double *rain){double u=t*CTRL;int i=(int)u,n=(int)(DUR*CTRL);if(i>=n)i=n-1;double f=u-i;Listener a=L[i],b=L[i+1];double x=a.x+(b.x-a.x)*f,y=a.y+(b.y-a.y)*f;Source so=source_at(src,t);double dx=so.x-x,dy=so.y-y,d=hypot(dx,dy);*wall=1;*cutoff=20000; +static void propagation(int src,double t,double *wall,double *cutoff,double *wind,double *rain){double u=t*CTRL;int i=(int)u,n=(int)(DUR*CTRL);if(i>=n)i=n-1;double f=u-i;Listener a=L[i],b=L[i+1];double x=a.x+(b.x-a.x)*f,y=a.y+(b.y-a.y)*f;Source so=source_at(src,t);double dx=so.x-x,dy=so.y-y,d=hypot(dx,dy);*wall=1;*cutoff=20000;if(duetMode||cosmosMode){*wind=0;*rain=1;return;} // 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)); - for(int j=0;jt*SR),nn=(long)(e->dur*SR);double ph=0,lp=0,hpLP=0,envLP=0;uint32_t rs=e->seed;double lastD=0;ACHrtf hs;memset(&hs,0,sizeof hs); - for(long k=0;ksrc,t,&az,&el,&d);propagation(e->src,t,&wall,&cutoff,&wind,&rain); - double v;if(e->type==0){double fullDop=(k?fmax(.985,fmin(1.015,1-(d-lastD)*SR/343)):1),dop=1+(fullDop-1)*spatialWet;double f=e->f0*pow(e->f1/e->f0,u/e->dur)*dop*(1+wind*spatialWet);ph+=TAU*f/SR;v=sin(ph);lastD=d;} + 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);} else {rs=rs*1664525u+1013904223u;double w=((rs>>8)/8388608.0)-1;double ca=exp(-TAU*e->f0/SR),cb=exp(-TAU*e->f1/SR);lp=(1-ca)*w+ca*lp;double hi=w-lp;hpLP=(1-cb)*hi+cb*hpLP;v=hpLP;} - if(cutoff<19000){double c=exp(-TAU*cutoff/SR);envLP=(1-c)*v+c*envLP;v=envLP;}v*=wall*rain*e->g*a*field_gain(e->src,t);float hl,hr;ac_hrtf_process(&hs,(float)v,az,el,d,&hl,&hr); - double dryPan=fmax(-.62,fmin(.62,S[e->src].x/6.0)),dl=sqrt((1-dryPan)*.5),dr=sqrt((1+dryPan)*.5); + if(cosmosMode&&e->type!=2&&e->type!=3){double modal=0;ACMeshAcoustics*ma=&BODY_ACOUSTICS[e->src];ACMaterial mm=BODY_MATERIAL[e->src];for(int m=0;m<3;m++){double mf=fmax(45,fmin(12000,ma->mode[m])),r=exp(-M_PI*mf*fmax(.004,mm.loss)/SR),y=2*r*cos(TAU*mf/SR)*mode1[m]-r*r*mode2[m]+(1-r)*v;mode2[m]=mode1[m];mode1[m]=y;modal+=y;}v=.68*v+.32*(modal/3.0);} + if(cutoff<19000){double c=exp(-TAU*cutoff/SR);envLP=(1-c)*v+c*envLP;v=envLP;}v*=wall*rain*e->g*clickGate*field_gain(e->src,t)*(cosmosMode?cosmos_plane_gain(e->src,t,az,el):1);float hl,hr;ac_hrtf_process(&hs,(float)v,az,el,spaceD,&hl,&hr); + double dryPan=fmax(-.62,fmin(.62,S[e->src].x/6.0)),range=(globeMode||cosmosMode)?.008+.992/(1+.32*spaceD*spaceD):1,dl=sqrt((1-dryPan)*.5)*range,dr=sqrt((1+dryPan)*.5)*range; double dg=cos(spatialWet*M_PI*.5),wg=sin(spatialWet*M_PI*.5),cl=v*dl*dg+hl*wg,cr=v*dr*dg+hr*wg; // Retarded reception: radio energy is written when it reaches the listener, // not when the emitter produced it. Distance therefore becomes real delay. - long at=s0+k+(long)(d*SR/343.0);if(at=0&&atsrc)*SR);if(echosrc)*SR);if(!duetMode&&!cosmosMode&&echo=0&&frsrc;meterL[mi]+=cl*cl;meterR[mi]+=cr*cr;}} } } // Cross-room early reflections. - int ds[]={3408,5424,8688};double dg[]={.075,.048,.03};for(int q=0;q<3;q++)for(long i=ds[q];ipk)pk=a;}double g=pk?.88/pk:1; for(long i=0;in-SR?((n-i)/(double)SR):1;busL[i]*=g*fo;busR[i]*=g*fo;} } @@ -179,14 +357,31 @@ static int wav(const char*p){FILE*f=fopen(p,"wb");if(!f)return 0;long n=(long)(D static void dot(unsigned char*p,int x,int y,int r,uint32_t c){for(int yy=-r;yy<=r;yy++)for(int xx=-r;xx<=r;xx++)if(xx*xx+yy*yy<=r*r){int X=x+xx,Y=y+yy;if(X>=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; +static double projectionRoll=0; static V3 sub3(V3 a,V3 b){return(V3){a.x-b.x,a.y-b.y,a.z-b.z};}static double dot3(V3 a,V3 b){return a.x*b.x+a.y*b.y+a.z*b.z;} static V3 cross3(V3 a,V3 b){return(V3){a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x};} static V3 norm3(V3 a){double n=sqrt(dot3(a,a));return n?(V3){a.x/n,a.y/n,a.z/n}:a;} -static P2 project(V3 p,V3 cam,V3 target){V3 f=norm3(sub3(target,cam)),r=norm3(cross3(f,(V3){0,0,1})),u=cross3(r,f),q=sub3(p,cam);double z=dot3(q,f);if(z<.15)return(P2){0,0,z,0};double focal=W*.694;return(P2){(int)(W*.5+dot3(q,r)*focal/z),(int)(H*.54-dot3(q,u)*focal/z),z,1};} -static void line2(unsigned char*p,int x0,int y0,int x1,int y1,uint32_t c,double alpha){int dx=abs(x1-x0),sx=x0=0&&x0=0&&y0>16,gg=(c>>8)&255,bb=c&255;p[o]=p[o]*(1-alpha)+rr*alpha;p[o+1]=p[o+1]*(1-alpha)+gg*alpha;p[o+2]=p[o+2]*(1-alpha)+bb*alpha;}if(x0==x1&&y0==y1)break;int e2=2*er;if(e2>=dy){er+=dy;x0+=sx;}if(e2<=dx){er+=dx;y0+=sy;}}} +static P2 project(V3 p,V3 cam,V3 target){V3 f=norm3(sub3(target,cam)),r=norm3(cross3(f,(V3){0,0,1})),u=cross3(r,f);if(projectionRoll){double c=cos(projectionRoll),s=sin(projectionRoll);V3 rr={r.x*c+u.x*s,r.y*c+u.y*s,r.z*c+u.z*s},uu={u.x*c-r.x*s,u.y*c-r.y*s,u.z*c-r.z*s};r=rr;u=uu;}V3 q=sub3(p,cam);double z=dot3(q,f);if(z<.15)return(P2){0,0,z,0};double focal=W*.694;return(P2){(int)(W*.5+dot3(q,r)*focal/z),(int)(H*.54-dot3(q,u)*focal/z),z,1};} +static void plot2(unsigned char*p,int x,int y,uint32_t c,double alpha){if(x<0||x>=W||y<0||y>=H)return;int o=(y*W+x)*3,rr=c>>16,gg=(c>>8)&255,bb=c&255;p[o]=p[o]*(1-alpha)+rr*alpha;p[o+1]=p[o+1]*(1-alpha)+gg*alpha;p[o+2]=p[o+2]*(1-alpha)+bb*alpha;} +// Native AC uses this exact Bresenham error walk. Stamp one 3x3 source cell at +// each step so the later 720 -> 240 nearest-neighbor pass preserves every step. +static void line2(unsigned char*p,int x0,int y0,int x1,int y1,uint32_t c,double alpha){int dx=abs(x1-x0),sx=x0=dy){er+=dy;x0+=sx;}if(e2<=dx){er+=dx;y0+=sy;}}} static void fill2(unsigned char*p,int x0,int y0,int x1,int y1,uint32_t c,double a){int rr=c>>16,gg=(c>>8)&255,bb=c&255;if(x0<0)x0=0;if(y0<0)y0=0;if(x1>W)x1=W;if(y1>H)y1=H;for(int y=y0;y=0&&v>=0&&w>=0){int o=(y*W+x)*3,rr=c>>16,gg=(c>>8)&255,bb=c&255;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;}}} +static void ellipse_points(unsigned char*p,int cx,int cy,int x,int y,uint32_t c,double a){for(int yy=-1;yy<=1;yy++)for(int xx=-1;xx<=1;xx++){plot2(p,cx+x+xx,cy+y+yy,c,a);plot2(p,cx-x+xx,cy+y+yy,c,a);plot2(p,cx+x+xx,cy-y+yy,c,a);plot2(p,cx-x+xx,cy-y+yy,c,a);}} +// Integer midpoint ellipse: stable, symmetric staircases without trig facets. +static void ellipse2(unsigned char*p,int cx,int cy,int rx,int ry,uint32_t c,double a){long x=0,y=ry,rx2=(long)rx*rx,ry2=(long)ry*ry,dx=0,dy=2*rx2*y,d1=ry2-rx2*ry+rx2/4;while(dx=0){ellipse_points(p,cx,cy,(int)x,(int)y,c,a);if(d2>0){y--;dy-=2*rx2;d2+=rx2-dy;}else{y--;x++;dx+=2*ry2;dy-=2*rx2;d2+=dx-dy+rx2;}}} +static const char*glyph3(char c){switch(c){case'A':return"010101111101101";case'B':return"110101110101110";case'C':return"011100100100011";case'D':return"110101101101110";case'E':return"111100110100111";case'F':return"111100110100100";case'G':return"011100101101011";case'H':return"101101111101101";case'I':return"111010010010111";case'J':return"001001001101010";case'K':return"101101110101101";case'L':return"100100100100111";case'M':return"101111111101101";case'N':return"101111111111101";case'O':return"010101101101010";case'P':return"110101110100100";case'Q':return"010101101111011";case'R':return"110101110101101";case'S':return"011100010001110";case'T':return"111010010010010";case'U':return"101101101101111";case'V':return"101101101101010";case'W':return"101101111111101";case'X':return"101101010101101";case'Y':return"101101010010010";case'Z':return"111001010100111";case'0':return"111101101101111";case'1':return"010110010010111";case'2':return"110001111100111";case'3':return"110001111001110";case'4':return"101101111001001";case'5':return"111100110001110";case'6':return"011100111101111";case'7':return"111001010010010";case'8':return"111101111101111";case'9':return"111101111001110";case'-':return"000000111000000";case'>':return"100010001010100";case'.':return"000000000000010";default:return"000000000000000";}} +static void text3(unsigned char*p,int x,int y,const char*s,int scale,uint32_t c){for(;*s;s++,x+=4*scale){const char*g=glyph3(*s>='a'&&*s<='z'?*s-32:*s);for(int yy=0;yy<5;yy++)for(int xx=0;xx<3;xx++)if(g[yy*3+xx]=='1')fill2(p,x+xx*scale,y+yy*scale,x+(xx+1)*scale,y+(yy+1)*scale,c,1);}} static void line3(unsigned char*p,V3 a,V3 b,V3 cam,V3 target,uint32_t c,double alpha){P2 x=project(a,cam,target),y=project(b,cam,target);if(x.ok&&y.ok)line2(p,x.x,x.y,y.x,y.y,c,alpha);} +static V3 globe_point(double x,double y,double altitude); +static uint32_t shade_color(uint32_t c,double k){int r=(c>>16)&255,g=(c>>8)&255,b=c&255;r=(int)fmin(255,r*k);g=(int)fmin(255,g*k);b=(int)fmin(255,b*k);return(uint32_t)(r<<16|g<<8|b);} +static void flat_tri3(unsigned char*p,V3 a,V3 b,V3 c,V3 cam,V3 target,uint32_t color,double alpha){V3 n=cross3(sub3(b,a),sub3(c,a));if(dot3(n,sub3(cam,a))<=0)return;P2 pa=project(a,cam,target),pb=project(b,cam,target),pc=project(c,cam,target);if(pa.ok&&pb.ok&&pc.ok)tri2(p,pa.x,pa.y,pb.x,pb.y,pc.x,pc.y,color,alpha);} +static void draw_prompt_rock_alpha(unsigned char*p,const ACRock*r,V3 center,double scale,double spin,V3 cam,V3 target,uint32_t color,double alpha){V3 vv[42];double c=cos(spin),s=sin(spin),ct=cos(spin*.63),st=sin(spin*.63);for(int i=0;inv;i++){double x=r->v[i].x*c-r->v[i].y*s,y=r->v[i].x*s+r->v[i].y*c,z=r->v[i].z,yy=y*ct-z*st,zz=y*st+z*ct;vv[i]=(V3){center.x+x*scale,center.y+yy*scale,center.z+zz*scale};}V3 light=norm3((V3){-.35,-.55,.76});for(int i=0;inf;i++){ACRockFace f=r->f[i];V3 n=norm3(cross3(sub3(vv[f.b],vv[f.a]),sub3(vv[f.c],vv[f.a])));if(dot3(n,sub3(cam,vv[f.a]))<=0)continue;double nd=dot3(n,light),lit=.18+.72*fmax(0,nd)+.18*fmax(0,-nd);uint32_t facet=shade_color(color,lit);flat_tri3(p,vv[f.a],vv[f.b],vv[f.c],cam,target,facet,.88*alpha);}} +static void draw_prompt_rock(unsigned char*p,const ACRock*r,V3 center,double scale,double spin,V3 cam,V3 target,uint32_t color){draw_prompt_rock_alpha(p,r,center,scale,spin,cam,target,color,1);} +static void draw_prompt_rock_facing(unsigned char*p,const ACRock*r,V3 center,V3 receiver,double scale,double phase,V3 cam,V3 target,uint32_t color){V3 f=norm3(sub3(receiver,center)),up=fabs(f.z)>.94?(V3){0,1,0}:(V3){0,0,1},right=norm3(cross3(up,f)),vert=cross3(f,right);double rockx=.16*sin(phase*.37),rocky=.12*cos(phase*.29),push=1+.12*sin(phase),cx=cos(rockx),sx=sin(rockx),cy=cos(rocky),sy=sin(rocky);V3 vv[42];for(int i=0;inv;i++){double x=r->v[i].x,y=r->v[i].y,z=r->v[i].z*push,yy=y*cx-z*sx,zz=y*sx+z*cx,xx=x*cy+zz*sy;zz=-x*sy+zz*cy;vv[i]=(V3){center.x+scale*(right.x*xx+vert.x*yy+f.x*zz),center.y+scale*(right.y*xx+vert.y*yy+f.y*zz),center.z+scale*(right.z*xx+vert.z*yy+f.z*zz)};}V3 light=norm3((V3){-.35,-.55,.76});for(int i=0;inf;i++){ACRockFace q=r->f[i];V3 n=norm3(cross3(sub3(vv[q.b],vv[q.a]),sub3(vv[q.c],vv[q.a])));if(dot3(n,sub3(cam,vv[q.a]))<=0)continue;double lit=.18+.72*fmax(0,dot3(n,light))+.18*fmax(0,-dot3(n,light));flat_tri3(p,vv[q.a],vv[q.b],vv[q.c],cam,target,shade_color(color,lit),.9);}} +static V3 pyramid_shard_point(ACRockV v,ACRockV center,Source hit,double age,int index,double scale){double n=sqrt(center.x*center.x+center.y*center.y+center.z*center.z)+1e-6,speed=1.7+fmod(index*2.371,3.6),omega=5.2+fmod(index*.731,3.1),travel=speed/omega*sin(omega*age)*exp(-age*.72),spin=age*(2.4+fmod(index*1.173,5.2)),c=cos(spin),s=sin(spin),ct=cos(spin*.71),st=sin(spin*.71);double x=(v.x-center.x)*c-(v.y-center.y)*s,y=(v.x-center.x)*s+(v.y-center.y)*c,z=v.z-center.z,yy=y*ct-z*st,zz=y*st+z*ct;double px=hit.x+center.x*scale+center.x/n*travel+x*scale,py=hit.y+center.y*scale+center.y/n*travel+yy*scale,pz=hit.z+center.z*scale+center.z/n*travel+zz*scale-.18*age*age;return globe_point(px,py,pz);} +static V3 capture_shard(V3 v,Source body,double age,int index,int corner){double u=fmax(0,fmin(1,(age-.65)/3.2));u=u*u*(3-2*u);double a=age*(.75+.07*(index%7))+index*2.399,r=.34+.045*(index%5),zoff=(corner-1.5)*.075;V3 dst=globe_point(body.x+cos(a)*r,body.y+sin(a)*r,body.z+zoff+.10*sin(a*1.7));return(V3){v.x+(dst.x-v.x)*u,v.y+(dst.y-v.y)*u,v.z+(dst.z-v.z)*u};} static void circle3(unsigned char*p,V3 c,double r,V3 cam,V3 target,uint32_t color,double alpha){V3 a={c.x+r,c.y,c.z};for(int i=1;i<=64;i++){double q=TAU*i/64;V3 b={c.x+cos(q)*r,c.y+sin(q)*r,c.z};line3(p,a,b,cam,target,color,alpha);a=b;}} static void marble(unsigned char*p,int cx,int cy,int r,uint32_t c,double energy){ int cr=c>>16,cg=(c>>8)&255,cb=c&255;for(int y=-r;y<=r;y++)for(int x=-r;x<=r;x++){double nx=x/(double)r,ny=y/(double)r,r2=nx*nx+ny*ny;if(r2>1)continue;double nz=sqrt(1-r2),lx=-.42,ly=-.55,lz=.72,nd=fmax(0,nx*lx+ny*ly+nz*lz),rim=pow(1-nz,2.2),hx=nx+.26,hy=ny+.34,hl=hypot(hx,hy),spec=pow(fmax(0,1-hl*2.4),18);double shade=.18+.66*nd+.34*rim+1.15*spec+fmin(.45,energy*10);int X=cx+x,Y=cy+y;if(X<0||X>=W||Y<0||Y>=H)continue;int o=(Y*W+X)*3;p[o]=(unsigned char)fmin(255,cr*shade+spec*110);p[o+1]=(unsigned char)fmin(255,cg*shade+spec*110);p[o+2]=(unsigned char)fmin(255,cb*shade+spec*110);}} @@ -195,6 +390,21 @@ static void source_meter(int fr,int src,double *l,double *r){ 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; } +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; + fill2(p,14,18,452,510,panel,.86);fill2(p,462,18,706,510,panel,.91);text3(p,26,30,"ACOUSTIC WORLD",2,ink);text3(p,474,30,"SIGNAL CHAIN",2,ink); + for(int g=-12;g<=12;g+=4){P2 a=chart_point(g,-17),b=chart_point(g,17);line2(p,a.x,a.y,b.x,b.y,sub,.12);a=chart_point(-17,g);b=chart_point(17,g);line2(p,a.x,a.y,b.x,b.y,sub,.12);} + int focus=tour_source(t);P2 ship=chart_point(l.x,l.y);glow(p,ship.x,ship.y,18,0xffd54f,.25);tri2(p,ship.x,ship.y-9,ship.x-7,ship.y+7,ship.x+7,ship.y+7,0xffd54f,.95);text3(p,ship.x+10,ship.y-3,"L",2,ink); + for(int k=0;k<2;k++){Source sun=system_sun_at(k,t);P2 sp=chart_point(sun.x,sun.y);ellipse2(p,sp.x,sp.y,(int)(COSMOS[k?4:0].orbit*13),(int)(COSMOS[k?4:0].orbit*8),SYSTEMS[k].color,.22);glow(p,sp.x,sp.y,16,SYSTEMS[k].color,.28);dot(p,sp.x,sp.y,7,SYSTEMS[k].color);line2(p,sp.x,sp.y,ship.x,ship.y,SYSTEMS[k].color,.22);} + for(int s=0;s=10?(i==10?0x4ecdc4:0xf8a5c2):S[focus].color,.84);snprintf(num,sizeof num,"%d",(int)lrint(vals[i]));text3(p,bx,y+13,num,2,ink);} + text3(p,28,526,"12 VOICES OSC > MESH > MOTION > SUN > SPACE > HRTF > L R",2,ink); + int base=574,rowh=10;for(int s=0;s0?1.0/peak:0;int gap=radius+2; @@ -202,18 +412,130 @@ static void stereo_lobes(unsigned char*p,P2 sp,double l,double r,int radius,uint glow(p,sp.x-gap,sp.y,rl*2,c,.12+fmin(.55,l*18));glow(p,sp.x+gap,sp.y,rr*2,c,.12+fmin(.55,r*18)); dot(p,sp.x-gap,sp.y,rl,c);dot(p,sp.x+gap,sp.y,rr,c); } -static void video(const char*wavp,const char*outp){char cmd[2048];size_t ol=strlen(outp);int lossless=ol>=4&&!strcmp(outp+ol-4,".mov");if(lossless) - snprintf(cmd,sizeof cmd,"ffmpeg -hide_banner -y -loglevel error -f image2pipe -vcodec ppm -r %d -i - -i '%s' -c:v libx265 -preset medium -x265-params lossless=1:log-level=error -pix_fmt yuv444p -tag:v hvc1 -c:a pcm_s24le -ar 48000 -shortest '%s'",FPS,wavp,outp); - else snprintf(cmd,sizeof cmd,"ffmpeg -hide_banner -y -loglevel error -f image2pipe -vcodec ppm -r %d -i - -i '%s' -c:v libx264 -pix_fmt yuv420p -crf 17 -c:a aac -b:a 320k -shortest '%s'",FPS,wavp,outp);FILE*ff=popen(cmd,"w");if(!ff)return;unsigned char*p=malloc(W*H*3); - for(int fr=0;fr<(int)(DUR*FPS);fr++){double t=fr/(double)FPS;for(int y=0;y>16)&255)/255.0,((c>>8)&255)/255.0,(c&255)/255.0),.16+.12*S[s].mass,(5.5+fmin(18,energy*130))*arrival,.18};} + Listener l=L[(int)(fmin(DUR,t)*CTRL)];V3 lv=globe_point(l.x,l.y,1.6);bodies[NSRC]=(PTBody){ptv(lv.x,lv.y,lv.z),ptv(1,.57,.16),.24,2.8,.3}; + // Matte objects catch and reveal indirect colored light inside the glass. + bodies[NSRC+1]=(PTBody){ptv(-2.7,2.1,-2.8),ptv(.12,.38,.31),1.15,0,.88}; + bodies[NSRC+2]=(PTBody){ptv(2.9,1.5,-2.25),ptv(.42,.12,.34),.82,0,.72}; + bodies[NSRC+3]=(PTBody){ptv(.3,-3.1,-2.65),ptv(.12,.2,.48),1.0,0,.82}; + int w=720,h=720;unsigned char*rgb=malloc(w*h*3);double spin=-.78+.12*sin(t*.018),dist=17;PTVec cam=ptv(dist*cos(spin),dist*sin(spin),10.2);pt_render_rgb(rgb,w,h,cam,ptv(0,0,0),bodies,NSRC+4,spp,(int)(t*FPS),t); + FILE*f=fopen(out,"wb");if(!f){free(rgb);return 0;}fprintf(f,"P6\n%d %d\n255\n",w,h);fwrite(rgb,1,w*h*3,f);fclose(f);free(rgb);return 1; +} +static int export_scene(const char*out,int frames){ + FILE*f=fopen(out,"wb");if(!f)return 0;uint32_t magic=0x53434e45,version=2,count=NSRC,fps=FPS;fwrite(&magic,4,1,f);fwrite(&version,4,1,f);fwrite(&frames,4,1,f);fwrite(&fps,4,1,f);fwrite(&count,4,1,f); + for(int fr=0;fr0){double u=fmax(0,fmin(1,(t-E[j].t)/E[j].dur)),freq=E[j].f0*pow(E[j].f1/E[j].f0,u);pitchSum+=freq*a;pitchWeight+=a;}}float row[6]={(float)so.x,(float)so.y,(float)so.z,(float)ml,(float)mr,(float)(pitchWeight?pitchSum/pitchWeight:0)};fwrite(row,4,6,f);}} + fclose(f);return 1; +} +static void glass_shell(unsigned char*p,V3 cam,V3 target,double t){ + P2 c=project((V3){0,0,0},cam,target);if(!c.ok||c.z<7)return; + double focal=W*.694,rp=focal*6.8/sqrt(c.z*c.z-6.8*6.8);int r=(int)rp; + for(int yy=-r;yy<=r;yy++)for(int xx=-r;xx<=r;xx++){double nx=xx/rp,ny=yy/rp,d2=nx*nx+ny*ny;if(d2>=1)continue;int X=c.x+xx,Y=c.y+yy;if(X<0||X>=W||Y<0||Y>=H)continue; + double nz=sqrt(1-d2),fres=pow(1-nz,4),glint=pow(fmax(0,1-hypot(nx+.36,ny+.42)*1.85),22),a=.018+.19*fres+.52*glint; + double caustic=.5+.5*sin(nx*19+ny*13+t*.22),rr=66+35*glint,gg=112+55*glint+10*caustic,bb=124+72*glint+12*caustic;int o=(Y*W+X)*3; + p[o]=(unsigned char)(p[o]*(1-a)+rr*a);p[o+1]=(unsigned char)(p[o+1]*(1-a)+gg*a);p[o+2]=(unsigned char)(p[o+2]*(1-a)+bb*a); + } + ellipse2(p,c.x,c.y,r,r,0x9bd5d5,.48); +} +static void globe_frame(unsigned char*p,int fr,double t,Listener l){ + if(acousticsView){acoustics_frame(p,fr,t,l);return;} + int focus=tour_source(t); + // Listener-mounted isometric rig: translation follows the receiver while the + // viewing basis remains stable, preventing the constellation from drifting + // offscreen without introducing camera tumble. + V3 lv=globe_point(l.x,l.y,cosmosMode?l.z:1.6);projectionRoll=(cosmosMode&&cameraMode)?l.roll:0; + V3 centroid={0,0,0};int visible=0;for(int s=0;s14?0x6b294f:0x2b6671);fill2(p,0,(int)(H*.57)+q,W-1,(int)(H*.57)+q,c,a);} + for(int k=0;k<2;k++){Source sun=system_sun_at(k,t);V3 sv=globe_point(sun.x,sun.y,sun.z);P2 sp=project(sv,cam,target);if(sp.ok)glow(p,sp.x,sp.y,210,SYSTEMS[k].color,.075);} + } + // True background volume: deterministic world-space stars share the scene + // camera, so depth, scale, occlusion ordering, and parallax are physical. + for(int q=0;q<240;q++){ + double sx=-24+fmod(q*17.731+3.7,48),sy=-24+fmod(q*29.417+11.2,48),sz=-10+fmod(q*13.137+5.9,27); + P2 star=project((V3){sx,sy,sz},cam,target);if(!star.ok||star.z<4||star.z>48)continue; + int r=star.z<14?2:1;uint32_t c=q%11==0?0xb7e8e4:(q%7==0?0x9c8fc4:0x536f78); + if(star.z<12)glow(p,star.x,star.y,7,c,.08);dot(p,star.x,star.y,r,c); + } + uint32_t major=brightMode?0x688f91:0x35666b,minor=brightMode?0xa4b9b4:0x25464b; + // Empty-space constellation: no floor, globe, grid, or explicit boundary. + // Listener history and inherited motion trails are the only spatial scaffold. + int li=(int)(t*CTRL);for(int q=1;q<=120;q++){int ia=li-(q-1)*CTRL/8,ib=li-q*CTRL/8;if(ib<0)break;V3 a=globe_point(L[ia].x,L[ia].y,1.48),b=globe_point(L[ib].x,L[ib].y,1.48);line3(p,a,b,cam,target,0xff6b9d,.34*(1-q/121.0));} + if(cosmosMode)for(int k=0;k<2;k++){Source sun=system_sun_at(k,t);V3 sv=globe_point(sun.x,sun.y,sun.z);P2 sp=project(sv,cam,target);line3(p,sv,lv,cam,target,SYSTEMS[k].color,.31);if(sp.ok)glow(p,sp.x,sp.y,38,SYSTEMS[k].color,.38);draw_prompt_rock(p,&BODY_MESH[NSRC+k],sv,.78,.22*k,cam,target,SYSTEMS[k].color);} + for(int s=0;s.00005?S[s].color:0x667176,.06+fmin(.62,heard*19)); + // Adaptive shutter trail. At launch, dense sampling over a short exposure + // resolves the 520 RPM orbit instead of drawing polygonal chords. As angular + // velocity settles, exposure grows into the full phosphor history. + double settle=1-exp(-t/6.8),trailSecs=.75+7.25*settle;int trailSteps=320; + V3 prev=sv;for(int q=1;q<=trailSteps;q++){double pt=fmax(0,t-q*trailSecs/trailSteps),fade=(1-q/(trailSteps+1.0))*(.018+fmin(.22,heard*9));V3 ov=globe_body(s,pt);line3(p,prev,ov,cam,target,S[s].color,fade);prev=ov;} + if(cosmosMode){ACRock*mesh=&BODY_MESH[s];for(int q=1;q<=72;q++){double pt=fmax(0,t-q*4.8/72),fade=(1-q/73.0)*(.10+fmin(.24,heard*7)),ang=pt*(COSMOS[s].rate+COSMOS[s].phase),ca=cos(ang),sa=sin(ang);V3 center=globe_body(s,pt);for(int layer=0;layer<3;layer++){int vi=(q*7+layer*13+s*17)%mesh->nv;ACRockV mv=mesh->v[vi];double shell=(.19+.055*S[s].mass)*(1+layer*.17),vx=(mv.x*ca-mv.y*sa)*shell,vy=(mv.x*sa+mv.y*ca)*shell,vz=mv.z*shell;V3 voxel={center.x+vx,center.y+vy,center.z+vz};P2 vp=project(voxel,cam,target);if(!vp.ok)continue;int block=vp.z<8?3:(vp.z<16?2:1);uint32_t vc=shade_color(S[s].color,.62+.28*layer);fill2(p,vp.x-block,vp.y-block,vp.x+block,vp.y+block,vc,fade*(.62+.16*layer));if(layer==2&&q%12==0)glow(p,vp.x,vp.y,block*3,vc,fade*.16);}}} + if(sp.ok){double focusScale=s==focus?1.0+2.4*push:1.0;int core=(int)fmax(3,fmin(18,(82/sp.z)*focusScale)),halo=(int)fmax(core*2,fmin(48,(5+heard*620)*focusScale));if(s==focus)glow(p,sp.x,sp.y,halo+8,0xffffff,.05+.08*push);glow(p,sp.x,sp.y,halo,S[s].color,.06+fmin(.25,heard*7));if(!cosmosMode){int gap=core+2,rl=(int)fmax(2,core*(.45+.55*sqrt(ml/(fmax(ml,mr)+1e-9)))),rr=(int)fmax(2,core*(.45+.55*sqrt(mr/(fmax(ml,mr)+1e-9))));dot(p,sp.x-gap,sp.y,rl,S[s].color);dot(p,sp.x+gap,sp.y,rr,S[s].color);marble(p,sp.x,sp.y,core+1,S[s].color,heard);}} + if(cosmosMode){double phase=TAU*(SYSTEMS[COSMOS[s].system].tempo/60.0)*t*COSMOS[s].rate+COSMOS[s].phase*TAU;draw_prompt_rock_facing(p,&BODY_MESH[s],sv,lv,.29+.060*S[s].mass,phase,cam,target,S[s].color);} + } + // Collision-blast particles: impact energy excites the audio event above and + // the same event seed launches deterministic ballistic luminous fragments. + if(cosmosMode)for(int j=0;jt;if(age< -10||age>10.0)continue;Source hit=source_at(e->src,e->t);uint64_t seed=((uint64_t)(e->t*1000003.0)<<17)^(uint64_t)(e->src+1)*UINT64_C(0x9e3779b97f4a7c15);ACRock rock;ac_rock_generate(seed,&rock); + if(age<0){Source sun=system_sun_at(COSMOS[e->src].system,t);double targetAng=atan2(hit.y-sun.y,hit.x-sun.x),u; + V3 comet;if(age< -4){u=(age+10)/6;double r=15+(4.4-15)*u,a=targetAng-.72*(1-u);comet=globe_point(sun.x+cos(a)*r,sun.y+sin(a)*r,sun.z+6.0*(1-u)+1.1*sin(u*M_PI));} + else{u=(age+4)/4;double r=4.4+(hypot(hit.x-sun.x,hit.y-sun.y)-4.4)*u,a=targetAng-TAU*3*(1-u);comet=globe_point(sun.x+cos(a)*r,sun.y+sin(a)*r,sun.z+(hit.z-sun.z)*u+.8*sin(u*TAU));} + P2 cp=project(comet,cam,target);if(cp.ok)glow(p,cp.x,cp.y,22,S[e->src].color,.22);draw_prompt_rock(p,&rock,comet,.72+.18*rock.roughness,t*2.1,cam,target,S[e->src].color);continue;} + double shardScale=.72+.18*rock.roughness;for(int q=0;qsrc+1+q%5)%NSRC,t);V3 va=capture_shard(pyramid_shard_point(a,center,hit,age,q,shardScale),catcher,age,q,0),vb=capture_shard(pyramid_shard_point(b,center,hit,age,q,shardScale),catcher,age,q,1),vc=capture_shard(pyramid_shard_point(c,center,hit,age,q,shardScale),catcher,age,q,2),vd=capture_shard(pyramid_shard_point(apex,center,hit,age,q,shardScale),catcher,age,q,3);P2 pa=project(va,cam,target),pb=project(vb,cam,target),pc=project(vc,cam,target),pd=project(vd,cam,target);if(pa.ok&&pb.ok&&pc.ok&&pd.ok){double fade=.16+.42*fmax(0,1-age/10);uint32_t col=S[e->src].color;tri2(p,pa.x,pa.y,pb.x,pb.y,pc.x,pc.y,shade_color(col,.62),fade);tri2(p,pa.x,pa.y,pb.x,pb.y,pd.x,pd.y,shade_color(col,.88),fade);tri2(p,pb.x,pb.y,pc.x,pc.y,pd.x,pd.y,shade_color(col,1.12),fade);tri2(p,pc.x,pc.y,pa.x,pa.y,pd.x,pd.y,shade_color(col,.74),fade);}} + } + if(cosmosMode)for(int j=0;jt;if(age<0||age>.32)continue;Source hit=source_at(e->src,e->t);ACRock*rock=&BODY_MESH[e->src];for(int q=0;q<6;q++){int fi=(int)(((uint64_t)q*11+e->seed)%(uint64_t)rock->nf);ACRockFace f=rock->f[fi];ACRockV a=rock->v[f.a],b=rock->v[f.b],c=rock->v[f.c],center={(a.x+b.x+c.x)/3,(a.y+b.y+c.y)/3,(a.z+b.z+c.z)/3},apex={center.x*.3,center.y*.3,center.z*.3};V3 va=pyramid_shard_point(a,center,hit,age,q,.24),vb=pyramid_shard_point(b,center,hit,age,q,.24),vc=pyramid_shard_point(c,center,hit,age,q,.24),vd=pyramid_shard_point(apex,center,hit,age,q,.24);P2 pa=project(va,cam,target),pb=project(vb,cam,target),pc=project(vc,cam,target),pd=project(vd,cam,target);if(pa.ok&&pb.ok&&pc.ok&&pd.ok){double fade=.62*(1-age/.32);uint32_t col=S[e->src].color;tri2(p,pa.x,pa.y,pb.x,pb.y,pc.x,pc.y,shade_color(col,.7),fade);tri2(p,pa.x,pa.y,pb.x,pb.y,pd.x,pd.y,shade_color(col,1.08),fade);tri2(p,pb.x,pb.y,pc.x,pc.y,pd.x,pd.y,shade_color(col,.86),fade);tri2(p,pc.x,pc.y,pa.x,pa.y,pd.x,pd.y,shade_color(col,.58),fade);}}} + P2 lp=project(lv,cam,target);if(lp.ok){glow(p,lp.x,lp.y,48,0xffffff,.15);glow(p,lp.x,lp.y,34,0x63cdda,.22);glow(p,lp.x,lp.y,20,0xffd54f,.31);}if(cosmosMode&&!cameraMode){draw_prompt_rock(p,&BODY_MESH[NSRC+2],lv,.62,t*.4,cam,target,0xdffcff);draw_prompt_rock(p,&BODY_MESH[NSRC+2],lv,.55,-t*.31,cam,target,0xffd54f);}else if(lp.ok)dot(p,lp.x,lp.y,8,0xffffff); + // Listener-relative 2D navigation radar. Distant stars never enter this local + // map; only suns, speaker bodies, and imminent collision contacts appear. + if(cosmosMode&&cameraMode){int mx=W-176,my=22,mw=154,cx=mx+mw/2,cy=my+mw/2;fill2(p,mx,my,mx+mw,my+mw,brightMode?0xe7eeeb:0x0c171b,.72);ellipse2(p,cx,cy,68,68,brightMode?0x536e73:0x75aeb2,.72);ellipse2(p,cx,cy,34,34,brightMode?0x82999c:0x426f76,.48);line2(p,cx-68,cy,cx+68,cy,0x688f91,.34);line2(p,cx,cy-68,cx,cy+68,0x688f91,.34); + for(int k=0;k<2;k++){Source o=system_sun_at(k,t);double dx=o.x-l.x,dy=o.y-l.y,rx=dx*cos(l.heading)+dy*sin(l.heading),ry=-dx*sin(l.heading)+dy*cos(l.heading),d=hypot(rx,ry),sc=5.2;if(d*sc>65){rx*=65/(d*sc);ry*=65/(d*sc);}int x=cx+(int)(ry*sc),y=cy-(int)(rx*sc),r=6;tri2(p,x,y-r,x-r,y,x,y+r,SYSTEMS[k].color,.95);tri2(p,x,y-r,x,y+r,x+r,y,SYSTEMS[k].color,.95);} + for(int s=0;s65){rx*=65/(d*sc);ry*=65/(d*sc);}int x=cx+(int)(ry*sc),y=cy-(int)(rx*sc),r=3;fill2(p,x-r,y-r,x+r+1,y+r+1,S[s].color,.92);} + tri2(p,cx,cy-8,cx-6,cy+6,cx+6,cy+6,0xffd54f,.95);} + // Score horizon: movement spans, present playhead, and the next change arriving. + {static const int bars[]={0,4,12,20,28,34,36,38};static const uint32_t colors[]={0x536069,0x4ecdc4,0x778beb,0xf6c915,0xf8a5c2,0xe0a464,0xff6b6b}; + int x0=54,x1=W-54,y=H-34,span=x1-x0;fill2(p,x0,y-2,x1,y+3,0x263438,.82); + for(int m=0;m<7;m++){int a=x0+(int)(span*bars[m]/38.0),b=x0+(int)(span*bars[m+1]/38.0);fill2(p,a,y-3,b,y+4,colors[m],m==0?.34:.56);line2(p,a,y-9,a,y+10,colors[m],.85);} + double bar=t/BAR;int px=x0+(int)(span*fmin(1,bar/38.0));line2(p,px,y-14,px,y+14,0xffffff,.95);dot(p,px,y,4,0xffffff); + for(int m=1;m<8;m++)if(bars[m]>bar){double away=bars[m]-bar;if(away<2.0){int nx=x0+(int)(span*bars[m]/38.0),r=5+(int)(8*(1-away/2.0));glow(p,nx,y,r,colors[m<7?m:6],.18+.32*(1-away/2.0));}break;} + } +} +static void video(const char*wavp,const char*outp){char cmd[2048];size_t ol=strlen(outp);int lossless=ol>=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 image2pipe -vcodec ppm -r %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'",FPS,wavp,clean,outp); + else snprintf(cmd,sizeof cmd,"ffmpeg -hide_banner -y -loglevel error -f image2pipe -vcodec ppm -r %d -i - -i '%s' %s -c:v libx264 -preset slow -pix_fmt yuv420p -crf 14 %s -c:a aac -b:a 320k -shortest '%s'",FPS,wavp,pixels,clean,outp);FILE*ff=popen(cmd,"w");if(!ff)return;unsigned char*p=malloc(W*H*3); + for(int fr=0;fr<(int)(DUR*FPS);fr++){double t=fr/(double)FPS;for(int y=0;y=(long)(DUR*SR))k=(long)(DUR*SR)-1;int yl=wy-(int)(busL[k]*42),yr=wy-(int)(busR[k]*42);line2(p,lx0+q-1,lastL,lx0+q,yl,0x4ecdc4,.9);line2(p,rx0+q-1,lastR,rx0+q,yr,0xf8a5c2,.9);lastL=yl;lastR=yr;}} // Receiver triangle points forward. Contact brightness is measured signal, // weak transmissions flicker unresolved instead of being presented as fact. 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); @@ -248,7 +573,8 @@ static void video(const char*wavp,const char*outp){char cmd[2048];size_t ol=strl } free(p);pclose(ff); } -int main(int argc,char**argv){const char*w="../out/spatial-sineabye.wav",*v="../out/spatial-sineabye.mp4",*mp3=NULL;for(int i=1;i +#include +#include +#include +#include +#define TAU 6.28318530717958647692 +#define PT_NO_SHELL 1 +#include "ac_terrarium_pt.h" + +__global__ static void render_kernel(unsigned char*out,int w,int h,PTVec cam,PTVec target,const PTBody*b,int count,int spp,int frame,double time){ + int x=blockIdx.x*blockDim.x+threadIdx.x,y=blockIdx.y*blockDim.y+threadIdx.y;if(x>=w||y>=h)return; + PTVec f=pt_norm(pt_sub(target,cam)),right=pt_norm(pt_cross(f,ptv(0,0,1))),up=pt_cross(right,f),sum=ptv(0,0,0);double aspect=w/(double)h,tanHalf=.72; + for(int s=0;s1)u=1;u=u*u*(3-2*u);if(u<1){double outer=9.2+.15*i,k=outer/fmax(.1,d);x=x*u+x*k*(1-u);y=y*u+y*k*(1-u);z=z*u+z*k*(1-u);}uint32_t c=colors[i];host[i]=(PTBody){ptv(x,y,z),ptv(((c>>16)&255)/255.0,((c>>8)&255)/255.0,(c&255)/255.0),.22+(i%3)*.08,(7.0+(i%5)*2.5)*u,.18};} + host[12]=(PTBody){ptv(.45*sin(time*.31),.35*cos(time*.27),.2),ptv(1,.57,.16),.24,4,.3};host[13]=(PTBody){ptv(-2.7,2.1,-2.8),ptv(.12,.38,.31),1.15,0,.88};host[14]=(PTBody){ptv(2.9,1.5,-2.25),ptv(.42,.12,.34),.82,0,.72};host[15]=(PTBody){ptv(.3,-3.1,-2.65),ptv(.12,.2,.48),1,0,.82}; +} +int main(int argc,char**argv){int video=argc>1&&!strcmp(argv[1],"--video"),spp=video?(argc>3?atoi(argv[3]):16):(argc>1?atoi(argv[1]):128),frames=video?(argc>4?atoi(argv[4]):360):1,w=720,h=720,duetVisual=video&&argc>6&&!strcmp(argv[6],"duet");const char*out=video?(argc>2?argv[2]:"terrarium-cuda.mp4"):(argc>2?argv[2]:"terrarium-cuda.ppm"),*scenePath=video&&argc>5?argv[5]:NULL;PTBody host[16]={0};float*sceneData=NULL;int sceneFrames=0,sceneStride=64,rowStride=5; + if(scenePath){FILE*sf=fopen(scenePath,"rb");uint32_t magic,ver,count,fps;fread(&magic,4,1,sf);fread(&ver,4,1,sf);fread(&sceneFrames,4,1,sf);fread(&fps,4,1,sf);fread(&count,4,1,sf);if(magic!=0x53434e45||count!=12){fprintf(stderr,"bad scene data\n");return 1;}rowStride=ver>=2?6:5;sceneStride=4+12*rowStride;sceneData=(float*)malloc((size_t)sceneFrames*sceneStride*sizeof(float));fread(sceneData,sizeof(float),(size_t)sceneFrames*sceneStride,sf);fclose(sf);if(frames>sceneFrames)frames=sceneFrames;} + unsigned char*rgb;PTBody*b;ck(cudaMallocManaged(&rgb,w*h*3),"pixels");ck(cudaMallocManaged(&b,sizeof(host)),"bodies");dim3 block(16,16),grid((w+15)/16,(h+15)/16);cudaEvent_t start,stop;ck(cudaEventCreate(&start),"event start");ck(cudaEventCreate(&stop),"event stop");FILE*f=NULL;if(video){char cmd[1024];snprintf(cmd,sizeof cmd,"ffmpeg -hide_banner -y -loglevel error -f image2pipe -vcodec ppm -r 24 -i - -c:v h264_nvenc -preset p7 -tune hq -rc vbr -cq 16 -b:v 0 -pix_fmt yuv420p '%s'",out);f=popen(cmd,"w");}else f=fopen(out,"wb");double total=0; + for(int frame=0;frame5?r[5]:440,pulse=heard/(heard+.002);host[s].center=pt_add(listener,pt_mul(offset,1-.38*pulse));if(pitch>0)host[s].color=pitch_color(pitch);host[s].emission=pitch>0?7.5:.42;double low=pitch>0?1-fmax(0.0,fmin(1.0,log2(pitch/45.0)/7.5)):0;host[s].radius=.14+.38*low;host[s].roughness=.12+.7*(1-low);}} + host[12].center=listener;host[12].emission=3.2;host[13]=(PTBody){ptv(listener.x-5,listener.y+3,-3.4),ptv(.08,.32,.25),2.2,0,.9};host[14]=(PTBody){ptv(listener.x+5,listener.y+2,-3.0),ptv(.36,.1,.31),1.7,0,.82};host[15]=(PTBody){ptv(listener.x,listener.y,-101.8),ptv(.055,.105,.12),100,0,.96};memcpy(b,host,sizeof(host));double drift=.08*sin(time*.055);PTVec cam=ptv(listener.x+11.8*cos(-.78+drift),listener.y+11.8*sin(-.78+drift),listener.z+11.5),target=ptv(listener.x,listener.y,0);ck(cudaEventRecord(start),"record start");render_kernel<<>>(rgb,w,h,cam,target,b,16,spp,frame,time);ck(cudaGetLastError(),"launch");ck(cudaEventRecord(stop),"record stop");ck(cudaEventSynchronize(stop),"kernel sync");float ms;ck(cudaEventElapsedTime(&ms,start,stop),"elapsed");total+=ms;fprintf(f,"P6\n%d %d\n255\n",w,h);fwrite(rgb,1,w*h*3,f);if(video&&frame%24==0)fprintf(stderr,"frame %d/%d · %.1f ms\n",frame,frames,ms);} + if(video)pclose(f);else fclose(f);fprintf(stderr,"720x720 %d spp · %d frames · %.2f ms/frame GPU -> %s\n",spp,frames,total/frames,out);free(sceneData);cudaFree(rgb);cudaFree(b);} diff --git a/pop/nullabye/gui/Package.swift b/pop/nullabye/gui/Package.swift new file mode 100644 index 0000000000..1a4dbbdc5b --- /dev/null +++ b/pop/nullabye/gui/Package.swift @@ -0,0 +1,9 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "SineabyeLive", + platforms: [.macOS(.v14)], + products: [.executable(name: "sineabye-live", targets: ["SineabyeLive"])], + targets: [.executableTarget(name: "SineabyeLive")] +) diff --git a/pop/nullabye/gui/Sources/SineabyeLive/main.swift b/pop/nullabye/gui/Sources/SineabyeLive/main.swift new file mode 100644 index 0000000000..6077c0e938 --- /dev/null +++ b/pop/nullabye/gui/Sources/SineabyeLive/main.swift @@ -0,0 +1,92 @@ +import SwiftUI +import AVFoundation + +struct BodyFrame { var x, y, z, left, right, pitch: Float } +struct ScoreFrame { var time, lx, ly, lz: Float; var bodies: [BodyFrame] } + +@MainActor final class World: ObservableObject { + @Published var time = 0.0 + @Published var playing = false + @Published var listeningRadius = 5.0 + @Published var rpmScale = 1.0 + @Published var oscillation = 1.0 + @Published var voices = 12 + var frames: [ScoreFrame] = [] + var audio: AVAudioPlayer? + private var timer: Timer? + let fps = 24.0 + + init() { + let args = CommandLine.arguments + if args.count > 1 { loadScene(args[1]) } + if args.count > 2, let p = try? AVAudioPlayer(contentsOf: URL(fileURLWithPath: args[2])) { audio = p; p.prepareToPlay() } + if args.count > 3 { voices = Int(args[3]) ?? 12 } + timer = Timer.scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true) { [weak self] _ in + Task { @MainActor in self?.tick() } + } + } + + var duration: Double { frames.isEmpty ? 120 : Double(frames.count) / fps } + var frame: ScoreFrame? { frames.isEmpty ? nil : frames[min(frames.count - 1, max(0, Int(time * fps)))] } + func toggle() { playing.toggle(); if playing { audio?.currentTime = time; audio?.play() } else { audio?.pause() } } + func seek(_ t: Double) { time = min(duration, max(0, t)); audio?.currentTime = time } + private func tick() { guard playing else { return }; time = audio?.isPlaying == true ? audio!.currentTime : time + 1 / 60; if time >= duration { playing = false; time = 0; audio?.stop() } } + + private func loadScene(_ path: String) { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return } + func u32(_ o: Int) -> UInt32 { data.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: o, as: UInt32.self) } } + func f32(_ o: Int) -> Float { Float(bitPattern: u32(o)) } + guard data.count >= 20, u32(0) == 0x53434e45 else { return } + let version = Int(u32(4)), count = Int(u32(16)), n = Int(u32(8)), row = version >= 2 ? 6 : 5 + var offset = 20, result: [ScoreFrame] = []; result.reserveCapacity(n) + for _ in 0.. 5 ? f32(offset + 20) : 440; bodies.append(.init(x:f32(offset), y:f32(offset+4), z:f32(offset+8), left:f32(offset+12), right:f32(offset+16), pitch:p)); offset += row * 4 } + result.append(.init(time:t, lx:lx, ly:ly, lz:lz, bodies:bodies)) + } + frames = result + } +} + +struct WorldView: View { + @ObservedObject var world: World + private func hue(_ hz: Float) -> Double { guard hz > 0 else { return 0.55 }; return min(0.86, max(0.02, 0.04 + 0.78 * log2(Double(hz) / 40) / 8)) } + var body: some View { + GeometryReader { geo in + Canvas { c, size in + let scale = min(size.width, size.height) / 22, center = CGPoint(x:size.width/2, y:size.height/2) + func point(_ x: Double, _ y: Double, _ z: Double) -> CGPoint { CGPoint(x:center.x + (x-y)*scale*0.72, y:center.y + (x+y)*scale*0.34-z*scale*0.72) } + for q in -10...10 { var p=Path(); p.move(to:point(Double(q),-10,0));p.addLine(to:point(Double(q),10,0));c.stroke(p,with:.color(.cyan.opacity(q == 0 ? 0.28:0.08))); var r=Path();r.move(to:point(-10,Double(q),0));r.addLine(to:point(10,Double(q),0));c.stroke(r,with:.color(.cyan.opacity(q == 0 ? 0.28:0.08))) } + guard let f = world.frame else { return } + let listener = point(Double(f.lx),Double(f.ly),Double(f.lz)), rr=world.listeningRadius*scale + c.stroke(Path(ellipseIn:CGRect(x:listener.x-rr,y:listener.y-rr*0.48,width:rr*2,height:rr*0.96)),with:.color(.yellow.opacity(0.42)),lineWidth:2) + c.fill(Path(ellipseIn:CGRect(x:listener.x-7,y:listener.y-7,width:14,height:14)),with:.color(.yellow)) + let order = world.voices >= 12 ? Array(f.bodies.indices) : Array([4,0,2,3].prefix(world.voices)) + for (i,index) in order.enumerated() { let b = f.bodies[index] + let energy = hypot(Double(b.left),Double(b.right)), phase=world.time*world.rpmScale*Double.pi*2*76/60 + Double(i)*0.37 + let ox=Double(b.x)+sin(phase*(1+Double(i)*0.023))*world.oscillation*0.16, oy=Double(b.y)+cos(phase*(1+Double(i)*0.031))*world.oscillation*0.16 + let p=point(ox,oy,Double(b.z)), low=b.pitch > 0 ? 1-min(1,max(0,log2(Double(b.pitch)/45)/7.5)):0, radius=4+low*10 + var trail=Path();trail.move(to:p);trail.addLine(to:CGPoint(x:p.x-cos(phase)*24*world.oscillation,y:p.y-sin(phase)*12*world.oscillation));c.stroke(trail,with:.color(Color(hue:hue(b.pitch),saturation:0.8,brightness:1).opacity(0.25+min(0.5,energy*25))),lineWidth:2) + c.fill(Path(ellipseIn:CGRect(x:p.x-radius,y:p.y-radius,width:radius*2,height:radius*2)),with:.color(Color(hue:hue(b.pitch),saturation:0.72,brightness:0.95))) + } + }.background(LinearGradient(colors:[Color(red:0.025,green:0.045,blue:0.07),Color(red:0.015,green:0.02,blue:0.035)],startPoint:.top,endPoint:.bottom)) + } + } +} + +struct ContentView: View { + @StateObject var world = World() + var body: some View { + VStack(spacing:0) { + WorldView(world:world) + VStack { + HStack { Button(world.playing ? "Pause" : "Play") { world.toggle() }; Slider(value:Binding(get:{world.time},set:{world.seek($0)}),in:0...max(1,world.duration)); Text(String(format:"%05.1f / %05.1f",world.time,world.duration)).monospacedDigit() } + HStack { Text("Range");Slider(value:$world.listeningRadius,in:1...12);Text("RPM");Slider(value:$world.rpmScale,in:0...4);Text("Oscillation");Slider(value:$world.oscillation,in:0...3);Picker("Voices",selection:$world.voices){ForEach([1,3,4,12],id:\.self){Text("\($0)")}}.frame(width:120) } + }.padding(12).background(.black.opacity(0.88)) + }.frame(minWidth:900,minHeight:700).preferredColorScheme(.dark) + } +} + +@main struct SineabyeLive: App { var body: some Scene { WindowGroup { ContentView() } } }