Five-voice chord with detuned partners and a saw shadow, continuous sub, pitched air noise, slow tremolo. Brian Eno reading by lamplight, but the lamp is warmer.
reading-room-previewimport type { ElementaryPatch } from '@/lib/engine/types'
const readingRoom: ElementaryPatch = {
id: 'reading-room',
title: 'Reading Room',
author: 'stuart',
year: 2026,
tags: ['ambient', 'chord', 'eno'],
description:
'Five-voice chord with detuned partners and a saw shadow, continuous sub, pitched air noise, slow tremolo. Brian Eno reading by lamplight, but the lamp is warmer.',
engine: 'elementary',
audio: ({ el, rng, schedule, render }) => {
// Five-note voicings (root, 3rd, 5th, 7th, 9th) — minor 7th add 9 mostly
const chords = [
[220.0, 261.63, 329.63, 392.0, 493.88], // A min7 add9
[196.0, 246.94, 293.66, 369.99, 440.0], // G min7 add9
[174.61, 220.0, 261.63, 329.63, 392.0], // F maj7 add9
[164.81, 196.0, 246.94, 293.66, 369.99], // E min7 add9
[146.83, 185.0, 220.0, 277.18, 329.63], // D min add9
]
const interval = 9 // chord changes every 9 seconds (was 14)
const detuneRatio = Math.pow(2, 7 / 1200) // +7 cents
schedule(interval, (tick) => {
const chord = rng.pick(chords, tick, 0)
const cutoff = rng.range(900, 1700, tick, 1)
const subAmp = rng.range(0.18, 0.32, tick, 2)
const airAmp = rng.range(0.012, 0.025, tick, 3)
// Each voice: sine + detuned sine + small saw component for body
const voices = chord.map((f, i) => {
const sine = el.cycle(el.const({ key: `s-${i}`, value: f }))
const sineDet = el.mul(
el.cycle(el.const({ key: `sd-${i}`, value: f * detuneRatio })),
0.7,
)
const sawShadow = el.mul(
el.saw(el.const({ key: `sw-${i}`, value: f })),
0.15,
)
return el.mul(el.add(sine, sineDet, sawShadow), 0.11)
})
const chordSum = el.add(...voices)
// Continuous sub (no fade-out swell), gently smoothed
const sub = el.mul(
el.cycle(el.const({ key: 'sub', value: chord[0] / 2 })),
el.smooth(el.tau2pole(2), el.const({ key: 'subAmp', value: subAmp })),
)
// Pitched "air" — bandpass pink noise around an upper harmonic
const air = el.mul(
el.bandpass(
el.const({ key: 'airFc', value: chord[0] * 4 }),
4,
el.pinknoise(),
),
el.const({ key: 'airAmp', value: airAmp }),
)
// Slow tremolo: 0.15 Hz LFO modulating master amp gently
const lfo = el.mul(
el.cycle(el.const({ key: 'lfo', value: 0.15 })),
0.06,
)
const tremolo = el.add(0.94, lfo)
// Mix → smoothed lowpass → tremolo → soft saturation
const mix = el.add(chordSum, sub, air)
const cutSm = el.smooth(
el.tau2pole(2.5),
el.const({ key: 'cut', value: cutoff }),
)
const filtered = el.lowpass(cutSm, 0.6, mix)
const out = el.mul(el.tanh(el.mul(filtered, tremolo, 0.65)), 0.2)
render(out, out)
})
},
visual: ({ ctx, t, width, height, getRms, mouseX, mouseY, mouseActive }) => {
// Vertical stratified bands of warm light — reading lamps. Audio
// amplitude swells the bands; cursor shifts the brightest column.
ctx.fillStyle = 'rgba(12, 12, 12, 0.08)'
ctx.fillRect(0, 0, width, height)
const bandCount = 12
const rms = getRms()
const focusX = mouseActive ? mouseX * width : width / 2
for (let i = 0; i < bandCount; i++) {
const x = (width / bandCount) * (i + 0.5)
const bandWidth = width / bandCount
const distFromFocus = Math.abs(x - focusX) / width
const closeness = 1 - distFromFocus
const baseGlow = 0.15 + closeness * 0.3
const audioGlow = Math.min(0.4, rms * 4)
const breath = 0.5 + 0.5 * Math.sin(t * (0.2 + i * 0.03) + i)
const alpha = baseGlow * breath + audioGlow
const grad = ctx.createLinearGradient(x - bandWidth / 2, 0, x + bandWidth / 2, 0)
grad.addColorStop(0, 'rgba(255, 220, 170, 0)')
grad.addColorStop(0.5, `rgba(255, 220, 170, ${alpha * 0.3})`)
grad.addColorStop(1, 'rgba(255, 220, 170, 0)')
ctx.fillStyle = grad
ctx.fillRect(x - bandWidth / 2, 0, bandWidth, height)
}
},
}
export default readingRoom