{"name":"audio_sparcles2.nu","source":"& `libc` @ rand → i\n\n& `libc` @ malloc i size → *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_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 4000\n: i FADE_MASK 16645629  // 0x00FDFDFD – keep 7/8 of each channel\n: i ALPHA 4278190080  // 0xFF000000 – opaque alpha\n\n// Linear‑congruential scramble – gives each pitch a distinct hue.\n@ hue_from_bin i bin → i {\n    : i h + * bin 53 17\n    ^ % h 360\n}\n\n// Convert hue (0‑360) to a rough RGB value (0x00RRGGBB).\n@ hue_rgb i hue → i {\n    : i seg / hue 60\n    : i f * % hue 60 4\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// Fade the whole framebuffer toward black (motion‑blur effect).\n@ fade_frame * i fb i total_px → v {\n    : ~ i i 0\n    ~ < i total_px {\n        : i old . fb i\n        : i kept & old FADE_MASK\n        : i faded - kept / kept 8\n        = . fb i | ALPHA & faded 16777215\n        = i + i 1\n    }\n}\n\n// --------------------------------------------------------------------\n//  Main program – microphone‑driven pixel fireworks\n// --------------------------------------------------------------------\n@ main → i {\n    // Allocate particle pools (parallel i64 arrays).\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    // Initialise life‑counters (0 = free slot).\n    : ~ i zi 0\n    ~ < zi MAX_PART {\n        = . plife zi 0\n        = zi + zi 1\n    }\n\n    // Open a canvas.\n    : *i fb ( canvas_open W H )\n    : i frame_ms / 1000 FPS\n    : i total_px * W H\n\n    // Main render loop – run while the window stays open.\n    ~ == 0 ( canvas_should_close ) {\n        // 1️⃣  Fade previous frame (motion blur).\n        ( fade_frame fb total_px )\n\n        // 2️⃣  Sample audio – guarded by audio_ready.\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 new particles proportional to loudness.\n        : ~ i spawn_cnt # i * lvl 500.0\n        ? != 0 silent { = spawn_cnt 0 } {}\n\n        // Colour based on dominant pitch.\n        : i hue ( hue_from_bin peak )\n        : i col | ALPHA ( hue_rgb hue )\n\n        // 4️⃣  Vertical bias from centroid (bright sound → top).\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        // 5️⃣  Spawn particles.\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            // If the pool is full, abort further spawns.\n            ? == 0 found { = s spawn_cnt } {}\n            ? != 0 found {\n                = . px slot / slot 2\n                = . py slot bias_y\n                : i speed + 100 # i * lvl 800.0\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  // life 60‑120 frames\n            } {}\n            = s + s 1\n        }\n\n        // 6️⃣  Update and draw existing particles.\n        : ~ i i 0\n        ~ < i MAX_PART {\n            ? != 0 . plife i {\n                // Advance position (fixed‑point Q8).\n                = . px i + . px i / . pvx i 100\n                = . py i + . py i / . pvy i 100\n                // Simple gravity.\n                = . pvy i + . pvy i 4\n                // Age.\n                = . plife i - . plife i 1\n\n                // Draw if inside the canvas.\n                : i x . px i\n                : i y . py i\n                : i col . pcol i\n                ? & >= x 0 < x W {\n                    ? & >= y 0 < y H {\n                        = . fb + * y W x col\n                    } {}\n                } {}\n                // Kill particles that wander off‑screen.\n                ? & >= x 0 < x W & >= y 0 < y H {} { = . plife i 0 }\n            } {}\n            = i + i 1\n        }\n\n        // 7️⃣  Present the frame and sleep.\n        ( canvas_present )\n        ( canvas_sleep frame_ms )\n    }\n\n    // Cleanup.\n    ( canvas_close )\n    ^ 0\n}\n","bytes":5196}