leach.studio

Cathedral

ambient · drone · reverb
↓ source · seed

about this piece

A single chord fed into a two-second delay with 80% feedback. Plays one chord per minute. Made for empty rooms — turn it on, walk away, come back later, the chord's still hanging.

seed
cathedral-preview
by
stuart
year
2026
tags
ambient · drone · reverb
permalink
/a/cathedral/cathedral-preview

source

import type { ElementaryPatch } from '@/lib/engine/types'

/**
 * Monolithic reverb pad. A single slowly-shifting chord fed into a long
 * feedback delay (~2s) with high regen, then lowpass-smoothed. The
 * chord changes about once a minute; everything else just decays into
 * itself. Made for empty rooms.
 */
const cathedral: ElementaryPatch = {
  id: 'cathedral',
  title: 'Cathedral',
  author: 'stuart',
  year: 2026,
  tags: ['ambient', 'drone', 'reverb'],
  description:
    "A single chord fed into a two-second delay with 80% feedback. Plays one chord per minute. Made for empty rooms — turn it on, walk away, come back later, the chord's still hanging.",
  engine: 'elementary',

  audio: ({ el, rng, schedule, render }) => {
    // Wide voicings with extended tones — built for sustained ringing
    const chords = [
      [110, 164.81, 220, 277.18, 329.63, 415.3], // A min add9 wide (A E A C# E G#)
      [98, 146.83, 196, 246.94, 293.66, 369.99], // G maj7 wide
      [87.31, 130.81, 174.61, 220, 261.63, 329.63], // F maj9 wide
      [82.41, 123.47, 164.81, 196, 246.94, 311.13], // E min7 wide
    ]
    const interval = 60 // chord change every minute

    schedule(interval, (tick) => {
      const chord = rng.pick(chords, tick, 0)
      const cutoffTarget = rng.range(800, 1500, tick, 1)

      // Pad voices — sine + tiny detuned sine
      const voices = chord.map((f, i) => {
        const a = el.cycle(el.const({ key: `c-${i}`, value: f }))
        const b = el.cycle(el.const({ key: `cd-${i}`, value: f * 1.003 }))
        return el.mul(el.add(a, el.mul(b, 0.7)), 0.06)
      })
      const pad = el.add(...voices)

      // Massive feedback delay — 2 second delay, 0.8 feedback. The room.
      const longDelay = el.delay(
        { key: 'cathDelay', size: 192000 },
        el.const({ key: 'dlen', value: 96000 }), // 2s @ 48k
        el.const({ key: 'dfb', value: 0.8 }),
        pad,
      )

      // Mix dry pad with delayed signal
      const wetDry = el.add(el.mul(pad, 0.4), el.mul(longDelay, 0.6))

      // Smooth the cutoff so it morphs slowly
      const cutSm = el.smooth(
        el.tau2pole(8),
        el.const({ key: 'cut', value: cutoffTarget }),
      )
      const filtered = el.lowpass(cutSm, 0.5, wetDry)

      // Master smoothing — softens any transient
      const mastered = el.smooth(el.tau2pole(0.5), filtered)
      const out = el.mul(el.tanh(el.mul(mastered, 0.5)), 0.18)
      render(out, out)
    })
  },

  visual: ({ ctx, t, width, height, getRms, mouseX, mouseY, mouseActive }) => {
    // Vertical light beams — like cathedral windows. Audio swells the beams.
    ctx.fillStyle = 'rgba(12, 12, 12, 0.06)'
    ctx.fillRect(0, 0, width, height)

    const beamCount = 8
    const rms = getRms()
    const intensity = 0.3 + Math.min(0.5, rms * 4)

    for (let i = 0; i < beamCount; i++) {
      const x = (width / (beamCount + 1)) * (i + 1)
      const breath = 0.5 + 0.5 * Math.sin(t * 0.08 + i * 0.7)
      const beamW = 60 + breath * 40
      const grad = ctx.createLinearGradient(x - beamW / 2, 0, x + beamW / 2, 0)
      const a = intensity * (0.4 + breath * 0.5)
      grad.addColorStop(0, 'rgba(220, 200, 240, 0)')
      grad.addColorStop(0.5, `rgba(220, 200, 240, ${a * 0.18})`)
      grad.addColorStop(1, 'rgba(220, 200, 240, 0)')
      ctx.fillStyle = grad
      ctx.fillRect(x - beamW / 2, 0, beamW, height)
    }

    // Soft floor of warm light at the bottom
    const floorGrad = ctx.createLinearGradient(0, height * 0.6, 0, height)
    floorGrad.addColorStop(0, 'rgba(255, 220, 180, 0)')
    floorGrad.addColorStop(1, `rgba(255, 220, 180, ${0.04 + intensity * 0.06})`)
    ctx.fillStyle = floorGrad
    ctx.fillRect(0, height * 0.6, width, height * 0.4)

    if (mouseActive) {
      const cx = mouseX * width
      const cy = mouseY * height
      const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, 100)
      grad.addColorStop(0, 'rgba(255, 230, 190, 0.18)')
      grad.addColorStop(1, 'rgba(255, 230, 190, 0)')
      ctx.fillStyle = grad
      ctx.fillRect(cx - 100, cy - 100, 200, 200)
    }
  },
}

export default cathedral
← leach.studio