Tunes itself to where the ISS is right now. Latitude becomes pitch (south = low, north = high), longitude picks the chord intervals. As the station moves, the music slowly changes with it (next listener tunes in next hour to a new chord).
orbit-previewimport type { ElementaryPatch } from '@/lib/engine/types'
/**
* ISS-driven pad. The International Space Station's current position
* (from open-notify-style API, snapshotted each UTC hour) maps to:
*
* - latitude (-90..+90) → main pitch (south-low, north-high)
* - longitude (-180..+180) → which intervals form the chord
*
* So when the ISS passes over the equator near 0°E, we land on a tonic
* fifth; over the pacific or polar regions, the chord shifts. Two
* listeners in the same hour share the snapshot → same chord.
*/
const orbit: ElementaryPatch = {
id: 'orbit',
title: 'Orbit',
author: 'stuart',
year: 2026,
tags: ['ambient', 'iss', 'data'],
description:
'Tunes itself to where the ISS is right now. Latitude becomes pitch (south = low, north = high), longitude picks the chord intervals. As the station moves, the music slowly changes with it (next listener tunes in next hour to a new chord).',
engine: 'elementary',
worldFields: ['iss'],
audio: ({ el, rng, schedule, render, world }) => {
const lat = world?.iss?.lat ?? 0
const lon = world?.iss?.lon ?? 0
// Latitude → fundamental frequency. -52..52 is ISS's range; map to
// ~110 Hz (deep south) to ~330 Hz (high north).
const baseFreq = 220 * Math.pow(2, lat / 60)
// Longitude → choose a set of intervals (in semitones) for the chord.
// Six different chord shapes, picked by which 60° band of longitude.
const intervalSets = [
[0, 7, 12, 19, 24], // open fifths (Atlantic)
[0, 4, 7, 11, 14], // major7add9 (Africa)
[0, 3, 7, 10, 14], // minor7add9 (Asia)
[0, 5, 7, 12, 17], // suspended (Pacific west)
[0, 4, 7, 9, 14], // major6 add9 (Pacific central)
[0, 3, 6, 10, 13], // half-diminished (Pacific east / Americas)
]
const lonBand = Math.floor(((lon + 180) / 360) * intervalSets.length)
const semis = intervalSets[Math.min(lonBand, intervalSets.length - 1)]
const chord = semis.map((s) => baseFreq * Math.pow(2, s / 12))
const interval = 7 // re-render every 7s
schedule(interval, (tick) => {
const detune = Math.pow(2, rng.range(-6, 6, tick, 0) / 1200)
// Pad — sine voices with detuned partner
const padVoices = chord.map((f, i) => {
const a = el.cycle(el.const({ key: `pa-${i}`, value: f }))
const b = el.cycle(el.const({ key: `pb-${i}`, value: f * detune }))
return el.mul(el.add(a, el.mul(b, 0.6)), 0.07)
})
const pad = el.add(...padVoices)
// Continuous earth-drone — sub at the lowest octave of the chord root
const droneFreq = chord[0] / 2
const drone = el.mul(
el.add(
el.mul(el.cycle(el.const({ key: 'dr1', value: droneFreq })), 0.4),
el.mul(el.saw(el.const({ key: 'dr2', value: droneFreq })), 0.12),
),
0.45,
)
// Satellite ping: a short FM bell once per render at chord[3] (a tension tone)
const pingFreq = chord[3]
const pingGate = el.metro({ name: 'pingM', interval: interval * 1000 })
const pingEnv = el.adsr(0.005, 1.8, 0, 0, pingGate)
const pingMod = el.mul(
el.cycle(el.const({ key: 'pingMod', value: pingFreq * 5 })),
25,
)
const ping = el.mul(
el.cycle(el.add(el.const({ key: 'pingC', value: pingFreq * 2 }), pingMod)),
el.mul(pingEnv, 0.09),
)
// Cosmic background — bandpass pink noise way up high
const cosmic = el.mul(
el.bandpass(
el.const({ key: 'cosFc', value: chord[chord.length - 1] * 4 }),
2,
el.pinknoise(),
),
0.014,
)
// Mix → smoothed → soft saturation
const mix = el.add(pad, drone, ping, cosmic)
const wet = el.smooth(el.tau2pole(2.5), 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 slowly-orbiting dot tracing the world's curve, with a faint
// longitude/latitude grid behind it. Audio RMS pulses the dot.
ctx.fillStyle = 'rgba(12, 12, 12, 0.10)'
ctx.fillRect(0, 0, width, height)
const cx = width / 2
const cy = height / 2
const r = Math.min(width, height) * 0.36
// Equator + a couple of latitude bands
ctx.strokeStyle = 'rgba(232, 230, 225, 0.10)'
ctx.lineWidth = 1
for (const f of [0, -0.5, 0.5]) {
ctx.beginPath()
ctx.ellipse(cx, cy + f * r * 0.8, r, r * 0.35, 0, 0, Math.PI * 2)
ctx.stroke()
}
// Orbital ring
ctx.strokeStyle = 'rgba(232, 230, 225, 0.22)'
ctx.beginPath()
ctx.arc(cx, cy, r, 0, Math.PI * 2)
ctx.stroke()
// Orbiting dot — represents the ISS-ish position over time
const angle = t * 0.18
const dotX = cx + Math.cos(angle) * r
const dotY = cy + Math.sin(angle) * r * 0.6 // squashed for "perspective"
const rms = getRms()
const pulse = 6 + Math.min(rms * 80, 30)
ctx.fillStyle = 'rgba(239, 68, 68, 0.85)'
ctx.beginPath()
ctx.arc(dotX, dotY, pulse, 0, Math.PI * 2)
ctx.fill()
// glow
const grad = ctx.createRadialGradient(dotX, dotY, 0, dotX, dotY, pulse * 3)
grad.addColorStop(0, 'rgba(239, 68, 68, 0.4)')
grad.addColorStop(1, 'rgba(239, 68, 68, 0)')
ctx.fillStyle = grad
ctx.fillRect(
dotX - pulse * 3,
dotY - pulse * 3,
pulse * 6,
pulse * 6,
)
if (mouseActive) {
ctx.fillStyle = 'rgba(232, 230, 225, 0.06)'
ctx.beginPath()
ctx.arc(mouseX * width, mouseY * height, 30, 0, Math.PI * 2)
ctx.fill()
}
},
}
export default orbit