Glass Animals combine psychedelic indie pop with R&B and hip-hop production: dry, syncopated drums; warm sub-bass; processed guitar; unusual organic textures; bittersweet harmony; and short hooks that leave room for the vocal.
The two runnable pieces below are original style studies, not transcriptions. They use new chord progressions and melodies while exploring the production ideas behind Heat Waves and Youth. Unlike a four-bar beat, each piece is a 32-bar arrangement with an intro, contrasting sections, drum fills, dropouts, and a final return.
The sampled drums come from Strudel’s standard Dirt Samples collection. The map and individual hits load from GitHub the first time you press Play; all pitched parts are synthesized in the browser.
Heat Waves: hazy nocturnal pop
For Heat Waves, Dave Bayley recorded guitar parts an octave above and below the original to leave space in the middle for the vocal. He then added a deep synth, layered acoustic and electronic drums, off-beat seventh chords, larger chorus vocals, and 808 bass. He describes those parts in the Song Exploder episode transcript.
This original F-sharp minor piece translates that production shape into a filtered opening, a restrained verse, a four-bar lift, a wide chorus, a breakdown, and a shorter final chorus. Separate effect orbits keep the pad, processed-guitar voices, and lead from overwriting one another’s delay and reverb settings.
// Load only when this block is played; samples are not bundled with Notes.
samples('github:tidalcycles/dirt-samples');
// F#m(add9) -> Dmaj7 -> A(add9) -> E6, one chord per bar.
const heatChords = note(
"<[f#3,a3,c#4,g#4] [d3,a3,c#4,f#4] [a2,e3,b3,c#4] [e3,g#3,b3,c#4]>"
);
const heatPad = heatChords
.sound("supersaw")
.lpf("<900 1120 1460 1080>")
.lpq(2)
.attack(0.07)
.decay(0.34)
.sustain(0.32)
.release(0.85)
.room(0.24)
.roomsize(3)
.orbit(2)
.gain(0.065);
// Short, high-register attacks occupy the space above an imagined vocal.
const heatUpperGuitar = note(`<
[c#5 ~ a4 [~ e5] ~ c#5 ~ ~]
[a4 ~ f#4 [a4 c#5] ~ a4 ~ ~]
[b4 ~ e5 [~ c#5] ~ b4 a4 ~]
[g#4 ~ b4 [c#5 b4] ~ g#4 ~ ~]
>`)
.sound("sawtooth")
.hpf(520)
.lpf("<2500 2200 2800 2350>")
.lpenv(2.2)
.lpdecay(0.08)
.decay(0.105)
.sustain(0)
.release(0.12)
.pan("<0.27 0.73>")
.delay(0.12)
.delaytime(0.19)
.room(0.16)
.orbit(3)
.gain(0.078);
// A darker answer below the vocal range creates the octave-split illusion.
const heatLowerGuitar = note(`<
[f#3 ~ c#4 ~ a3 ~ [c#4 a3] ~]
[d3 ~ a3 ~ f#3 ~ [a3 f#3] ~]
[a2 ~ e3 ~ c#3 ~ [e3 b2] ~]
[e3 ~ b3 ~ g#3 ~ [b3 g#3] ~]
>`)
.sound("triangle")
.hpf(170)
.lpf(1450)
.decay(0.14)
.sustain(0)
.release(0.16)
.pan("<0.69 0.31>")
.orbit(3)
.gain(0.055);
// Syncopation and octave jumps make the bass a phrase, not four held roots.
const heatBass = note(`<
[f#1 ~ f#2 ~ ~ c#2 e2 ~]
[d2 ~ d2 ~ a1 ~ c#2 ~]
[a1 ~ a2 ~ ~ e2 b1 c#2]
[e2 ~ e2 ~ b1 ~ [c#2 e2] ~]
>`)
.sound("sine")
.lpf(260)
.attack(0.012)
.decay(0.24)
.sustain(0.4)
.release(0.24)
.orbit(5)
.gain(0.22);
// This new melody appears only at chorus scale.
const heatHook = note(`<
[c#5 ~ e5 [f#5 e5] ~ c#5 ~ a4]
[a4 ~ c#5 ~ [e5 c#5] a4 ~ f#4]
[b4 ~ c#5 e5 ~ [c#5 b4] a4 ~]
[g#4 ~ b4 [c#5 e5] ~ b4 g#4 ~]
>`)
.sound("square")
.vowel("<a e i o>")
.hpf(430)
.lpf(2050)
.attack(0.018)
.decay(0.16)
.sustain(0.08)
.release(0.26)
.delay(0.17)
.delaytime(0.24)
.room(0.34)
.roomsize(4)
.orbit(4)
.gain(0.044);
// The verse pocket avoids the straight lo-fi kick/snare template.
const heatKick = s("bd ~ ~ [~ bd] ~ ~ bd ~")
.n("<4 5 4 6>")
.speed("<1 0.985 1.01 1>")
.duckorbit("2:3:4")
.duckattack(0.2)
.duckdepth("0.18:0.1:0.08")
.gain(0.48);
const heatKickFill = s("bd ~ [~ bd] ~ bd ~ [bd bd] [~ bd]")
.n("4 4 5 4 6 4")
.duckorbit("2:3:4")
.duckattack(0.2)
.duckdepth("0.2:0.12:0.1")
.gain(0.5);
const heatBackbeat = s("~ ~ [sd:1,cp:1] ~ ~ ~ [sd:1,cp:0] ~")
.hpf(190)
.late(0.006)
.gain(0.31);
const heatHats = s("hh*8")
.n("0 1 2 1 3 1 2 1")
.hpf(5600)
.gain("[0.075 0.15]*4");
const heatPerc = s("~ rim ~ [~ rim] ~ ~ [rim ~] ~")
.n("<1 2 1 3>")
.hpf(900)
.pan("<0.25 0.75>")
.gain(0.16);
const heatVerseDrums = stack(
cat(heatKick, heatKick, heatKick, heatKickFill),
heatBackbeat,
heatHats,
heatPerc
);
const heatLiftDrums = stack(
heatKick,
heatBackbeat,
cat(
s("hh*8"),
s("hh*8").gain(0.17),
s("hh*16").gain("[0.055 0.13]*8"),
s("hh*16").gain("[0.065 0.15]*8")
).n("0 1 2 3").hpf(5400),
s("<~ ~ ~ [lt mt ht sd]>").gain(0.34)
);
const heatChorusKick = s("bd ~ [~ bd] ~ bd ~ [bd ~] [~ bd]")
.n("4 5 4 6")
.duckorbit("2:3:4")
.duckattack(0.24)
.duckdepth("0.28:0.17:0.13")
.gain(0.53);
const heatChorusDrums = stack(
cat(heatChorusKick, heatChorusKick, heatChorusKick, heatKickFill),
heatBackbeat.gain(0.37),
s("hh*16").n("0 1 2 1").hpf(5800).gain("[0.055 0.125]*8"),
s("~ oh ~ [~ oh]").n("1 2").hpf(4200).gain(0.16),
s("<cr ~ ~ ~>").hpf(700).gain(0.28)
);
// A filtered noise breath makes the four-bar lift audible as a transition.
const heatAir = sound("white")
.hpf("<9000 7000 5200 3600>")
.lpf(12500)
.attack(0.5)
.decay(1.05)
.sustain(0)
.pan("<0.2 0.8>")
.orbit(6)
.gain("<0.004 0.007 0.011 0.017>");
const heatIntro = stack(
heatPad.lpf("<430 560 730 920>").gain(0.042),
heatUpperGuitar.gain("<0.06 0.07 0.078 0.085>"),
arrange([2, silence], [2, heatPerc.gain(0.11)])
);
const heatVerse = stack(
heatPad,
heatUpperGuitar,
heatBass,
heatVerseDrums,
arrange([4, silence], [4, heatLowerGuitar])
);
const heatLift = stack(
heatPad.lpf("<1250 1550 1900 2400>").gain(0.082),
heatLowerGuitar.gain(0.067),
heatBass.gain(0.245),
heatLiftDrums,
heatAir
);
const heatChorus = stack(
heatPad.lpf(2650).gain(0.088),
heatPad.add(note(12)).lpf(3300).gain(0.025),
heatUpperGuitar.gain(0.092),
heatLowerGuitar.gain(0.064),
heatBass.gain(0.255),
heatHook,
heatChorusDrums
);
const heatBreak = stack(
heatPad.lpf(640).gain(0.038),
heatLowerGuitar.gain(0.063),
heatBass.gain(0.18),
arrange([2, silence], [2, stack(heatKick, heatBackbeat.gain(0.24))]),
arrange([2, heatHook.gain(0.03)], [2, silence])
);
const heatFinal = stack(
heatChorus,
heatHook.add(note(12)).gain(0.018),
s("<cr ~ ~ ~>").hpf(600).gain(0.34)
);
arrange(
[4, heatIntro], // bars 1-4: filtered intro
[8, heatVerse], // bars 5-12: pocket and gradual second guitar
[4, heatLift], // bars 13-16: hats open and filters rise
[8, heatChorus], // bars 17-24: full drums, hook, and wider harmony
[4, heatBreak], // bars 25-28: kick dropout and reset
[4, heatFinal] // bars 29-32: compact final chorus
).cpm(82 / 4)
Listen past the first four bars: the verse brings in the sampled rhythm, its second half adds the low guitar answer, and the lift doubles the hat rate before the hook arrives. The kick also ducks the pad and guitar effect orbits more deeply in the chorus, which makes the section breathe without simply turning every layer up.
Youth: bright, bouncing psychedelia
Bayley explained that the guitar on Youth began as a rough laptop-microphone recording that he processed until it barely sounded like a guitar. His percussion often comes from homemade recordings and is treated as another melodic hook, according to his Universal Audio interview. He also turned an owl recording into a playable synth for the track, as described in his Coup De Main interview.
This A Dorian piece is intentionally different from the first arrangement. The harmony is clipped into off-beat stabs, the kick uses a busier dancehall-like pocket, and shaker, rim, cowbell, and tom phrases behave like little melodies. The bent lead is an original synthetic call rather than an imitation of the song’s vocal line.
samples('github:tidalcycles/dirt-samples');
// Am7 -> D9 -> Cmaj7 -> Em7 supplies minor color with a bright Dorian lift.
const youthChords = note(
"<[a3,c4,e4,g4] [d3,f#3,a3,c4,e4] [c3,e3,g3,b3] [e3,g3,b3,d4]>"
);
// struct() turns held harmony into a syncopated, breathing part.
const youthStabs = youthChords
.struct("x ~ [~ x] ~")
.sound("triangle")
.lpf("<1450 1900 1650 1320>")
.lpenv(1.7)
.lpdecay(0.11)
.attack(0.012)
.decay(0.19)
.sustain(0.11)
.release(0.23)
.room(0.14)
.roomsize(2.5)
.orbit(2)
.gain(0.092);
// A clipped, band-limited synth suggests heavily transformed guitar.
const youthGuitar = note(`<
[e5 ~ a4 [c5 b4] ~ e5 ~ ~]
[f#5 ~ a4 [c5 a4] ~ f#5 ~ ~]
[e5 ~ g4 [b4 g4] ~ e5 ~ d5]
[b4 ~ g4 [d5 b4] ~ e5 ~ ~]
>`)
.sound("sawtooth")
.hpf(690)
.lpf("<2350 2850 2500 2050>")
.lpenv(2.6)
.lpdecay(0.065)
.decay(0.078)
.sustain(0)
.release(0.09)
.crush(10)
.pan("<0.72 0.28>")
.phaser(0.65)
.phaserdepth(0.22)
.room(0.1)
.orbit(3)
.gain(0.052);
const youthBass = note(`<
[a1 ~ a2 ~ [~ e2] g2 ~ a1]
[d2 ~ a1 ~ [d2 e2] f#2 ~ a1]
[c2 ~ g1 ~ [~ c2] e2 g2 ~]
[e2 ~ b1 ~ [e2 d2] b1 ~ e2]
>`)
.sound("sine")
.lpf(310)
.attack(0.009)
.decay(0.2)
.sustain(0.33)
.release(0.17)
.orbit(5)
.gain(0.205);
// A quiet mid-bass edge makes small speakers reveal the syncopation.
const youthBassEdge = youthBass
.sound("square")
.hpf(95)
.lpf(620)
.decay(0.13)
.sustain(0.08)
.gain(0.027);
const youthCall = note(`<
[e5 ~ [a5 g5] ~ e5 ~ c5 ~]
[f#5 ~ e5 [~ a5] ~ f#5 ~ d5]
[g5 ~ [e5 d5] ~ b4 ~ e5 ~]
[b4 ~ d5 [e5 ~] g5 e5 ~ ~]
>`)
.sound("triangle")
.penv("<5 8 3 6>")
.pdecay(0.09)
.lpf(2850)
.attack(0.012)
.decay(0.16)
.sustain(0.06)
.release(0.24)
.delay(0.13)
.delaytime(0.16)
.room(0.26)
.roomsize(3.5)
.orbit(4)
.gain(0.061);
const youthAnswer = note(`<
[~ c5 ~ e5 ~ [g5 e5] ~ ~]
[~ a4 ~ c5 ~ [e5 c5] ~ ~]
[~ b4 ~ d5 ~ [e5 g5] ~ ~]
[~ g4 ~ b4 ~ [d5 b4] ~ ~]
>`)
.sound("sine")
.penv(10)
.pdecay(0.045)
.decay(0.09)
.sustain(0)
.pan("<0.22 0.78>")
.delay(0.09)
.orbit(4)
.gain(0.04);
const youthKick = s("bd ~ [~ bd] ~ ~ bd ~ [bd ~]")
.n("1 3 2 4")
.speed("<1 1.015 0.99 1>")
.duckorbit("2:3:4")
.duckattack(0.15)
.duckdepth("0.14:0.09:0.07")
.gain(0.46);
const youthKickTurn = s("bd ~ [bd bd] ~ ~ [bd ~] [~ bd] bd")
.n("1 2 3 4")
.duckorbit("2:3:4")
.duckattack(0.16)
.duckdepth("0.17:0.11:0.09")
.gain(0.48);
const youthBackbeat = s("~ ~ [sd:2,cp:1] ~ ~ ~ [sd:3,cp:0] ~")
.hpf(240)
.gain(0.29);
const youthShaker = s("sh*16")
.n("0 1 3 2")
.hpf(4100)
.pan("[0.36 0.64]*8")
.gain("[0.035 0.075 0.045 0.095]*4");
// Rim and cowbell trade places instead of acting as background dust.
const youthPerc = stack(
s("~ rim ~ [~ rim]").n("1 2 3").hpf(850).gain(0.18),
s("~ ~ cb ~ ~ [cb ~] ~ ~").n("0 1 2").hpf(620).gain(0.11)
);
const youthDrums = stack(
cat(youthKick, youthKick, youthKick, youthKickTurn),
youthBackbeat,
s("hh*8").n("0 2 1 3").hpf(5900).gain("[0.065 0.135]*4"),
youthPerc
);
const youthLiftDrums = stack(
youthKick,
youthBackbeat,
youthShaker,
cat(
silence,
s("~ ~ ~ [lt mt]").gain(0.25),
s("~ ~ [lt mt] [ht sd]").gain(0.29),
s("[lt ~] [mt mt] [ht ht] [sd cp]").gain(0.32)
)
);
const youthChorusDrums = stack(
cat(youthKick, youthKickTurn, youthKick, youthKickTurn),
youthBackbeat.gain(0.34),
youthShaker,
s("hh*8").n("0 2 1 3").hpf(6100).gain("[0.055 0.12]*4"),
s("~ oh ~ [~ oh]").n("0 1").hpf(3900).gain(0.13),
s("<cr ~ ~ ~>").gain(0.25)
);
const youthIntro = stack(
youthGuitar.gain(0.043),
youthStabs.lpf(980).gain(0.058),
youthPerc.gain(0.105),
arrange([2, silence], [2, youthKick.gain(0.31)])
);
const youthGroove = stack(
youthStabs,
youthGuitar,
youthBass,
youthBassEdge,
youthDrums,
arrange([4, silence], [4, youthAnswer.gain(0.032)])
);
const youthLift = stack(
youthStabs.lpf("<1700 2100 2550 3100>").gain(0.105),
youthGuitar.gain(0.061),
youthBass.gain(0.225),
youthLiftDrums,
youthCall.gain(0.048)
);
const youthChorus = stack(
youthStabs.lpf(3150).gain(0.112),
youthStabs.add(note(12)).lpf(3700).gain(0.027),
youthGuitar.gain(0.066),
youthBass.gain(0.235),
youthBassEdge.gain(0.034),
youthCall,
youthChorusDrums,
arrange([4, silence], [4, youthAnswer])
);
// The bridge keeps the percussion hooks but removes the expected backbeat.
const youthBridge = stack(
youthBass.gain(0.18),
cat(youthGuitar, youthGuitar.rev(), youthGuitar, youthGuitar.rev()),
youthPerc.gain(0.2),
youthShaker.gain(0.065),
arrange([3, silence], [1, s("[lt mt] [ht ht] [sd cp] cr").gain(0.32)])
);
const youthFinal = stack(
youthChorus,
youthAnswer.add(note(12)).gain(0.02),
s("<cr ~ ~ ~>").gain(0.31)
);
arrange(
[4, youthIntro], // bars 1-4: processed guitar and percussion clues
[8, youthGroove], // bars 5-12: main pocket; answer enters halfway
[4, youthLift], // bars 13-16: shaker and tom build
[8, youthChorus], // bars 17-24: lead call and brighter chord stabs
[4, youthBridge], // bars 25-28: backbeat drops out
[4, youthFinal] // bars 29-32: full-color return
).cpm(96 / 4)
Here the obvious change arrives before the melody: the harmony stops sustaining and begins answering the percussion. During the lift, sixteenth-note shaker and increasing tom fills raise the density. The bridge then removes the backbeat and reverses every second guitar bar before the final four-bar return.
Reuse the style without copying the songs
Keep the contrast between intimate and expansive sections more carefully than any individual sound:
- Begin with one processed pluck or chord texture.
- Give each section a different rhythmic role, not merely a louder mix.
- Introduce sub-bass only after the groove is legible.
- Save the brightest hook, wider register, and busiest percussion for a payoff.
- Drop an expected element before the final return.
- Build a new melody from a short rhythmic phrase rather than borrowing the melody of either reference track.
Strudel measures time in cycles. Here one cycle is one four-beat bar, so
.cpm(82 / 4) produces 82 BPM and .cpm(96 / 4) produces 96 BPM. The official
documentation shows the same ideas through
arrange,
struct,
mini-notation, and
separate effect orbits.