Subsystem

Controllers, bends and slides

By the end of this chapter, the pad's filter opens across the build, from dark to bright, and the bass slides up into its top note, in the manner of an acid bass line.

The piece so far

Everything Patterns that listen to each other built, without its render. The pad and bass 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

import subsequence.sequence_utils

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=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=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)

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)

@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)

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.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")
	steps = [note.position // 6 for note in p.placed()]
	p.data["drum_syncopation"] = subsequence.sequence_utils.syncopation(steps, p.grid)
	p.groove(swing)

A controller that moves

MIDI carries more than notes. A control change, CC, sets one of an instrument's controls to a value from 0 to 127. CC 74 is the one the MIDI specification names brightness, and many synths send it to the filter's cutoff, so that is the control this chapter moves. Check which CC your instrument's filter answers to, and use that number if it differs.

A third signal gives the cutoff its path across the build, as the swell does for the pad's velocity:

composition.conductor.line("cutoff", start_val=20, end_val=127, duration_beats=32, start_beat=16)

The pad, rewritten to sweep the filter:

@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")))
	p.cc_ramp(74, int(p.signal("cutoff")), int(p.conductor.get("cutoff", (p.bar + 1) * p.bar_beats)))

A bass that slides

A slide bends a note's pitch into the next note instead of jumping, the sound of an acid bass line. The bass, rewritten with one line more:

@composition.pattern(channel=2, beats=4, min_energy=0.5)
def bass (p, chord):
	busy = p.data.get("drum_syncopation", 0.0) > 0.5
	if busy:
		p.arpeggio(chord, root=notes.E1, count=1, spacing=1)
	else:
		p.arpeggio(chord, root=notes.E1, count=4, spacing=0.25)
	p.groove(swing)
	if not busy:
		p.slide(notes=[3, 7, 11, 15], time=0.5, bend_range=12)

composition.render(bars=24, filename="controllers-bends-and-slides.mid")

In piece.py, put the cutoff line with the other signals, and replace pad and bass with these.

What moved

The pad sends the cutoff in every bar, one control change after another, and the summary reads each stretch as one line: held at 20 through the intro, rising to 127 across the build, and held there after it:

MIDI channel 3
    1  1.000  CC 74 value 20, repeated until   5  1.125 in 392 messages
    5  1.125  CC 74 value 20, ramping to 127 at  12  4.875 in 770 changes
   12  4.875  CC 74 value 127, repeated until  24  4.958 in 1167 messages

In bar 6, where the bass plays its arpeggio, each beat's third note bends up into the swung top note, and the bend returns to 0 as that note starts:

MIDI channel 2
    6  1.000  note G1 (31)  velocity 100  length 0.250
    6  1.292  note B1 (35)  velocity 100  length 0.250
    6  1.500  note D2 (38)  velocity 100  length 0.292
    6  1.625  pitch bend 0, ramping to 3413 at   6  1.792 in 5 changes
    6  1.792  note G2 (43)  velocity 100  length 0.250
    6  1.792  pitch bend 0

More expression