Listens to today's geomagnetic activity. Quiet K-index = calm 5-tone shimmer. Storm K-index (5+) = the chord starts pulling itself apart. Solar wind speed sets the LFO rate. Real space weather, sounding.
aurora-previewimport type { ElementaryPatch } from '@/lib/engine/types'
/**
* Geomagnetically-driven shimmer. NOAA SWPC supplies:
*
* - Kp index (0..9) → "chaos" parameter
* - solar wind speed (km/s) → LFO speed (slow at 300, fast at 800)
*
* On a calm day (Kp 0-2) the patch is a steady 5-tone chord with light
* shimmer. On a storm day (Kp 5+) the chord detunes wildly, the noise
* thickens, and pitches drift.
*/
const aurora: ElementaryPatch = {
id: 'aurora',
title: 'Aurora',
author: 'stuart',
year: 2026,
tags: ['ambient', 'space-weather', 'data'],
description:
"Listens to today's geomagnetic activity. Quiet K-index = calm 5-tone shimmer. Storm K-index (5+) = the chord starts pulling itself apart. Solar wind speed sets the LFO rate. Real space weather, sounding.",
engine: 'elementary',
worldFields: ['spaceWeather'],
audio: ({ el, rng, schedule, render, world }) => {
const kp = world?.spaceWeather?.kpIndex ?? 2
const sw = world?.spaceWeather?.solarWindSpeedKmS ?? 420
// Chaos: 0 (calm) to 1 (storm)
const chaos = Math.min(1, kp / 9)
// LFO speed scales with solar wind
const lfoHz = 0.05 + ((sw - 200) / 600) * 0.25 // 0.05 to 0.3
// Base chord in C lydian — bright, ethereal
const baseChord = [261.63, 329.63, 369.99, 493.88, 587.33] // C E F# B D
const interval = 5
schedule(interval, (tick) => {
// At higher chaos, each voice gets randomized detuning each tick
const voices = baseChord.map((f, i) => {
const detuneCents = rng.range(-50 * chaos, 50 * chaos, tick, i + 1)
const driftedF = f * Math.pow(2, detuneCents / 1200)
const sine = el.cycle(el.const({ key: `aur-${i}`, value: driftedF }))
const tri = el.mul(
el.triangle(el.const({ key: `aurT-${i}`, value: driftedF * 2 })),
0.25,
)
return el.mul(el.add(sine, tri), 0.08)
})
const chord = el.add(...voices)
// Two LFOs — one for filter, one for amp; speed scales with solar wind
const lfoA = el.cycle(el.const({ key: 'lfoA', value: lfoHz }))
const lfoB = el.cycle(el.const({ key: 'lfoB', value: lfoHz * 1.3 }))
const cutoff = el.add(1500, el.mul(lfoA, 800))
const ampMod = el.add(0.85, el.mul(lfoB, 0.12))
// Storm noise — bandpass pink, thicker at high Kp
const stormNoise = el.mul(
el.bandpass(
el.const({ key: 'stormFc', value: 800 }),
1.2,
el.pinknoise(),
),
el.const({ key: 'stormAmp', value: 0.005 + chaos * 0.04 }),
)
// Shimmering top partial that flickers at LFO rate
const shimmer = el.mul(
el.cycle(el.const({ key: 'shimF', value: baseChord[3] * 4 })),
el.add(0.05, el.mul(lfoA, 0.05)),
)
// Filtered + LFO'd amp
const filtered = el.lowpass(cutoff, 0.6, el.add(chord, stormNoise, shimmer))
const swelling = el.mul(filtered, ampMod)
const out = el.mul(el.tanh(el.mul(swelling, 0.55)), 0.18)
render(out, out)
})
},
visual: ({ ctx, rng, t, width, height, mouseX, mouseY, mouseActive, getRms }) => {
// Vertical aurora curtain — wavy ribbons of light. Audio amplitude
// brightens the curtain. Cursor adds a subtle local heat.
ctx.fillStyle = 'rgba(12, 12, 12, 0.10)'
ctx.fillRect(0, 0, width, height)
const ribbonCount = 5
const rms = getRms()
const intensity = 0.4 + Math.min(rms * 5, 0.6)
for (let r = 0; r < ribbonCount; r++) {
const baseX = (width / (ribbonCount + 1)) * (r + 1)
const phaseShift = r * 1.3
const colorShift = (r * 30) % 100
const grad = ctx.createLinearGradient(baseX - 60, 0, baseX + 60, 0)
const a = intensity * (0.6 + (r / ribbonCount) * 0.4)
grad.addColorStop(0, `rgba(150, 200, 230, 0)`)
grad.addColorStop(0.5, `rgba(${100 + colorShift}, 220, 200, ${a * 0.5})`)
grad.addColorStop(1, `rgba(150, 200, 230, 0)`)
ctx.fillStyle = grad
ctx.beginPath()
const segments = 30
// Draw the curving ribbon as a polygon
for (let s = 0; s <= segments; s++) {
const y = (s / segments) * height
const wave = Math.sin(t * 0.4 + phaseShift + (s / segments) * 4) * 40
const x = baseX + wave
if (s === 0) ctx.moveTo(x - 30, y)
else ctx.lineTo(x - 30, y)
}
for (let s = segments; s >= 0; s--) {
const y = (s / segments) * height
const wave = Math.sin(t * 0.4 + phaseShift + (s / segments) * 4) * 40
const x = baseX + wave
ctx.lineTo(x + 30, y)
}
ctx.closePath()
ctx.fill()
}
if (mouseActive) {
const mx = mouseX * width
const my = mouseY * height
const grad = ctx.createRadialGradient(mx, my, 0, mx, my, 100)
grad.addColorStop(0, 'rgba(232, 230, 225, 0.08)')
grad.addColorStop(1, 'rgba(232, 230, 225, 0)')
ctx.fillStyle = grad
ctx.fillRect(mx - 100, my - 100, 200, 200)
}
// Random "flares" — quick bright dots that fade
const tickFlare = Math.floor(t * 2)
if (rng.at(tickFlare, 99) > 0.7) {
const fx = rng.range(0, width, tickFlare, 100)
const fy = rng.range(0, height, tickFlare, 101)
const fr = rng.range(2, 5, tickFlare, 102)
ctx.fillStyle = 'rgba(232, 230, 225, 0.6)'
ctx.beginPath()
ctx.arc(fx, fy, fr, 0, Math.PI * 2)
ctx.fill()
}
},
}
export default aurora