leach.studio

Earth

ambient · data · composite
↓ source · seed

about this piece

Every data feed at once: weather tunes the mode, ISS picks the root chord, earthquakes punctuate with low gongs, geomagnetic activity adds the shimmer. The whole snapshot of the moment becoming sound.

seed
earth-preview
by
stuart
year
2026
tags
ambient · data · composite
permalink
/a/earth/earth-preview

source

import type { ElementaryPatch } from '@/lib/engine/types'
import { MODES, scaleNotes } from '@/lib/generative'

/**
 * Every world feed feeding music at once. The whole snapshot — weather,
 * seismic activity, ISS position, geomagnetic state — becomes a single
 * piece, four voices breathing together:
 *
 *   weather.tempF        → mode (cold = phrygian, warm = dorian, hot = lydian)
 *   weather.windMph      → pad amplitude swell rate
 *   iss.lat              → root pitch (south = low, north = high)
 *   iss.lon              → which intervals form the chord
 *   earthquakes          → low gong each cycle, magnitudes drive pitch
 *   spaceWeather.kp      → high shimmer chaos
 *   spaceWeather.sw      → LFO speed for the whole field
 *
 * Same snapshot for every listener in the same UTC hour.
 */
const earth: ElementaryPatch = {
  id: 'earth',
  title: 'Earth',
  author: 'stuart',
  year: 2026,
  tags: ['ambient', 'data', 'composite'],
  description:
    'Every data feed at once: weather tunes the mode, ISS picks the root chord, earthquakes punctuate with low gongs, geomagnetic activity adds the shimmer. The whole snapshot of the moment becoming sound.',
  engine: 'elementary',
  worldFields: ['weather', 'earthquakes', 'iss', 'spaceWeather'],

  audio: ({ el, rng, schedule, render, world }) => {
    const tempF = world?.weather?.tempF ?? 55
    const windMph = world?.weather?.windMph ?? 5
    const lat = world?.iss?.lat ?? 0
    const lon = world?.iss?.lon ?? 0
    const recentMags = world?.earthquakes?.recentMags ?? [3, 4, 2.5, 5]
    const kp = world?.spaceWeather?.kpIndex ?? 2
    const sw = world?.spaceWeather?.solarWindSpeedKmS ?? 420

    // Temperature → mode
    let mode: readonly number[]
    if (tempF < 40) mode = MODES.phrygian
    else if (tempF < 60) mode = MODES.minor
    else if (tempF < 75) mode = MODES.dorian
    else mode = MODES.lydian

    // ISS latitude → root frequency
    const rootHz = 220 * Math.pow(2, lat / 60)
    const palette = scaleNotes(rootHz, mode, 2)

    // ISS longitude → chord shape (which scale degrees to use)
    const lonNorm = (lon + 180) / 360
    const shapes = [
      [0, 2, 4, 6], // 1-3-5-7
      [0, 2, 4, 5], // 1-3-5-6
      [0, 2, 5, 7], // 1-3-6-9
      [0, 4, 5, 7], // 1-5-6-9
      [0, 1, 4, 6], // 1-b3-5-7 (mood-shift)
    ]
    const shape = shapes[Math.floor(lonNorm * shapes.length) % shapes.length]
    const chord = shape.map((s) => palette[s % palette.length])

    // Solar wind → LFO speed (200 km/s = 0.05 Hz, 800 = 0.4 Hz)
    const lfoHz = 0.05 + ((sw - 200) / 600) * 0.35

    // Wind speed → pad swell rate (faster wind = faster swell)
    const padSwellHz = 0.05 + windMph / 300

    const interval = 6

    schedule(interval, (tick) => {
      // ── Pad (weather + ISS): chord at ISS pitch in weather mode ──
      const padVoices = chord.map((f, i) => {
        const a = el.cycle(el.const({ key: `e-${i}`, value: f }))
        const b = el.cycle(el.const({ key: `eb-${i}`, value: f * 1.0035 }))
        return el.mul(el.add(a, el.mul(b, 0.6)), 0.07)
      })
      const padRaw = el.add(...padVoices)
      const padLfo = el.cycle(el.const({ key: 'padLfo', value: padSwellHz }))
      const padAmp = el.add(0.7, el.mul(padLfo, 0.25))
      const pad = el.mul(padRaw, padAmp)

      // ── Sub (ISS root one octave below) ──
      const sub = el.mul(
        el.cycle(el.const({ key: 'sub', value: rootHz / 2 })),
        0.22,
      )

      // ── Quake gong (USGS) — pitch from a recent magnitude ──
      const mag = recentMags[tick % recentMags.length] ?? 3
      const gongFreq = 110 * Math.pow(2, -(mag - 1) / 2)
      const gongGate = el.metro({
        name: 'earthGong',
        interval: interval * 1000,
      })
      const gongEnv = el.adsr(0.005, Math.min(6, 1.5 + mag), 0, 0, gongGate)
      const gongMod = el.mul(
        el.cycle(el.const({ key: 'gongMod', value: gongFreq * 1.41 })),
        el.mul(gongEnv, 60),
      )
      const gong = el.mul(
        el.cycle(el.add(el.const({ key: 'gongCar', value: gongFreq }), gongMod)),
        el.mul(
          gongEnv,
          el.const({ key: 'gongAmp', value: Math.min(0.3, 0.1 + mag / 20) }),
        ),
      )

      // ── ISS lead — high sustained note tracking latitude ──
      const leadFreq = palette[5] // a high scale tone
      const leadLfo = el.cycle(el.const({ key: 'leadLfo', value: lfoHz * 0.7 }))
      const leadAmp = el.mul(el.add(0.5, el.mul(leadLfo, 0.3)), 0.05)
      const lead = el.mul(
        el.cycle(el.const({ key: 'leadF', value: leadFreq })),
        leadAmp,
      )

      // ── Geomagnetic shimmer — noise + sine partial scaled by Kp ──
      const chaos = Math.min(1, kp / 9)
      const shimmer = el.mul(
        el.cycle(el.const({ key: 'shim', value: leadFreq * 4 })),
        el.const({ key: 'shimAmp', value: 0.02 + chaos * 0.06 }),
      )
      const stormNoise = el.mul(
        el.bandpass(
          el.const({ key: 'stormFc', value: leadFreq * 2 }),
          1.2,
          el.pinknoise(),
        ),
        el.const({ key: 'stormAmp', value: 0.005 + chaos * 0.04 }),
      )

      // Mix → smoothed master → soft saturation
      const mix = el.add(pad, sub, gong, lead, shimmer, stormNoise)
      const wet = el.smooth(el.tau2pole(0.6), mix)
      const out = el.mul(el.tanh(el.mul(wet, 0.55)), 0.18)
      render(out, out)
    })
  },

  visual: ({ ctx, t, width, height, mouseX, mouseY, mouseActive, getRms }) => {
    // A horizon line with three bands of motion: stratospheric (top),
    // surface (middle), seismic (bottom). Each band represents a different
    // data source.
    ctx.fillStyle = 'rgba(12, 12, 12, 0.10)'
    ctx.fillRect(0, 0, width, height)

    const rms = getRms()
    const cx = width / 2
    const cy = height / 2

    // Horizon line
    ctx.strokeStyle = 'rgba(232, 230, 225, 0.25)'
    ctx.lineWidth = 1
    ctx.beginPath()
    ctx.moveTo(0, cy)
    ctx.lineTo(width, cy)
    ctx.stroke()

    // Sky band (top half) — slow drifting curves like aurora
    for (let i = 0; i < 3; i++) {
      ctx.strokeStyle = `rgba(150, 200, 230, ${0.15 + i * 0.06})`
      ctx.lineWidth = 1
      ctx.beginPath()
      const yBase = cy * (0.3 + i * 0.2)
      for (let x = 0; x <= width; x += 4) {
        const wave = Math.sin(t * 0.3 + (x / width) * 4 + i) * 18
        const y = yBase + wave
        if (x === 0) ctx.moveTo(x, y)
        else ctx.lineTo(x, y)
      }
      ctx.stroke()
    }

    // Surface band — ISS-like dot orbiting
    const orbAngle = t * 0.15
    const orbX = cx + Math.cos(orbAngle) * width * 0.35
    const orbY = cy + Math.sin(orbAngle) * 30
    ctx.fillStyle = 'rgba(232, 230, 225, 0.7)'
    ctx.beginPath()
    ctx.arc(orbX, orbY, 4, 0, Math.PI * 2)
    ctx.fill()

    // Seismic band (bottom half) — audio-reactive trace
    ctx.strokeStyle = `rgba(239, 68, 68, ${0.5 + Math.min(0.4, rms * 5)})`
    ctx.lineWidth = 1.2 + Math.min(2, rms * 10)
    ctx.beginPath()
    for (let x = 0; x <= width; x += 2) {
      const slow = Math.sin(t * 0.5 + (x / width) * 6) * (5 + rms * 30)
      const y = cy + height * 0.3 + slow
      if (x === 0) ctx.moveTo(x, y)
      else ctx.lineTo(x, y)
    }
    ctx.stroke()

    if (mouseActive) {
      ctx.fillStyle = 'rgba(232, 230, 225, 0.06)'
      ctx.beginPath()
      ctx.arc(mouseX * width, mouseY * height, 60, 0, Math.PI * 2)
      ctx.fill()
    }
  },
}

export default earth
← leach.studio