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)
in_nineteenis a composition of its own, so the piece incompositionkeeps the tuning it has.composition.tuningtunes every part of a composition except its drums, andequal=19divides the octave into nineteen equal steps.reference_note=notes.E4keeps E4 where it is, and counts the steps from there.- A note number now counts steps of nineteen.
notes.E4 + 19is the octave above E4, and+ 5and+ 11are the minor third and the fifth, where twelve steps to the octave would take+ 3and+ 7.notes.G4is still three note numbers above E4, and three steps of nineteen make less than a whole tone.
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")
notes.E4 - 19is E3, nineteen steps below E4.notes.E4 - 14andnotes.E4 - 8are five and eleven steps above E3: its minor third and its fifth.p.apply_tuningtunes one pattern, and the composition's tuning then leaves that pattern alone.subsequence.Tuning.equalmakes the same nineteen steps as a tuning to hand to it.channels=[3, 7, 8]spreads the chord's notes across MIDI channels 3, 7 and 8, counted from 1 as a pattern'schannelis.- Give the MIDI channels to the pattern that plays chords, as here. Given to
composition.tuninginstead, they serve every part whose notes overlap, and a part that plays one note at a time, such as the lead, keeps its own MIDI channel.
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
""")
pathlib.Path("just.scl").write_textwrites the text into a file calledjust.scl, beside the script. Three quotation marks at each end let the text run over several lines.- A line that starts with
!is a comment. The first other line describes the tuning, and the next says how many notes follow. - Each note is a line: a ratio such as
6/5, or cents, written with a decimal point, counted from the note the tuning starts on, which the file leaves out. The last,2/1, is the octave, where the notes start again.
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")
in_just.tuning("just.scl", ...)reads the composition's tuning from the file, andsubsequence.Tuning.from_sclreads the same file into a tuning for the pad's ownp.apply_tuning.- This tuning has twelve notes to the octave, so note numbers keep the meaning they have always had:
notes.G4andnotes.B4are the minor third and the fifth above E4, as in the piece.
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
subsequence.Tuning.from_ratiosandsubsequence.Tuning.from_centsmake a tuning from a list, without a file, andcomposition.tuningtakes the same lists asratiosandcents.exclude_drums=Falsemakescomposition.tuningtune the drums too, which it otherwise leaves alone.