Subsystem Search

Shaping over time

By the end of this chapter, the pad swells from quiet to full across the build, the closed hi-hats breathe in and out every four bars, and the tempo rises through the build into the drop and settles back in the breakdown.

The piece so far

Everything Sections and form built, without its render. The pad and hats patterns are left out, because this chapter rewrites them.

import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums
import subsequence.constants.midi_notes as notes

composition = subsequence.Composition(bpm=120, key="E", scale="minor", seed=1)
swing = subsequence.Groove.swing(percent=57)
composition.harmony(style="aeolian_minor")
melody_state = subsequence.MelodicState(low=notes.E4, high=notes.E5, rest_probability=0.2)
hook = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
answered = subsequence.period(hook)

composition.form(subsequence.Form([
	subsequence.Section("intro", 4, energy=0.2),
	subsequence.Section("build", 8, energy=0.5),
	subsequence.Section("drop", 8, energy=1.0),
	subsequence.Section("breakdown", 4, energy=0.3),
]), loop=True)

composition.transition(before="drop", mute=["drums", "hats"])

@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP, min_energy=0.5)
def drums (p):
	p.hit_steps("kick", [0, 4, 8, 12], velocity=110)
	p.hit_steps("snare", [4, 12], velocity=100)
	p.ghost_fill("snare", density=0.5, velocity=(20, 40), bias="before")
	p.groove(swing)

@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP, min_energy=1.0)
def euclidean_snare (p):
	p.euclidean("snare_2", pulses=5, velocity=60).rotate(2).groove(swing)

@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP, min_energy=0.5)
def fill (p):
	if p.section.last_bar:
		p.sequence(steps=[12, 13, 14, 15], pitches=["high_tom", "high_mid_tom", "low_tom", "low_floor_tom"], velocities=[100, 90, 90, 110])
	p.groove(swing)

@composition.pattern(channel=10, beats=0.75, reschedule_lookahead=0.75, drum_note_map=gm_drums.GM_DRUM_MAP, min_energy=0.5)
def pedal_hats (p):
	p.hit_steps("hi_hat_pedal", [0], velocity=80)

@composition.pattern(channel=2, beats=4, min_energy=0.5)
def bass (p, chord):
	p.arpeggio(chord, root=notes.E1, count=4, spacing=0.25)
	p.groove(swing)

@composition.pattern(channel=4, beats=4, min_energy=0.3)
def melody (p, chord):
	p.melody(melody_state, spacing=0.5, chord_tones=chord.tones(notes.E4))

@composition.pattern(channel=5, bars=4, min_energy=1.0)
def hook_line (p):
	p.phrase(answered, root=notes.E5)

Signals

A section switches parts on and off. Some things should change gradually instead, over many bars. The composition's conductor holds signals for that: named values that change over time, which any pattern can read.

composition.conductor.line("swell", start_val=40, end_val=90, duration_beats=32, start_beat=16)
composition.conductor.lfo("breath", shape="sine", cycle_beats=16, min_val=60, max_val=100)

Reading a signal

p.signal reads a signal at the start of the bar a pattern is building. The pad and the hi-hats, rewritten to read them:

@composition.pattern(channel=3, beats=4, voice_leading=True)
def pad (p, chord):
	p.chord(chord, root=notes.E3, sustain=True, velocity=int(p.signal("swell")))

@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP, min_energy=0.2)
def hats (p):
	p.hit_steps("hi_hat_closed", range(16))
	p.velocity_shape(low=40, high=int(p.signal("breath")))
	p.groove(swing)

int turns a signal's value, which can have a fraction, into the whole number a velocity has to be. The pad now plays at the swell's value, and the hi-hats' loudest notes reach the breath's.

Because a signal is read once a bar, an LFO needs a cycle of several bars to be heard moving. Read at the start of each bar, a sine wave four bars long gives 80, 100, 80 and 60 in turn. One a bar long would give the same value every bar.

The tempo, section by section

composition.target_bpm moves the tempo gradually to a new one over a number of bars. To start that at the right moment, give composition.on_section a function of your own, which Subsequence calls each time a section begins:

def tempo (section):
	if section is not None and section.name == "build":
		composition.target_bpm(126, bars=8)
	elif section is not None and section.name == "breakdown":
		composition.target_bpm(120, bars=4)

composition.on_section(tempo)

composition.render(bars=24, filename="shaping-over-time.mid")

In piece.py, put the two signals and tempo with its on_section line below the transition, and replace pad and hats with these.

What changed over time

The tempo, as the file states it: 120 at the start, a ramp up to 126 across the build, and a ramp back down in the breakdown.

tempo 120 bpm from   1  1.000
tempo 120.01 bpm from   4  4.042, ramping to 126 bpm at  12  4.000 in 768 changes
tempo 125.98 bpm from  20  4.042, ramping to 120 bpm at  24  4.000 in 384 changes

The pad's chords get louder bar by bar through the build, and stay at full strength in the drop:

MIDI channel 3
    5  1.000  note E3 (52)  velocity 40  length 4.000
    7  1.000  note E3 (52)  velocity 52  length 4.000
    9  1.000  note E3 (52)  velocity 65  length 4.000
   11  1.000  note E3 (52)  velocity 77  length 4.000
   13  1.000  note C3 (48)  velocity 90  length 4.000

More signals