Subsystem Search

Patterns that listen to each other

By the end of this chapter, the drums measure how syncopated each bar of theirs is, and the bass listens: when the drums are busy on the weak sixteenth notes, it plays one long root note a beat and leaves them room, and when they are plain, it plays its full arpeggio.

The piece so far

Everything Shaping over time built, without its render. The drums 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

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

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)

Shared data

Patterns so far have known about the piece, its bar, its section and its chord, but not about each other. p.data is a place any pattern can write to and any pattern can read from: one dictionary, shared by the whole composition, which Subsequence also offers as composition.data. A dictionary holds values under names, written p.data["name"] = value and read p.data["name"].

When two patterns are the same length, the one defined first builds first in every bar, so the one defined after it reads what it wrote in that bar. Here the drums write, so they are defined first.

Measuring a rhythm

subsequence.sequence_utils holds measures of rhythm. syncopation scores a list of steps by how far they sit from the strong beats: a note on the downbeat adds nothing, and a note on one of the weakest sixteenth notes adds the most, and the score is the average over the notes. The drums, rewritten to measure themselves and share the score:

import subsequence.sequence_utils

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

With the kick and snare alone, the score is about a third. Each ghost note on a weak sixteenth note pulls it up, so a bar full of ghost notes scores more than half.

A bass that leaves room

The bass reads the score and decides how to play:

@composition.pattern(channel=2, beats=4, min_energy=0.5)
def bass (p, chord):
	if p.data.get("drum_syncopation", 0.0) > 0.5:
		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)

composition.render(bars=24, filename="patterns-that-listen.mid")

In piece.py, put the sequence_utils import with the others, and replace drums and bass with these, drums before bass.

What the bass did

The drums come in with the build in bar 5, and their first bar has ghost notes on the last sixteenth note before beat 2, before beat 4 and before the next bar, three of the weakest places in a bar, which scores more than half:

MIDI channel 10
    5  1.792  note D2 (38)  velocity 34  length 0.083
    5  2.000  note D2 (38)  velocity 100  length 0.083
    5  3.792  note D2 (38)  velocity 26  length 0.083
    5  4.000  note D2 (38)  velocity 100  length 0.083
    5  4.792  note D2 (38)  velocity 26  length 0.083

So in bar 5 the bass plays a root a beat. Bar 6 is plainer, and the arpeggio comes back:

MIDI channel 2
    5  1.000  note E1 (28)  velocity 100  length 1.000
    5  2.000  note E1 (28)  velocity 100  length 1.000
    5  3.000  note E1 (28)  velocity 100  length 1.000
    5  4.000  note E1 (28)  velocity 100  length 1.000
    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.250
    6  1.792  note G2 (43)  velocity 100  length 0.250

Other measures

subsequence.sequence_utils has other measures of a line, described in its own docstrings rather than in the API reference: rhythmic_evenness scores how evenly a rhythm's notes are spread around the bar, offbeatness counts the notes on off-beat pulses, and vl_distance measures how far the voices move between two chords.