Subsystem Search

A lead driven by the weather

By the end of this chapter, a lead plays in the drop that follows a day of London weather: it climbs as the temperature rises and brightens as the wind picks up, moving smoothly from one reading to the next.

The piece so far

Everything Controllers, bends and slides built, without its render. This chapter adds a pattern and changes none.

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)

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

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

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

Weather, as data

The weather comes from Open-Meteo, a weather service whose forecast API answers a plain web address with a reading, and needs no account or key. Its free API is for non-commercial use only, and its data is published under the Creative Commons Attribution 4.0 licence, which asks for a credit wherever the data is used: Weather data by Open-Meteo.com.

A guide cannot check an example against today's weather, which will be different tomorrow. So the piece plays a day that has already happened, recorded from Open-Meteo: central London on 15 September 2026, every third hour from three in the morning. The live request, one swap away, is at the end of the chapter.

import itertools
import subsequence.easing

# Weather data by Open-Meteo.com, CC BY 4.0: central London, 15 September 2026.
recorded = {
	"time": ["2026-09-15T03:00", "2026-09-15T06:00", "2026-09-15T09:00", "2026-09-15T12:00", "2026-09-15T15:00", "2026-09-15T18:00", "2026-09-15T21:00"],
	"temperature_2m": [18.1, 17.8, 17.7, 19.8, 22.3, 21.7, 18.4],
	"wind_speed_10m": [13.0, 14.4, 15.5, 16.2, 13.7, 13.0, 15.8],
}
readings = itertools.cycle(zip(recorded["temperature_2m"], recorded["wind_speed_10m"]))

temperature = subsequence.easing.EasedValue()
wind = subsequence.easing.EasedValue()

Reading it on a schedule

composition.schedule runs a function of your own every so many beats while the piece plays:

async def read_weather ():
	temperature_now, wind_now = next(readings)
	temperature.update(temperature_now)
	wind.update(wind_now)

composition.schedule(read_weather, cycle_beats=16, reschedule_lookahead=2, wait_for_initial=True)

A lead that follows it

The lead, on MIDI channel 6, plays in the drop:

@composition.pattern(channel=6, beats=4, min_energy=1.0)
def lead (p, chord):
	progress = (p.bar % 4) / 4
	p.arpeggio(chord, root=int(notes.E4 + 2 * (temperature.get(progress) - 18)), count=3, spacing=0.5)
	p.cc(74, int(wind.get(progress) * 6))

composition.render(bars=24, filename="a-lead-driven-by-the-weather.mid")

In piece.py, put the imports with the others, recorded to wind and read_weather with its schedule line below the signals, and lead below the other patterns. Give a lead instrument MIDI channel 6.

What the weather did

The drop, bars 13 to 20, hears the day warm up: noon's 19.8 degrees arrives for bar 13, and three o'clock's 22.3 for bar 17. Bars 13 and 16 both play C major. Between them the eased temperature rises by nearly two degrees, from 17.7, and the chord built nearest the higher root is an octave up. The filter opens a little as the wind rises:

MIDI channel 6
   13  1.000  note C4 (60)  velocity 100  length 0.500
   13  1.000  CC 74 value 93
   13  1.500  note E4 (64)  velocity 100  length 0.500
   13  2.000  note G4 (67)  velocity 100  length 0.500
   16  1.000  note C5 (72)  velocity 100  length 0.500
   16  1.000  CC 74 value 96
   16  1.500  note E5 (76)  velocity 100  length 0.500
   16  2.000  note G5 (79)  velocity 100  length 0.500

Live weather

To play today's weather instead, replace recorded, readings and read_weather, and the schedule line, with these. latitude and longitude in the address choose where; these are central London's.

Example not checked: it is not run when this site is built.

import json
import urllib.request

WEATHER = "https://api.open-meteo.com/v1/forecast?latitude=51.51&longitude=-0.13&current=temperature_2m,wind_speed_10m"

def read_weather ():
	with urllib.request.urlopen(WEATHER, timeout=10) as response:
		current = json.load(response)["current"]
	temperature.update(current["temperature_2m"])
	wind.update(current["wind_speed_10m"])

composition.schedule(read_weather, cycle_beats=128, reschedule_lookahead=2, wait_for_initial=True)