Subsystem

Microtonal tuning, a study

By the end of this chapter, a lead and a pad play in nineteen-tone equal temperament, with nineteen equal steps to the octave instead of twelve, and then in just intonation read from a Scala file. Any instrument that follows pitch bend can play them.

This chapter is a study. A tuning reaches every pitched part of a composition and changes what its note numbers mean, so the study builds compositions of its own and leaves the piece as it is. It needs nothing from the piece but its first lines, the imports, so it can live in a file of its own, such as tuning.py.

The piece so far

Everything A lead driven by the weather built, without its render. This chapter is a study, and changes nothing in it.

import itertools

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

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

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)

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

Nineteen notes to the octave

Twelve-tone equal temperament divides the octave into twelve equal semitones of 100 cents each, and MIDI note numbers count them. Nineteen-tone equal temperament divides the octave into nineteen equal steps of about 63 cents. Its minor third is within a fraction of a cent of a pure minor third, the frequency ratio 6:5, where twelve-tone's is about 16 cents narrow.

A lead, on MIDI channel 6, in a composition of its own:

in_nineteen = subsequence.Composition(bpm=120, seed=1)
in_nineteen.tuning(equal=19, reference_note=notes.E4)

@in_nineteen.pattern(channel=6, beats=4)
def lead (p):
	p.arpeggio([notes.E4, notes.E4 + 5, notes.E4 + 11, notes.E4 + 19], spacing=0.5)

A chord needs a MIDI channel for each note

Subsequence plays a tuning with the notes MIDI already has: it sends each note as the nearest of the twelve, with a pitch bend sent ahead of it that moves it the rest of the way. A pitch bend moves every note sounding on its MIDI channel, so a lead that plays one note at a time can keep one MIDI channel, and notes that sound together each need their own. A pad under the lead, playing E minor an octave lower:

@in_nineteen.pattern(channel=3, beats=4)
def pad (p):
	p.chord([notes.E4 - 19, notes.E4 - 14, notes.E4 - 8], sustain=True)
	p.apply_tuning(subsequence.Tuning.equal(19), reference_note=notes.E4, channels=[3, 7, 8])

in_nineteen.render(bars=4, filename="nineteen-tones.mid")

A MIDI channel for each sounding note is the idea MPE, MIDI Polyphonic Expression, is built on, but the instrument needs no MPE. On a hardware synthesiser, set the same sound on MIDI channels 3, 7 and 8. In a DAW, route the three MIDI channels to three copies of one instrument, or to one instrument that listens on all three.

Each bend is worked out for a pitch bend range of 2 semitones, which Subsequence assumes unless bend_range says otherwise. Set the instruments on MIDI channels 3, 6, 7 and 8 to a pitch bend range of 2 semitones.

What nineteen steps sent

The summary gives each bend as a number from -8192, the bottom of the bend range, to 8191, the top. The lead's minor third goes out as G4 bent up 647, about 16 cents, and its fifth as B4 bent down 216, about 5 cents. The pad's three notes each go out on a MIDI channel of their own, with a bend of their own:

MIDI channel 3
    1  1.000  note E3 (52)  velocity 90  length 4.000
    1  1.000  pitch bend 0
MIDI channel 6
    1  1.000  note E4 (64)  velocity 100  length 0.500
    1  1.000  pitch bend 0
    1  1.500  note G4 (67)  velocity 100  length 0.500
    1  1.500  pitch bend 647
    1  2.000  note B4 (71)  velocity 100  length 0.500
    1  2.000  pitch bend -216
MIDI channel 7
    1  1.000  note G3 (55)  velocity 90  length 4.000
    1  1.000  pitch bend 647
MIDI channel 8
    1  1.000  note B3 (59)  velocity 90  length 4.000
    1  1.000  pitch bend -216

From a Scala file

Scala is a program for experimenting with tunings, and a Scala file, ending .scl, holds one tuning as plain text. Scala's own site offers an archive of them to download. The study writes one, so everything the example needs is on the page. It is just intonation, which tunes each interval to a ratio of whole numbers, such as 3:2 for a pure fifth and 5:4 for a pure major third:

import pathlib

pathlib.Path("just.scl").write_text("""! just.scl
Just intonation, twelve notes
12
!
16/15
9/8
6/5
5/4
4/3
45/32
3/2
8/5
5/3
9/5
15/8
2/1
""")

The same lead and pad in just intonation:

in_just = subsequence.Composition(bpm=120, seed=1)
in_just.tuning("just.scl", reference_note=notes.E4)

@in_just.pattern(channel=6, beats=4)
def lead (p):
	p.arpeggio([notes.E4, notes.G4, notes.B4, notes.E5], spacing=0.5)

@in_just.pattern(channel=3, beats=4)
def pad (p):
	p.chord([notes.E3, notes.G3, notes.B3], sustain=True)
	p.apply_tuning(subsequence.Tuning.from_scl("just.scl"), reference_note=notes.E4, channels=[3, 7, 8])

in_just.render(bars=4, filename="just-intonation.mid")

What just intonation sent

The lead's minor third goes out bent up 641, within a fraction of a cent of nineteen steps' 647, and its fifth bent up 80, about 2 cents, where nineteen steps bent it down:

MIDI channel 6
    1  1.000  note E4 (64)  velocity 100  length 0.500
    1  1.000  pitch bend 0
    1  1.500  note G4 (67)  velocity 100  length 0.500
    1  1.500  pitch bend 641
    1  2.000  note B4 (71)  velocity 100  length 0.500
    1  2.000  pitch bend 80

More tunings