# Chapter 5 · Relative Pitch and Scales In [Chapter 4](04-generators-euclidean) you let generators fill the *rhythm* for you; now we do the same for *pitch* — working in keys and scale degrees instead of raw MIDI numbers, and snapping generated lines onto a scale so they always sound in key. Up to now every pitch has been an exact number: `notes.C2` is `36`, and `36` is always the same semitone. That's fine when you know precisely which note you want. But musicians rarely think "play MIDI 43" — they think "play the fifth, in the key we're in." This chapter teaches Subsequence to think that way too: you describe a *key* and a *scale*, generate or hand-write a line, and let the library keep every note diatonic — most of the time, with the occasional chromatic passing tone when you ask for one. That shift, from absolute numbers to pitches-relative-to-a-key, is the groundwork everything in Part IV (harmony) is built on. ```{testsetup} ch5 # Hidden per-chapter setup: the imports the first example shows in full, so later # blocks can use `subsequence` and `notes` without repeating them. import subsequence import subsequence.constants.midi_notes as notes composition = subsequence.Composition(bpm=120) ``` (sec-keys-and-degrees)= ## 5.1 Keys and scale degrees A **key** is a home note — `"C"`, `"F#"`, `"Bb"` — and a **scale** (or *mode*) is the set of steps you're allowed to walk away from it. Together they pick seven (or five, for a pentatonic) pitch classes out of the twelve, and those are the notes that sound "in tune" with each other. A **scale degree** then names a note by its *position* in that set rather than its absolute pitch: the 1st degree is the root, the 5th is the fifth, and so on. "The fifth in C" is G; "the fifth in E♭" is B♭ — same degree, different note. That indirection is the whole point. You can hand Subsequence a key right at the top, when you create the `Composition`: ```{testcode} ch5 import subsequence import subsequence.constants.midi_notes as notes composition = subsequence.Composition(bpm=120, key="C", scale="minor") ``` The two new arguments are written in plain musical language: ```{list-table} :header-rows: 1 :widths: 26 74 * - Argument - What it does * - `key="C"` - The home note of the piece, as a note name (`"C"`, `"F#"`, `"Bb"`). No octave — a key is a pitch *class*, not one specific C. * - `scale="minor"` - The scale/mode that defines which degrees are in key. Defaults to major (`"ionian"`) when you leave it out. ``` Setting `key` and `scale` on the `Composition` records the *home* the piece lives in. On its own it changes nothing you hear — your `p.note(...)` lines still place exactly the pitches you write. What the key buys you is a reference the pitch tools in this chapter can read, so that "snap this to the scale" or "give me the notes of the scale" know which scale you mean without you repeating it every time. ```{note} **Sharps and flats both work for a key.** `key="F#"` and `key="Gb"` name the same home note; use whichever spelling reads right for the music. (This is unlike the note *constants* from [§3.5](03-notes-beats-durations), which only spell sharps — `notes.FS4`. A key is written as you'd write it on a lead sheet.) ``` ```{tip} Picking a `scale` is a quick way to set a mood before you write a single note: `"minor"` (natural minor) is dark, `"dorian"` is minor-but-hopeful, `"mixolydian"` is bluesy-major, `"major_pentatonic"` can't sound wrong. We list the full set of names in [§5.2](#sec-snap). ``` (sec-snap)= ## 5.2 Snapping to a scale: `snap_to_scale` (pitch, not timing) The single most useful pitch verb is **`p.snap_to_scale`**. It takes every note already placed in the pattern and nudges each one to the nearest pitch that belongs to a scale. Generate a line however you like — a random walk, a melody mapped from sensor data, or just a hand-written line with a wrong note in it — then snap it, and it lands in key. Its shape is: ```python p.snap_to_scale(key, mode, strength=1.0) ``` - **`key`** — the root note name (`"C"`, `"F#"`, `"Bb"`). - **`mode`** — the scale name (default `"ionian"`, i.e. major). - **`strength`** — the probability, `0.0`–`1.0`, that *each* note is snapped. Here is a line written deliberately "out" — a chromatic climb that ignores the key entirely — snapped into C minor so every note becomes diatonic: ```{testcode} ch5 import subsequence composition = subsequence.Composition(bpm=120) @composition.pattern(channel=1, beats=4) def melody(p): chromatic = [60, 61, 63, 66, 68, 70] # a climb with several off-key notes for i, pitch in enumerate(chromatic): p.note(pitch, beat=i * 0.5, duration=0.5) p.snap_to_scale("C", "minor") # every note is now in C minor composition.render(bars=2, filename="snapped.mid") ``` Like the articulation verbs in [§3.4](03-notes-beats-durations), `snap_to_scale` acts on notes **already placed**, so you call it *after* the `p.note(...)` lines. It edits pitches in place and leaves everything else — beats, durations, velocities — untouched. A note that is already in the scale is left exactly where it is. ```{important} **`snap_to_scale` changes PITCH, never timing.** It's the pitch counterpart of the *timing* verb `swing` from [Chapter 4](04-generators-euclidean): `swing` moves *when* a note sounds; `snap_to_scale` moves *which note* sounds. They are completely separate axes. If you want a line that's both in key and grooving, you snap its pitches **and** swing its timing — two independent calls. (Older drafts of Subsequence overloaded the word "quantize" for pitch; that is retired. In this guide pitch is always `snap_to_scale`, and "quantize" only ever means *timing* — snapping note onsets onto the grid, a job you'll meet much later.) ``` ### How a note chooses where to land When a note is off the scale, `snap_to_scale` moves it to the *nearest* scale tone. If it sits exactly halfway between two scale tones — a tritone gap — the **upward** note wins. That has one consequence worth seeing, because it occasionally surprises people: in C major, C♯ is one semitone above C and one below D, a tie, so it snaps **up to D**, not down to C. ```{list-table} Snapping into C major (pitch classes C D E F G A B) :header-rows: 1 :widths: 30 30 40 * - Off-scale note - Snaps to - Why * - C♯ (61) - D (62) - Tie between C and D → upward wins * - D♯ (63) - E (64) - Tie between D and E → upward wins * - F♯ (66) - G (67) - Tie between F and G → upward wins ``` ### Most notes diatonic, a few chromatic: `strength` At the default `strength=1.0`, *every* note snaps and the line is purely diatonic. Lower it and only *some* notes snap — the rest keep their original, possibly chromatic, pitch. This is how you get a line that's mostly in key but still has the occasional spicy passing tone, instead of a sterile, scrubbed-clean melody. `strength=0.6` means each note independently has a 60% chance of being snapped. The choice uses the pattern's seeded random number generator, so it's repeatable — pass a `seed=` to pin it down exactly: ```{testcode} ch5 import subsequence composition = subsequence.Composition(bpm=120) @composition.pattern(channel=1, beats=4) def coloured_line(p): run = [60, 61, 62, 63, 64, 65, 66, 67] # a full chromatic run for i, pitch in enumerate(run): p.note(pitch, beat=i * 0.5, duration=0.5) p.snap_to_scale("C", "ionian", strength=0.6, seed=42) # mostly C major… composition.render(bars=2, filename="coloured.mid") ``` With that exact seed the run `60, 61, 62, 63, 64, 65, 66, 67` comes out as `60, 62, 62, 64, 64, 65, 66, 67` — the off-key 61 and 63 were pulled into the scale, but 66 (F♯) escaped the snap and stays as a chromatic passing tone. Change the seed and a different selection survives; raise the strength toward `1.0` and fewer escape. ```{tip} Think of `strength` as a "how strict is the bouncer" dial. `1.0` lets nobody off-key in; `0.8` is a sensible setting for a melody that should read as in-key but still feel human and surprising; `0.0` is the door wide open (nothing changes). Reach for the lower values *after* generating a line, to colour it. ``` ### The mode names you can use The `mode` argument accepts any of these built-in names. The seven-note modes have chord qualities defined (so they'll work with the harmony engine in Part IV too); the pentatonic and non-western scales are pitch-only, which is all `snap_to_scale` needs: ```{list-table} :header-rows: 1 :widths: 34 66 * - Family - Names * - Major / minor - `"ionian"` (= `"major"`), `"aeolian"` (= `"minor"`) * - Other church modes - `"dorian"`, `"phrygian"`, `"lydian"`, `"mixolydian"`, `"locrian"` * - Minor variants - `"harmonic_minor"`, `"melodic_minor"` * - Pentatonic - `"major_pentatonic"`, `"minor_pentatonic"` * - Non-western - `"hirajoshi"`, `"in_sen"`, `"iwato"`, `"yo"`, `"egyptian"` ``` ```{note} `"major"` is just an alias for `"ionian"`, and `"minor"` for `"aeolian"` (natural minor) — write whichever reads more naturally. If you pass a name Subsequence doesn't know, it raises a `ValueError` that lists the valid names, so a typo fails loudly rather than silently doing the wrong thing. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.pattern_builder.PatternBuilder.snap_to_scale` ``` (sec-scale-notes)= ## 5.3 Scale note lists: `scale_notes` `snap_to_scale` *corrects* pitches you've already placed. Sometimes you want the opposite: a ready-made **list of the in-scale notes** in a register, to choose from as you build a line — a bass that only ever picks scale tones, an arpeggio drawn from the scale, a random melody that can't go wrong. That list comes from **`subsequence.scale_notes`**. It's a top-level function (not a method on `p`), so you call it with `subsequence.scale_notes(...)`. Its shape: ```python subsequence.scale_notes(key, mode, low, high, count) ``` - **`key`** — the root, as a note name. - **`mode`** — the scale name (same set as [§5.2](#sec-snap)); defaults to `"ionian"`. - **`low`** / **`high`** — the MIDI range to draw from, *inclusive*. - **`count`** — *instead of* `high`, ask for an exact number of notes ascending from `low`, climbing through octaves as needed. Asked for a range, it returns every scale tone between `low` and `high`: ```{testcode} ch5 import subsequence import subsequence.constants.midi_notes as notes scale = subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5) print(scale) ``` ```{testoutput} ch5 [60, 62, 64, 65, 67, 69, 71, 72] ``` That's C major from middle C up to the C an octave above — the white keys, as MIDI numbers. Because the result is just a plain list of numbers, you use it like any other step/pitch list from earlier chapters. Here a bass walks up the first few scale tones, picking them by *index* into the list rather than by absolute pitch: ```{testcode} ch5 import subsequence import subsequence.constants.midi_notes as notes composition = subsequence.Composition(bpm=120) # The notes of E natural minor across the bass register, named once. E_MINOR = subsequence.scale_notes("E", "minor", low=notes.E2, high=notes.E3) @composition.pattern(channel=1, beats=4) def scale_bass(p): # E_MINOR[0] is the root, [2] the third, [4] the fifth — degrees by index. p.note(E_MINOR[0], beat=0, duration=1.0) # root p.note(E_MINOR[2], beat=1, duration=1.0) # third up p.note(E_MINOR[4], beat=2, duration=1.0) # fifth up p.note(E_MINOR[0], beat=3, duration=1.0) # back home composition.render(bars=4, filename="scale-bass.mid") ``` The `count` form is handy when you want a fixed-size palette regardless of register — say "give me eight notes of A minor pentatonic starting at A3", climbing into the next octave automatically: ```{testcode} ch5 import subsequence import subsequence.constants.midi_notes as notes pool = subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=8) print(pool) ``` ```{testoutput} ch5 [57, 60, 62, 64, 67, 69, 72, 74] ``` ```{warning} **`low` must be a note that's actually in the scale**, or `scale_notes` silently starts from the next in-scale pitch above it. Asking for `scale_notes("E", "minor", low=60, ...)` starts on C (60 is in E minor, but it's not the root), so your "first note" won't be the root you expected. The safe habit: pass a `low` whose note name matches the `key`, e.g. `low=notes.E2` for `key="E"`. Then `scale[0]` is always the root. ``` ```{tip} A scale-note list pairs beautifully with the generators from [Chapter 4](04-generators-euclidean) and with `random`. Build the pool once with `scale_notes`, then let a generator pick *indices* into it — every choice is guaranteed in key, because the pool only ever contained scale tones. You get generative variety with zero wrong notes. ``` ```{admonition} Reference :class: seealso {py:func}`~subsequence.intervals.scale_notes` ``` (sec-custom-scales)= ## 5.4 Custom scales: `register_scale` The built-in list in [§5.2](#sec-snap) covers most music, but you may want a scale it doesn't ship — a raga, a synthetic mode, a folk scale. **`register_scale`** adds one by name, after which it works everywhere a built-in name does: in `snap_to_scale`, in `scale_notes`, and as a `Composition` `scale=`. ```python subsequence.register_scale(name, intervals) ``` - **`name`** — the name you'll use later (e.g. `"raga_bhairav"`). It must *not* clash with a built-in name — you can't redefine `"minor"`. - **`intervals`** — the scale's steps as **semitone offsets from the root**, a list that starts at `0`, ascends strictly, and stays within `0`–`11`. You register a scale once, near the top of your script, then use it like any other. Here is Raga Bhairav — a seven-note scale with a flat 2nd and flat 6th — registered and immediately snapped onto a generated line: ```{testcode} ch5 import subsequence # Register once, up front. Semitone offsets from the root: 0 1 4 5 7 8 11. subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11]) composition = subsequence.Composition(bpm=120) @composition.pattern(channel=1, beats=4) def raga_line(p): climb = [60, 61, 62, 63, 64, 65, 66, 67] # a chromatic run again… for i, pitch in enumerate(climb): p.note(pitch, beat=i * 0.5, duration=0.5) p.snap_to_scale("C", "raga_bhairav") # …pulled into the custom raga composition.render(bars=2, filename="raga.mid") ``` And the same name works in `scale_notes`, so you can build a pool from your custom scale exactly as in [§5.3](#sec-scale-notes): ```{testcode} ch5 import subsequence import subsequence.constants.midi_notes as notes pool = subsequence.scale_notes("C", "raga_bhairav", low=notes.C3, count=7) print(pool) ``` ```{testoutput} ch5 [48, 49, 52, 53, 55, 56, 59] ``` ```{important} **The interval list has rules, and breaking them raises a `ValueError` rather than failing quietly.** It must start with `0` (the root), every step must be strictly larger than the last, and all values must be `0`–`11` (one octave). So `[0, 2, 4, 7, 9]` is a valid major-pentatonic-ish scale; `[0, 0, 4]` (repeat), `[2, 4, 7]` (no root), and `[0, 5, 13]` (out of range) all raise. Whole numbers only — semitones, not fractions. ``` ```{note} A custom scale you register is pitch-only by default, which is everything `snap_to_scale` and `scale_notes` need. (`register_scale` also takes an optional `qualities=` argument that maps a chord to each degree — that only matters for the harmony engine in Part IV, so we leave it off here.) ``` ```{admonition} Reference :class: seealso {py:func}`~subsequence.intervals.register_scale` ``` (sec-resolved-late)= ## 5.5 A pitch is a specification resolved late Step back from the verbs for a moment, because this section is the *idea* the whole chapter has been circling — and it's the bridge into harmony. When you write `p.note(60, beat=0)`, it's tempting to think you've committed to a sound: middle C, full stop. But look at what `snap_to_scale` actually did. The number `60` you wrote was not the final sound — it was a **specification**, a request that only got its real musical meaning when a key and scale were applied, *later*, at the moment the bar was built. The same written line came out as different notes depending on the key you snapped it into. Here is that made concrete. The identical raw line `[60, 62, 64, 66, 67]` is snapped into two different keys; watch the *third note* land in two different places: ```{testcode} ch5 import subsequence # The SAME written line, snapped into two different keys, sounds different. for key in ("C", "Eb"): composition = subsequence.Composition(bpm=120) sounded = {} @composition.pattern(channel=1, beats=4) def line(p, _key=key, _out=sounded): raw = [60, 62, 64, 66, 67] for i, pitch in enumerate(raw): p.note(pitch, beat=i * 0.5, duration=0.5) p.snap_to_scale(_key, "ionian") # Peek at the pitches the bar actually ended up with, just to print them. _out["pitches"] = [ n.pitch for step in sorted(p._pattern.steps) for n in p._pattern.steps[step].notes ] composition.render(bars=1, filename="resolved.mid") print(key, "->", sounded["pitches"]) ``` ```{testoutput} ch5 C -> [60, 62, 64, 67, 67] Eb -> [60, 62, 65, 67, 67] ``` The written specification was *byte-for-byte identical* both times. Under C major the off-scale 66 resolved to 67; under E♭ major the in-between 64 resolved to 65. **Same spec, different sound** — because the pitch was resolved against the key *late*, when the bar was built, not when you typed the number. (That little loop over `p._pattern.steps` is just so we can *print* the resolved pitches here; you'd never reach inside `p` like that in real music — you'd simply let the notes play. It's a peek behind the curtain to make the point visible.) This is worth naming carefully, because it's two different notions of "the same" that musicians juggle without thinking: ```{list-table} :header-rows: 1 :widths: 30 70 * - Kind of "same" - What it means * - **Structural equality** - Two things are written the *same way* — the same specification, the same degree, the same instruction. The two `raw` lines above are structurally identical. * - **Sonic equality** - Two things *sound the same* — the same actual pitches come out. The two lines above are **not** sonically equal, because the key resolved them differently. ``` A raw MIDI number like `36` is the rare case where structural and sonic equality coincide: `36` is always the same C, resolved or not. The moment you describe pitch *relative to a key* — which is what `snap_to_scale` and a `Composition(key=...)` let you do — the two come apart, and that gap is a feature. It's what lets you write one line and transpose a whole piece by changing one `key=`, or write a bass that follows wherever the music goes. ```{note} **You've only seen the gentle version of this.** Here the "context" a pitch is resolved against is just the *key and scale* of the piece. The full power arrives in Part IV, where the context becomes the *current chord*: a stored degree can mean the chord's root in one bar and its third in the next, as the harmony moves underneath it. The principle is identical — a pitch is a specification resolved late — but the thing it's resolved against gets richer. Keep this idea in your pocket; harmony is where it pays off. ``` --- You can now work in keys and scales instead of bare numbers: set a key on the `Composition`, snap any line diatonic (with `strength` for chromatic colour), build note pools with `scale_notes`, and add your own scales with `register_scale` — all resting on the idea that a pitch is a specification resolved late. Next we turn on the chord engine, so that "context" becomes a living harmony your patterns can follow bar by bar.