Slow notes drifting in D Phrygian. Sine-based poly pad through a long reverb.
still-water-previewimport type { Patch } from '@/lib/engine/types'
const stillWater: Patch = {
id: 'still-water',
title: 'Still Water',
author: 'stuart',
year: 2026,
tags: ['ambient', 'drone'],
description:
'Slow notes drifting in D Phrygian. Sine-based poly pad through a long reverb.',
audio: ({ tone, rng, initialTick, dispose }) => {
const reverb = new tone.Reverb({ decay: 12, wet: 0.6 }).toDestination()
const synth = new tone.PolySynth(tone.Synth, {
oscillator: { type: 'sine' },
envelope: { attack: 2, decay: 1, sustain: 0.6, release: 6 },
}).connect(reverb)
synth.volume.value = -14
const notes = ['D3', 'Eb3', 'F3', 'G3', 'A3', 'Bb3', 'C4', 'D4', 'F4']
const interval = tone.Time('2m').toSeconds()
let tick = initialTick(interval)
tone.getTransport().scheduleRepeat((time) => {
const note = rng.pick(notes, tick, 0)
const dur = rng.range(2, 6, tick, 1)
synth.triggerAttackRelease(note, dur, time)
if (rng.at(tick, 2) > 0.55) {
const harmony = rng.pick(notes, tick, 3)
synth.triggerAttackRelease(harmony, dur * 0.8, time + 0.3)
}
tick++
}, interval)
dispose(() => {
synth.dispose()
reverb.dispose()
})
},
visual: ({ ctx, t, width, height, getRms, getWaveform, mouseX, mouseY, mouseActive }) => {
// Still water surface — horizontal lines that ripple with audio,
// with the cursor causing a localized splash.
ctx.fillStyle = 'rgba(12, 12, 12, 0.10)'
ctx.fillRect(0, 0, width, height)
const wave = getWaveform()
const rms = getRms()
const lineCount = 26
const cursorX = mouseActive ? mouseX * width : -1000
const cursorY = mouseActive ? mouseY * height : -1000
for (let i = 0; i < lineCount; i++) {
const baseY = (height / lineCount) * (i + 0.5)
const phase = t * (0.05 + i * 0.008) + i
ctx.strokeStyle = `rgba(120, 160, 200, ${0.06 + (i / lineCount) * 0.16})`
ctx.lineWidth = 1
ctx.beginPath()
const segments = 110
for (let s = 0; s <= segments; s++) {
const x = (s / segments) * width
const sineWave = Math.sin(phase + (s / segments) * 4) * (4 + rms * 30)
const audioRipple = wave
? wave[Math.floor((s / segments) * wave.length)] * 16
: 0
let y = baseY + sineWave + audioRipple
// Cursor splash
if (mouseActive) {
const dx = x - cursorX
const dy = y - cursorY
const d = Math.hypot(dx, dy)
if (d < 100) {
const fall = 1 - d / 100
y += dy * fall * 0.4
}
}
if (s === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
}
},
}
export default stillWater