{"name":"audio_sparkles.nu","source":"// audio_sparkles.nu — Microphone-driven pixel fireworks.\n//\n// Reads the browser microphone through the `audio` FFI and paints\n// particles on a pixel canvas whose count, colour and velocity track\n// the sound:\n//\n//   audio_level      → brightness + number of new particles / frame\n//   audio_peak_bin   → hue shift (dominant pitch)\n//   audio_centroid   → vertical bias (bright sound → top)\n//   audio_is_silent  → particles fall & fade (no new spawns)\n//\n// Build (in-browser): press Build then Run in the NURL playground.\n// The browser will ask for microphone permission on first run.\n//\n// The program is wasm-only: all audio_* hooks are provided by the\n// playground's JS shim (stdlib/audio_wasm.c forwards to them). A\n// native SDL2 back-end is *not* wired up for audio today, so running\n// this via `nurl.sh` will link-fail with unresolved host imports.\n\n& `libc` @ rand → i\n\n& `canvas` @ canvas_open i w i h → *i\n\n& `canvas` @ canvas_present → v\n\n& `canvas` @ canvas_sleep i ms → v\n\n& `canvas` @ canvas_should_close → i\n\n& `canvas` @ canvas_close → v\n\n& `audio` @ audio_level → f\n\n& `audio` @ audio_peak_bin → i\n\n& `audio` @ audio_centroid → f\n\n& `audio` @ audio_bin_count → i\n\n& `audio` @ audio_is_silent i pct → i\n\n& `audio` @ audio_ready → i\n\n: i W 480\n: i H 270\n: i FPS 60\n: i MAX_PART 2000\n: i FADE_MASK 16645629  // 0x00FDFDFD — keep 7/8 of each channel\n: i ALPHA 4278190080  // 0xFF000000\n\n// Linear-congruential-ish scramble so `hue_from_bin` doesn't just\n// produce rainbows; gives each dominant pitch a distinct colour.\n@ hue_from_bin i bin → i {\n    : i h + * bin 53 17\n    ^ % h 360\n}\n\n// Convert hue 0..360 into a rough RGB value (sextant-based, no sat/val).\n// Returns 0x00RRGGBB (alpha added by caller).\n@ hue_rgb i hue → i {\n    : i seg / hue 60\n    : i f * % hue 60 4  // 0..236 within segment\n    : i q - 255 f\n    : ~ i r 0 : ~ i g 0 : ~ i b 0\n    ? == seg 0 { = r 255 = g f = b 0 } {}\n    ? == seg 1 { = r q = g 255 = b 0 } {}\n    ? == seg 2 { = r 0 = g 255 = b f } {}\n    ? == seg 3 { = r 0 = g q = b 255 } {}\n    ? == seg 4 { = r f = g 0 = b 255 } {}\n    ? == seg 5 { = r 255 = g 0 = b q } {}\n    ^ | | * r 65536 * g 256 b\n}\n\n@ main → i {\n    // Particle pools: parallel i64 arrays. Positions are in framebuffer\n    // coords. vx/vy are pixels/frame × 100 (fixed-point Q8-ish).\n    : *i px # *i ( malloc * MAX_PART 8 )\n    : *i py # *i ( malloc * MAX_PART 8 )\n    : *i pvx # *i ( malloc * MAX_PART 8 )\n    : *i pvy # *i ( malloc * MAX_PART 8 )\n    : *i pcol # *i ( malloc * MAX_PART 8 )\n    : *i plife # *i ( malloc * MAX_PART 8 )\n\n    // Zero life-counter for every slot (0 = free).\n    : ~ i zi 0\n    ~ < zi MAX_PART {\n        = . plife zi 0\n        = zi + zi 1\n    }\n\n    : *i fb ( canvas_open W H )\n    : i frame_ms / 1000 FPS\n    : ~ i t 0\n    : i total_px * W H\n\n    ~ == 0 ( canvas_should_close ) {\n\n        // ── 1. Fade the whole framebuffer toward black for motion blur.\n        : ~ i px_idx 0\n        ~ < px_idx total_px {\n            : i old . fb px_idx\n            // Preserve 7/8 of each 8-bit channel → smooth exponential fade.\n            : i kept & old FADE_MASK\n            // NURL has no bit-shift operator; integer division by 8 is the\n            // moral equivalent of `kept >> 3` for the unsigned channel bytes.\n            : i faded - kept / kept 8  // approx *7/8\n            = . fb px_idx | ALPHA & faded 16777215\n            = px_idx + px_idx 1\n        }\n\n        // ── 2. Sample microphone.\n        : i ready ( audio_ready )\n        : f lvl ? != 0 ready ( audio_level ) 0.0\n        : i peak ? != 0 ready ( audio_peak_bin ) 0\n        : f cen ? != 0 ready ( audio_centroid ) 0.0\n        : i silent ( audio_is_silent 2 )\n\n        // ── 3. Spawn up to N new particles proportional to loudness.\n        //      lvl is 0..1; typical speech/music peaks around 0.2–0.5.\n        : ~ i spawn_cnt # i * lvl 300.0\n        ? != 0 silent { = spawn_cnt 0 } {}\n\n        : i hue ( hue_from_bin peak )\n        : i col | ALPHA ( hue_rgb hue )\n\n        // cen is in Hz; map a rough 50–8000 Hz range to vertical bias\n        // 0 (top) .. H (bottom). Bright pitches aim up.\n        : i cen_i # i cen\n        : ~ i bias_y - H / * cen_i H 8000\n        ? < bias_y 0 { = bias_y 0 } {}\n        ? >= bias_y H { = bias_y - H 1 } {}\n\n        : ~ i s 0\n        ~ < s spawn_cnt {\n            // Find a free slot (plife == 0).\n            : ~ i slot 0\n            : ~ i found 0\n            ~ & == 0 found < slot MAX_PART {\n                ? == . plife slot 0 { = found 1 } { = slot + slot 1 }\n            }\n            ? == 0 found { = s spawn_cnt } {}  // pool full, stop spawning\n\n            ? != 0 found {\n                = . px slot / W 2\n                = . py slot bias_y\n                // Velocity: random direction, magnitude ∝ loudness.\n                : i speed + 100 # i * lvl 800.0\n                : i ang % ( rand ) 360\n                // Cheap sin/cos via table is overkill; approximate with\n                // modulo-branches. Use rand again for variety.\n                : i vx - % ( rand ) 400 200\n                : i vy - % ( rand ) 400 200\n                = . pvx slot / * vx speed 200\n                = . pvy slot / * vy speed 200\n                = . pcol slot col\n                = . plife slot + 60 % ( rand ) 60  // 60–120 frames\n            } {}\n\n            = s + s 1\n        }\n\n        // ── 4. Integrate and draw existing particles.\n        : ~ i i 0\n        ~ < i MAX_PART {\n            ? != 0 . plife i {\n                // Advance position (vx/vy are pixels/frame × 100).\n                = . px i + . px i / . pvx i 100\n                = . py i + . py i / . pvy i 100\n                // Gravity (pulls down).\n                = . pvy i + . pvy i 4\n                // Decay life.\n                = . plife i - . plife i 1\n\n                : i xx . px i\n                : i yy . py i\n                ? & & >= xx 0 < xx W & >= yy 0 < yy H {\n                    = . fb + * yy W xx . pcol i\n                } { = . plife i 0 }\n            } {}\n            = i + i 1\n        }\n\n        ( canvas_present )\n        ( canvas_sleep frame_ms )\n        = t + t 1\n    }\n\n    ( canvas_close )\n    ^ 0\n}\n","bytes":6220}