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()
recordedholds Open-Meteo's answer as Python data: under each name, a list of values in the order of the times.temperature_2mis the air temperature two metres up, in degrees Celsius, andwind_speed_10mthe wind speed ten metres up, in kilometres an hour.zippairs the two lists, a temperature with the wind at the same hour, anditertools.cyclehands the pairs out one at a time, going back to the first after the last.- An
EasedValue, fromsubsequence.easing, remembers the last reading and the one before it, so a pattern can move smoothly between them instead of jumping when a new reading arrives.
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)
next(readings)takes the next pair, andtemperature_now, wind_now = ...gives each value of the pair a name of its own.updategives eachEasedValueits new reading.cycle_beats=16reads the next of the recorded day's readings every four bars, so most of the day passes in the piece's first loop.reschedule_lookahead=2runs it two beats before each fourth bar starts, so the reading is in before the patterns build that bar.wait_for_initial=Truereads once before the piece starts, so the first bar already has a reading.async defmakes the function run in step with the music. A render waits for a scheduled function either way,async defor a plaindef, so every render of the piece is the same.
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.
progressruns 0, 0.25, 0.5 and 0.75 across the four bars between readings:%is what is left over after dividing, sop.bar % 4counts 0 to 3 and starts again.temperature.get(progress)is part of the way from the reading before to the latest one, eased so that it starts and ends gently. At 0 it is the reading before. By 0.75 it is most of the way to the latest, and the next reading carries on from there.- The lead's chord is built around a root that moves two semitones for every degree: E4 at 18 degrees, higher when it is warmer, and lower when it is cooler.
- The wind sets the lead's filter through CC 74: six times the wind speed, so about 78 at 13 kilometres an hour and about 97 at 16.
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¤t=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)
urllib.request.urlopenasks Open-Meteo for the current weather, andjson.loadturns its answer into the same kind of data asrecorded, with the latest reading under"current".- This
read_weatheris a plaindef. Asking a server takes time, and a plain function runs alongside the music, so waiting for an answer never holds up the beat. cycle_beats=128asks at most once a minute at any tempo up to 128 BPM, and the piece never goes faster than 126. Open-Meteo's current values change every 15 minutes, so asking more often would bring nothing new, and its free API limits how often it may be asked.progressin the lead assumes a new reading every four bars. With a reading every 128 beats, change the lead's4s to32s to match.