Making a track

Showcase panel 21: silent until clicked: a Synth note (x sets the pitch), a Pluck, and a bar of Kick, Snare and HiHat in a Sequencer with its playhead and a Reverb on the master, with master.spectrum() and master.level() drawn live; needs kiln-gl[sound].

Showcase panel 21: silent until clicked: a Synth note (x sets the pitch), a Pluck, and a bar of Kick, Snare and HiHat in a Sequencer with its playhead and a Reverb on the master, with master.spectrum() and master.level() drawn live; needs kiln-gl[sound].
Try it live: run uv run python examples/showcase.py, then click panel 21, or see the showcase.

The pages before this one each gave you a piece: a synth, a sequencer, drums, effects and visuals that listen. This page puts them together into a whole track: one minute of melodic techno in the style of Stephan Bodzin, with a rolling bass, a hypnotic arpeggio whose filter opens over half the track, dark pads, and a kick that makes everything breathe. Listen first:

And this is the sketch playing it, examples/19_melodic_techno.py, a few bars into the drop:

melodic-techno-live

The glowing core thumps with each kick. The spectrum fans out around it as rays, and the ring is the arpeggio: the bar’s 16 steps go round like a clock, each note further out the higher it is, and the step you hear is lit. Along the bottom is the arrangement, with the playhead.

You need the sound extra: pip install "kiln-gl[sound]".

The plan

Before any sound, the plan. It’s 122 beats per minute in A minor, and 32 bars of four beats make 63 seconds. The chords are the classic dark loop i–VI–III–VII: Am, F, C and G, two bars each. And the track is cut into sections, each a few bars long:

section bars what plays
intro 4 kick and hats
build 8 the bass and the arpeggio come in, then a clap; its filter starts to open; a riser
breakdown 4 no kick: pads and the arpeggio, then a riser into…
drop 12 everything
outro 4 the parts leave, the kick and hats play out

The groove

The drums are kiln’s drum machine, Kick, HiHat, Clap and Cymbal, played by a Sequencer, one pattern each. The kick is a 909-style one: a fast, deep pitch drop with some drive, and a dark plate behind it for the long rumble. Closed hats play the off-16ths and open hats the off-beat 8ths. All hats are one choke group, so each open hat is cut short by the closed one a 16th later, a “tss-t”. The clap is on 2 and 4.

import kiln

kick = kiln.Kick()
kick.tune, kick.drop, kick.drop_time = 47, 3.5, 0.035
kick.decay, kick.click, kick.tone, kick.drive = 0.9, 0.35, 0.55, 0.45
kick.fx = [kiln.Reverb(size=0.5, decay=1.4, damping=0.9, mix=0.2)]   # the rumble
kick.volume = 0.8
hat, open_hat, clap = kiln.HiHat(), kiln.HiHat(open=True), kiln.Clap()
hat.volume, open_hat.decay, open_hat.volume = 0.35, 0.35, 0.3
clap.decay, clap.volume = 0.35, 0.35
clap.fx = [kiln.Reverb(size=0.6, decay=1.2, damping=0.4, mix=0.2)]

seq = kiln.Sequencer(bpm=122)
seq.pattern(kick,     "x...x...x...x...")
seq.pattern(hat,      ".x.x.x.x.x.x.x.X")
seq.pattern(open_hat, "..x...x...x...x.")
seq.pattern(clap,     "....x.......x...")

with record():
    seq.start()
    wait(2 * 4 * 60 / 122)    # two bars
    seq.stop()
    wait(0.5)

waveform of the sound

The rolling bass

The bass is the engine of the track. It is two detuned saws and a sub through the ladder filter, with a short filter envelope that plucks each note. It rests on the beat, because that belongs to the kick, and rolls through the three 16ths after: the root twice, then an octave up. The Sequencer takes MIDI note numbers as well as names, so the line is easy to build in Python:

bass = kiln.Synth(polyphony=1)
bass.osc2_level, bass.detune, bass.sub_level = 0.5, 6, 0.35
bass.cutoff, bass.resonance, bass.drive = 900, 0.4, 0.6
bass.filter_env_amount, bass.filter_decay, bass.filter_sustain = 0.6, 0.12, 0.2
bass.attack, bass.decay, bass.sustain, bass.release = 0.002, 0.18, 0.45, 0.06
bass.volume = 1.0

A1 = 33
roll = [None, A1, A1, A1 + 12] * 4        # one bar: rest on the beat, roll after it
seq.notes(bass, roll)

with record(view="spectrogram"):
    seq.start()
    wait(2 * 4 * 60 / 122)
    seq.stop()
    wait(0.5)

spectrogram of the sound

Turn the drums down rather than the synths up. A synth voice at full volume sits about 14 dB below full scale, which keeps chords from clipping, while the drums hit harder. So the drums carry volumes below 1, and the bass plays at 1.

The arpeggio

This is the hook, and the part that makes the track hypnotic. It plays 16ths on the chord tones, cycling six of them against a bar of sixteen: the cycle lands somewhere new on every beat, and comes back round every three bars. The 7th and 15th 16ths of each bar jump an octave. The sound is a Moog-like lead, two saws into the resonant ladder, through a ping-pong Delay three 16ths long, so every note answers itself from the other side.

The slow move is the filter. seq.ramp turns a control from one value to another over a number of bars, starting on the next bar line. In the track the arpeggio’s cutoff opens from 250 Hz to 4200 Hz over 24 bars, across the build, the breakdown and the drop. Here it opens over two:

arp = kiln.Synth(polyphony=3)
arp.osc2_level, arp.detune = 0.6, -8
arp.resonance, arp.drive = 0.55, 0.4
arp.filter_env_amount, arp.filter_decay, arp.filter_sustain = 1.5, 0.15, 0.1
arp.attack, arp.decay, arp.sustain, arp.release = 0.002, 0.25, 0.3, 0.15
arp.fx = [
    kiln.Delay(time=3 * 60 / 122 / 4, feedback=0.45, tone=0.5, ping_pong=True, mix=0.3),
    kiln.Reverb(size=0.7, decay=2.5, damping=0.5, mix=0.15),
]
arp.volume = 0.9

am = [57, 60, 64, 69]                     # A3, C4, E4, A4
cycle = [0, 2, 3, 1, 3, 2]
line = [am[cycle[i % 6]] + (12 if i % 16 in (6, 14) else 0) for i in range(32)]  # 7th, 15th
seq.notes(arp, line)
seq.ramp(arp, "cutoff", 250, 4000, bars=2)

with record(view="spectrogram", stereo=True):
    seq.start()
    wait(2 * 4 * 60 / 122)
    seq.stop()
    wait(1.0)

spectrogram of the sound

Listen to the arpeggio brighten. On its own, its brightness (the spectral centroid) climbs from about 900 Hz in the first half bar to 3200 Hz in the last. Listen in headphones for the echoes bouncing left and right.

Dark pads

The pads play chords, four notes at once. seq.chords is notes for chords: each step is a list of notes, a rest (None) or "-" to hold the chord on. All of a chord’s notes start on the same sample. With gate=1 a chord is held right up to the next one, and its 2.5 s release rings on underneath it. That is why the pad has 8 voices, four sounding and four fading. The voicings are sevenths kept above the bass’s octaves, so they shade the track without muddying the low end, and a large plate Reverb gives them their space.

pad = kiln.Synth(polyphony=8)
pad.detune, pad.cutoff, pad.resonance = 9, 700, 0.2
pad.lfo_rate, pad.lfo_to_cutoff = 0.15, 0.4     # a slow breath in the filter
pad.attack, pad.decay, pad.sustain, pad.release = 1.0, 1.0, 0.8, 2.5
pad.fx = [kiln.Reverb(size=0.85, decay=4.0, damping=0.5, mix=0.35)]
pad.volume = 0.17                                     # under the arpeggio

am7, fmaj7 = [57, 60, 64, 67], [53, 57, 60, 64]
seq.chords(pad, [am7] + ["-"] * 15 + [fmaj7] + ["-"] * 15, gate=1.0)   # a bar each

with record():
    seq.start()
    wait(2 * 4 * 60 / 122)
    seq.stop()
    wait(2.0)

waveform of the sound

The arrangement is the patterns

Now the trick that turns loops into a track. A pattern doesn’t have to be one bar long: each instrument’s steps loop on their own length. So each part gets one long line of steps, the whole track, built section by section from the bars above, with rests where the part sits out. The sequencer then plays the arrangement, to the sample, on its own. In the example, Track builds those lines from small functions, drum_bar(part, section, bar), synth_bar(...) and pad_bar(...), which say what a part plays in each bar of each section:

lines = {part: [] for part in PARTS}
for name, bars in [("intro", 4), ("build", 8), ("breakdown", 4), ("drop", 12), ("outro", 4)]:
    for bar in range(bars):
        lines["kick"].append(drum_bar("kick", name, bar))     # "x...x...x...x..." or rests
        lines["bass"].extend(synth_bar("bass", name, bar, step))
seq.pattern(kick, "".join(lines["kick"]))                     # 512 steps: the whole kick part
seq.notes(bass, lines["bass"])
seq.chords(pad, lines["pad"], gate=1.0)                       # the chords, the same way

Here is the whole plan, two characters per bar: ## where a part plays, . where it rests.

          intro   build           break   drop                    outro   
kick      ########################. . . . ################################
hat       ########################. . . . ################################
open_hat  . . . . . . . . ########. . . . ########################. . . . 
clap      . . . . . . ############. . . . ############################. . 
ride      . . . . . . . . . . . . . . . . ############################. . 
bass      . . . . ################. . . . ############################. . 
arp       . . . . ####################################################. . 
pad       . . . . . . . . . . . . ####################################. . 
riser     . . . . . . . . . . ####. . ####. . . . . . . . . . . . . . . . 

The slow moves go on the sequencer too. seq.ramp(..., at_bar=N) places a ramp on bar N, counted from start(): it begins on that bar’s first sample and runs for bars bars. The track’s moves are a small table per section: the arpeggio’s filter, the risers’ sweep and swell, and the arpeggio’s and pads’ level in the breakdown and the drop. A level held through a section is a ramp from a value to itself. Track places them all before it starts, and nothing has to happen on time in draw():

AUTOMATION = {
    "build": [("arp", "cutoff", 250, 1100, 0, 8), ("riser", "volume", 0, 0.32, 6, 2), ...],
    "breakdown": [("arp", "volume", 0.7, 0.7, 0, None), ...],   # None: to the section's end
    ...
}
for name, first, bars in sections:
    for part, control, start, end, at, length in AUTOMATION.get(name, []):
        length = bars - at if length is None else length
        seq.ramp(instruments[part], control, start, end, bars=length, at_bar=first + at)

Live, the track loops: once per time through, Track.update() places the next pass’s ramps a whole pass ahead.

The mix

Three things make the mix. The first is gain staging: every part is measured on the steps it plays, in its own band (the bass from 90 to 600 Hz, the hats above 7 kHz), against the whole mix in that band. Each part comes out within 5 dB of the whole band there, so you hear it. The pads are kept low (volume 0.16 in the drop), because louder they buried the bass.

The second is the pump. The master limiter normally only catches stray peaks at -1 dBFS, and kiln’s instruments leave plenty of headroom below that. The track raises kiln.master.gain, the level of the whole mix going into the limiter, by 13.5 dB, so the kicks reach the ceiling. Each kick turns the whole mix down a few dB and it springs back in a tenth of a second. That is the breathing of the genre, with no sidechain to wire up:

kiln.master.gain = 13.5                 # into the limiter: the mix ducks under each kick
seq.notes(bass, [])                     # just the kick and a held chord, to hear the dip
seq.chords(pad, [])
seq.notes(arp, [])
seq.pattern(hat, "")
seq.pattern(open_hat, "")
seq.pattern(clap, "")
pad.volume = 0.3                        # alone, it stays under the ceiling
for note in [57, 60, 64, 67]:
    pad.note_on(440 * 2 ** ((note - 69) / 12))
wait(1.0)                               # the chord swells in first

dips = []
with record():
    seq.start()
    for _ in range(16):                 # one bar, a 16th at a time
        wait(60 / 122 / 4)
        dips.append(kiln.master.limiter.reduction)   # the deepest cut since the last read
    seq.stop()
    pad.note_off()
    wait(0.5)
kiln.master.gain = 0.0
print("limiter, per 16th (dB):", " ".join(f"{d:4.1f}" for d in dips))

waveform of the sound
limiter, per 16th (dB): -4.7 -2.6 -1.4 -0.4 -1.2 -0.8 -0.4 -0.1 -1.1 -0.9 -0.8 -0.4 -1.7 -1.6 -0.5 -0.1

The printout is the pump: on each kick (every fourth 16th) the limiter turns everything down, by most on the first, and lets go again before the next kick.

The third is contrast. The breakdown steps back (the arpeggio a little quieter, the risers soft), so it measures 7 dB below the drop, and the drop lands. The effects are sends in spirit: each instrument has its own, and an effect’s mix is how much of the instrument goes into it. The arpeggio has the delay, the pads and the clap have plates, and the kick and bass stay dry and centred.

Drawing on the beat

The picture reads the same clock. seq.beat is where the music is as you hear it, and seq.phase is how far through the beat. So (1 - phase) ** 3 is a pulse that peaks on every beat. It only thumps when the kick’s own line has a hit there, so the core rests in the breakdown:

step = int(track.beat * 4)                       # the 16th being heard
on_kick = track.lines["kick"][step - step % 4] != "."
pulse = (1 - track.seq.phase) ** 3 if on_kick else 0.0
radius = 60 + 50 * pulse

The arpeggio’s ring uses the same step: the notes of this bar are lines["arp"] from the bar’s first step, and the one at step is lit.

Run it yourself (press space to stop and start; the track loops):

uv run python examples/19_melodic_techno.py
uv run python examples/19_melodic_techno.py --render track.mp3    # the track, to a file

Try this

  • Change ARP_FIGURE in the example, [0, 2, 3, 1, 3, 2], to a five-note cycle: it comes back round every five bars, and the hook changes shape.
  • Put the pads on a Chorus before the plate, for a wider, more Juno-like sound.
  • Swap the order of the sections in ARRANGEMENT, or add a second breakdown: the lines, the cues and the timeline at the bottom of the picture all follow.