Sine + saw chord pad, sub kick on every beat, brushy hat on 16ths, snare on the off-beats, a soft arpeggio above, sub-bass underneath. Lo-fi study tape on the back of the bus, fuller than before.
late-bus-previewimport type { ElementaryPatch } from '@/lib/engine/types'
const lateBus: ElementaryPatch = {
id: 'late-bus',
title: 'Late Bus',
author: 'stuart',
year: 2026,
tags: ['lo-fi', 'study', 'beats'],
description:
'Sine + saw chord pad, sub kick on every beat, brushy hat on 16ths, snare on the off-beats, a soft arpeggio above, sub-bass underneath. Lo-fi study tape on the back of the bus, fuller than before.',
engine: 'elementary',
audio: ({ el, rng, schedule, render }) => {
// Voicings include the 7th high for that suspended/wide feel
const chords = [
[196.0, 233.08, 293.66, 349.23, 466.16], // G min7
[220.0, 261.63, 329.63, 392.0, 523.25], // A min7
[174.61, 207.65, 261.63, 311.13, 415.3], // F min7
[164.81, 196.0, 246.94, 311.13, 392.0], // E min7
]
const bpm = 75
const beatMs = 60_000 / bpm
const eighthMs = beatMs / 2
const sixteenthMs = beatMs / 4
const halfMs = beatMs * 2
const interval = (beatMs * 8) / 1000 // chord every 8 beats (~6.4s)
schedule(interval, (tick) => {
const chord = rng.pick(chords, tick, 0)
const kickAmp = rng.range(0.4, 0.55, tick, 1)
const hatAmp = rng.range(0.05, 0.08, tick, 2)
const snareAmp = rng.range(0.12, 0.2, tick, 3)
const padCutoff = rng.range(1600, 2400, tick, 4)
// Chord pad: sine + saw mix per voice
const voices = chord.map((f, i) => {
const sine = el.cycle(el.const({ key: `s-${i}`, value: f }))
const saw = el.mul(
el.saw(el.const({ key: `sw-${i}`, value: f })),
0.18,
)
return el.mul(el.add(sine, saw), 0.085)
})
const pad = el.add(...voices)
// Kick: sine pulse with pitch envelope
const kickGate = el.metro({ name: 'kick', interval: beatMs })
const kickEnv = el.adsr(0.001, 0.18, 0, 0, kickGate)
const kickPitch = el.add(
el.const({ key: 'kFreq', value: 55 }),
el.mul(kickEnv, 40),
)
const kick = el.mul(
el.cycle(kickPitch),
el.mul(kickEnv, el.const({ key: 'kAmp', value: kickAmp })),
)
// Hat: bright noise burst on every 16th
const hatGate = el.metro({ name: 'hat', interval: sixteenthMs })
const hatEnv = el.adsr(0.001, 0.04, 0, 0, hatGate)
const hat = el.mul(
el.highpass(
el.const({ key: 'hatHpf', value: 6500 }),
0.9,
el.noise(),
),
el.mul(hatEnv, el.const({ key: 'hAmp', value: hatAmp })),
)
// Snare: noise burst + body sine on beats 2 and 4 (every half note)
const snareGate = el.metro({ name: 'snare', interval: halfMs })
const snareEnv = el.adsr(0.001, 0.12, 0, 0, snareGate)
const snareNoise = el.mul(
el.bandpass(
el.const({ key: 'snFc', value: 2000 }),
0.7,
el.noise(),
),
el.mul(snareEnv, el.const({ key: 'snAmp', value: snareAmp })),
)
const snareBody = el.mul(
el.cycle(el.const({ key: 'snBody', value: 180 })),
el.mul(snareEnv, snareAmp * 0.4),
)
const snare = el.add(snareNoise, snareBody)
// Arpeggio: 8th-note sequence through chord tones (octave up)
const arpGate = el.metro({ name: 'arp', interval: eighthMs })
const arpFreq = el.seq(
{
key: `arp-${tick}`,
seq: chord.map((f) => f * 2),
loop: true,
},
arpGate,
el.const({ key: 'arpReset', value: 0 }),
)
const arpEnv = el.adsr(0.005, 0.08, 0, 0, arpGate)
const arp = el.mul(el.cycle(arpFreq), el.mul(arpEnv, 0.07))
// Continuous sub
const sub = el.mul(
el.cycle(el.const({ key: 'sub', value: chord[0] / 2 })),
el.smooth(
el.tau2pole(3),
el.const({ key: 'subAmp', value: 0.18 }),
),
)
// Mix → warm lowpass on pad → soft saturation → master gain
const padFiltered = el.lowpass(
el.smooth(
el.tau2pole(1.5),
el.const({ key: 'cut', value: padCutoff }),
),
0.5,
pad,
)
const mix = el.add(padFiltered, sub, kick, hat, snare, arp)
const out = el.mul(el.tanh(el.mul(mix, 0.5)), 0.2)
render(out, out)
})
},
visual: ({ ctx, t, width, height, getRms, getWaveform, mouseX, mouseY, mouseActive }) => {
// Pulsing concentric circles from a center point that follows the
// cursor — like a tape head bobbing. Each kick expands a new ring.
ctx.fillStyle = 'rgba(12, 12, 12, 0.18)'
ctx.fillRect(0, 0, width, height)
const cx = mouseActive ? mouseX * width : width / 2
const cy = mouseActive ? mouseY * height : height / 2
const rms = getRms()
const wave = getWaveform()
// Audio-reactive expanding rings — radius cycles with time, alpha with audio
for (let i = 0; i < 6; i++) {
const phase = (t * 0.5 + i / 6) % 1
const radius = phase * Math.min(width, height) * 0.5
const alpha = (1 - phase) * (0.15 + Math.min(0.4, rms * 4))
ctx.strokeStyle = `rgba(232, 230, 225, ${alpha})`
ctx.lineWidth = 1.5
ctx.beginPath()
ctx.arc(cx, cy, radius, 0, Math.PI * 2)
ctx.stroke()
}
// Inner waveform — small swirling circle of audio samples
if (wave) {
const innerR = 30 + rms * 100
ctx.strokeStyle = `rgba(232, 230, 225, ${0.4 + Math.min(0.4, rms * 4)})`
ctx.lineWidth = 1.2
ctx.beginPath()
for (let i = 0; i < wave.length; i += 2) {
const a = (i / wave.length) * Math.PI * 2 + t * 0.3
const r = innerR + wave[i] * 30
const x = cx + Math.cos(a) * r
const y = cy + Math.sin(a) * r
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.closePath()
ctx.stroke()
}
// Center dot
ctx.fillStyle = `rgba(239, 68, 68, ${0.5 + Math.min(0.5, rms * 5)})`
ctx.beginPath()
ctx.arc(cx, cy, 4 + Math.min(8, rms * 30), 0, Math.PI * 2)
ctx.fill()
},
}
export default lateBus