subsequence.intervals
Interval and scale definitions, plus the helpers that resolve them.
Holds INTERVAL_DEFINITIONS (named scales and chords as semitone lists) and
the functions that work against it — scale_notes, scale_pitch_classes,
quantize_pitch, register_scale and friends.
1"""Interval and scale definitions, plus the helpers that resolve them. 2 3Holds ``INTERVAL_DEFINITIONS`` (named scales and chords as semitone lists) and 4the functions that work against it — ``scale_notes``, ``scale_pitch_classes``, 5``quantize_pitch``, ``register_scale`` and friends. 6""" 7 8import logging 9import typing 10 11import subsequence.chords 12 13 14logger = logging.getLogger(__name__) 15 16 17INTERVAL_DEFINITIONS: typing.Dict[str, typing.List[int]] = { 18 "augmented": [0, 3, 4, 7, 8, 11], 19 "augmented_7th": [0, 4, 8, 10], 20 "augmented_triad": [0, 4, 8], 21 "blues_scale": [0, 3, 5, 6, 7, 10], 22 "chromatic": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 23 "diminished_7th": [0, 3, 6, 9], 24 "diminished_triad": [0, 3, 6], 25 "dominant_7th": [0, 4, 7, 10], 26 "dominant_9th": [0, 4, 7, 10, 14], 27 "dorian_mode": [0, 2, 3, 5, 7, 9, 10], 28 "double_harmonic": [0, 1, 4, 5, 7, 8, 11], 29 "enigmatic": [0, 1, 4, 6, 8, 10, 11], 30 "half_diminished_7th": [0, 3, 6, 10], 31 "harmonic_minor": [0, 2, 3, 5, 7, 8, 11], 32 "hungarian_minor": [0, 2, 3, 6, 7, 8, 11], 33 "locrian_mode": [0, 1, 3, 5, 6, 8, 10], 34 "lydian": [0, 2, 4, 6, 7, 9, 11], 35 "lydian_dominant": [0, 2, 4, 6, 7, 9, 10], 36 "major_6th": [0, 4, 7, 9], 37 "major_7th": [0, 4, 7, 11], 38 "major_9th": [0, 4, 7, 11, 14], 39 "major_ionian": [0, 2, 4, 5, 7, 9, 11], 40 "major_pentatonic": [0, 2, 4, 7, 9], 41 "major_triad": [0, 4, 7], 42 "melodic_minor": [0, 2, 3, 5, 7, 9, 11], 43 "minor_6th": [0, 3, 7, 9], 44 "minor_7th": [0, 3, 7, 10], 45 "minor_9th": [0, 3, 7, 10, 14], 46 "minor_blues": [0, 3, 5, 6, 7, 10], 47 "minor_major_7th": [0, 3, 7, 11], 48 "minor_pentatonic": [0, 3, 5, 7, 10], 49 "minor_triad": [0, 3, 7], 50 "mixolydian": [0, 2, 4, 5, 7, 9, 10], 51 "natural_minor": [0, 2, 3, 5, 7, 8, 10], 52 "neapolitan_major": [0, 1, 3, 5, 7, 9, 11], 53 "phrygian_dominant": [0, 1, 4, 5, 7, 8, 10], 54 "phrygian_mode": [0, 1, 3, 5, 7, 8, 10], 55 "power_chord": [0, 7], 56 "superlocrian": [0, 1, 3, 4, 6, 8, 10], 57 "sus2": [0, 2, 7], 58 "sus4": [0, 5, 7], 59 "whole_tone": [0, 2, 4, 6, 8, 10], 60 # -- Non-western / pentatonic scales -- 61 "hirajoshi": [0, 2, 3, 7, 8], 62 "in_sen": [0, 1, 5, 7, 10], 63 "iwato": [0, 1, 5, 6, 10], 64 "yo": [0, 2, 5, 7, 9], 65 "egyptian": [0, 2, 5, 7, 10], 66 "root": [0], 67 "fifth": [0, 7], 68 "minor_3rd": [0, 3], 69 "tritone": [0, 6], 70} 71 72 73# --------------------------------------------------------------------------- 74# Diatonic chord quality constants. 75# 76# Each list contains 7 chord quality strings, one per scale degree (I–VII). 77# These can be paired with the corresponding scale intervals from 78# INTERVAL_DEFINITIONS to build diatonic Chord objects for any key. 79# --------------------------------------------------------------------------- 80 81# -- Church modes (rotations of the major scale) -- 82 83IONIAN_QUALITIES: typing.List[str] = [ 84 "major", "minor", "minor", "major", "major", "minor", "diminished" 85] 86 87DORIAN_QUALITIES: typing.List[str] = [ 88 "minor", "minor", "major", "major", "minor", "diminished", "major" 89] 90 91PHRYGIAN_QUALITIES: typing.List[str] = [ 92 "minor", "major", "major", "minor", "diminished", "major", "minor" 93] 94 95LYDIAN_QUALITIES: typing.List[str] = [ 96 "major", "major", "minor", "diminished", "major", "minor", "minor" 97] 98 99MIXOLYDIAN_QUALITIES: typing.List[str] = [ 100 "major", "minor", "diminished", "major", "minor", "minor", "major" 101] 102 103AEOLIAN_QUALITIES: typing.List[str] = [ 104 "minor", "diminished", "major", "minor", "minor", "major", "major" 105] 106 107LOCRIAN_QUALITIES: typing.List[str] = [ 108 "diminished", "major", "minor", "minor", "major", "major", "minor" 109] 110 111# -- Non-modal scales -- 112 113HARMONIC_MINOR_QUALITIES: typing.List[str] = [ 114 "minor", "diminished", "augmented", "minor", "major", "major", "diminished" 115] 116 117MELODIC_MINOR_QUALITIES: typing.List[str] = [ 118 "minor", "minor", "augmented", "major", "major", "diminished", "diminished" 119] 120 121 122# Map mode/scale names to (interval_key, qualities) for use by helpers. 123# qualities is None for scales without predefined chord mappings — these 124# can still be used with scale_pitch_classes() and p.snap_to_scale(), but not 125# with diatonic_chords() or composition.harmony(). 126SCALE_MODE_MAP: typing.Dict[str, typing.Tuple[str, typing.Optional[typing.List[str]]]] = { 127 # -- Western diatonic modes (7-note, with chord qualities) -- 128 "ionian": ("major_ionian", IONIAN_QUALITIES), 129 "major": ("major_ionian", IONIAN_QUALITIES), 130 "dorian": ("dorian_mode", DORIAN_QUALITIES), 131 "phrygian": ("phrygian_mode", PHRYGIAN_QUALITIES), 132 "lydian": ("lydian", LYDIAN_QUALITIES), 133 "mixolydian": ("mixolydian", MIXOLYDIAN_QUALITIES), 134 "aeolian": ("natural_minor", AEOLIAN_QUALITIES), 135 "minor": ("natural_minor", AEOLIAN_QUALITIES), 136 "locrian": ("locrian_mode", LOCRIAN_QUALITIES), 137 "harmonic_minor": ("harmonic_minor", HARMONIC_MINOR_QUALITIES), 138 "melodic_minor": ("melodic_minor", MELODIC_MINOR_QUALITIES), 139 # -- Non-western and pentatonic scales (no chord qualities) -- 140 "hirajoshi": ("hirajoshi", None), 141 "in_sen": ("in_sen", None), 142 "iwato": ("iwato", None), 143 "yo": ("yo", None), 144 "egyptian": ("egyptian", None), 145 "major_pentatonic": ("major_pentatonic", None), 146 "minor_pentatonic": ("minor_pentatonic", None), 147} 148 149# Backwards-compatible alias. 150DIATONIC_MODE_MAP = SCALE_MODE_MAP 151 152 153# Snapshot of every built-in scale name, taken at import time. register_scale() 154# refuses to overwrite these so a custom scale can never silently change what 155# "minor" or "hirajoshi" means mid-composition. 156_BUILTIN_SCALE_NAMES: typing.FrozenSet[str] = frozenset(INTERVAL_DEFINITIONS) | frozenset(SCALE_MODE_MAP) 157 158 159def scale_pitch_classes (key_pc: int, mode: str = "ionian") -> typing.List[int]: 160 161 """ 162 Return the pitch classes (0–11) that belong to a key and mode. 163 164 Parameters: 165 key_pc: Root pitch class (0 = C, 1 = C#/Db, …, 11 = B). 166 mode: Scale mode name. Supports all keys of ``DIATONIC_MODE_MAP`` 167 (e.g. ``"ionian"``, ``"dorian"``, ``"minor"``, ``"harmonic_minor"``). 168 169 Returns: 170 Pitch classes in scale-degree order, starting from the root 171 (length varies by mode). Values wrap mod-12, so the list is 172 not numerically sorted for non-C roots. 173 174 Example: 175 ```python 176 # C major pitch classes 177 scale_pitch_classes(0, "ionian") # → [0, 2, 4, 5, 7, 9, 11] 178 179 # A minor pitch classes 180 scale_pitch_classes(9, "aeolian") # → [9, 11, 0, 2, 4, 5, 7] (mod-12) 181 ``` 182 """ 183 184 if mode not in SCALE_MODE_MAP: 185 raise ValueError( 186 f"Unknown mode '{mode}'. Available: {sorted(SCALE_MODE_MAP)}. " 187 "Use register_scale() to add custom scales." 188 ) 189 190 scale_key, _ = SCALE_MODE_MAP[mode] 191 intervals = get_intervals(scale_key) 192 return [(key_pc + i) % 12 for i in intervals] 193 194 195def scale_notes ( 196 key: str, 197 mode: str = "ionian", 198 low: int = 60, 199 high: int = 72, 200 count: typing.Optional[int] = None, 201) -> typing.List[int]: 202 203 """Return MIDI note numbers for a scale within a pitch range. 204 205 Parameters: 206 key: Scale root as a note name (``"C"``, ``"F#"``, ``"Bb"``, etc.). 207 This acts as a **pitch-class filter only** — it determines which 208 semitone positions (0–11) are valid members of the scale, but does 209 not affect which octave notes are drawn from. Notes are selected 210 starting from ``low`` upward; ``key`` controls *which* notes are 211 kept, not where the sequence starts. To guarantee the first 212 returned note is the root, ``low`` must be a MIDI number whose 213 pitch class matches ``key``. When starting from an arbitrary MIDI 214 number, derive the key name with 215 ``subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]``. 216 mode: Scale mode name. Supports all keys of :data:`SCALE_MODE_MAP` 217 (e.g. ``"ionian"``, ``"dorian"``, ``"natural_minor"``, 218 ``"major_pentatonic"``). Use :func:`register_scale` for custom scales. 219 low: Lowest MIDI note (inclusive). When ``count`` is set, this is 220 the starting note from which the scale ascends. **If ``low`` is 221 not a member of the scale defined by ``key``, it is silently 222 skipped** and the first returned note will be the next in-scale 223 pitch above ``low``. 224 high: Highest MIDI note (inclusive). Ignored when ``count`` is set. 225 count: Exact number of notes to return. Notes ascend from ``low`` 226 through successive scale degrees, cycling into higher octaves 227 as needed. When ``None`` (default), all scale tones between 228 ``low`` and ``high`` are returned. 229 230 Returns: 231 Sorted list of MIDI note numbers. 232 233 Examples: 234 ```python 235 import subsequence 236 import subsequence.constants.midi_notes as notes 237 238 # C major: all tones from middle C to C5 239 subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5) 240 # → [60, 62, 64, 65, 67, 69, 71, 72] 241 242 # E natural minor (aeolian) across one octave 243 subsequence.scale_notes("E", "aeolian", low=notes.E2, high=notes.E3) 244 # → [40, 42, 43, 45, 47, 48, 50, 52] 245 246 # 15 notes of A minor pentatonic ascending from A3 247 subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=15) 248 # → [57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88, 91] 249 250 # Misalignment: key="E" but low=C4 — first note is C, not E 251 subsequence.scale_notes("E", "minor", low=60, count=4) 252 # → [60, 62, 64, 66] (C D E F# — all in E natural minor, but starts on C) 253 254 # Fix: derive key name from root_pitch so low is always in the scale 255 root_pitch = 64 # E4 256 key = subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12] # → "E" 257 subsequence.scale_notes(key, "minor", low=root_pitch, count=4) 258 # → [64, 66, 67, 69] (E F# G A — starts on the root) 259 ``` 260 """ 261 262 key_pc = subsequence.chords.key_name_to_pc(key) 263 pcs = set(scale_pitch_classes(key_pc, mode)) 264 265 if count is not None: 266 if not pcs: 267 return [] 268 result: typing.List[int] = [] 269 pitch = low 270 while len(result) < count and pitch <= 127: 271 if pitch % 12 in pcs: 272 result.append(pitch) 273 pitch += 1 274 return result 275 276 return [p for p in range(low, high + 1) if p % 12 in pcs] 277 278 279def quantize_pitch (pitch: int, scale_pcs: typing.Sequence[int]) -> int: 280 281 """ 282 Snap a MIDI pitch to the nearest note in the given scale. 283 284 Searches outward in semitone steps from the input pitch. When two 285 notes are equidistant (e.g. C# between C and D in C major), the 286 upward direction is preferred. 287 288 Parameters: 289 pitch: MIDI note number to quantize. 290 scale_pcs: Pitch classes accepted by the scale (0–11). Typically 291 the output of :func:`scale_pitch_classes`. 292 293 Returns: 294 A MIDI note number that lies within the scale. 295 296 Example: 297 ```python 298 # Snap C# (61) to C (60) in C major 299 scale = scale_pitch_classes(0, "ionian") # [0, 2, 4, 5, 7, 9, 11] 300 quantize_pitch(61, scale) # → 60 301 ``` 302 """ 303 304 pc = pitch % 12 305 306 if pc in scale_pcs: 307 return pitch 308 309 for offset in range(1, 7): 310 if (pc + offset) % 12 in scale_pcs: 311 return pitch + offset 312 if (pc - offset) % 12 in scale_pcs: 313 return pitch - offset 314 315 # The search radius of ±6 semitones covers every gap in every scale with 316 # no gap wider than one tritone. A wider gap (unusual custom scale) falls 317 # through here and keeps the original off-scale pitch — warn so the caller 318 # knows the result is not actually snapped to the scale. 319 logger.warning( 320 "quantize_pitch: no scale note within ±6 semitones of MIDI %d (pc=%d); " 321 "returning pitch unquantized. scale_pcs=%s", 322 pitch, pc, sorted(scale_pcs), 323 ) 324 return pitch 325 326 327def get_intervals (name: str) -> typing.List[int]: 328 329 """ 330 Return a named interval list from the registry. 331 """ 332 333 if name not in INTERVAL_DEFINITIONS: 334 raise ValueError(f"Unknown interval set: {name}") 335 336 return list(INTERVAL_DEFINITIONS[name]) 337 338 339def register_scale ( 340 name: str, 341 intervals: typing.List[int], 342 qualities: typing.Optional[typing.List[str]] = None 343) -> None: 344 345 """ 346 Register a custom scale for use with ``p.snap_to_scale()`` and 347 ``scale_pitch_classes()``. 348 349 Built-in scale names (e.g. ``"minor"``, ``"hirajoshi"``) cannot be 350 overwritten. Custom names may be re-registered freely — live reload 351 re-runs registration on every save, so this must not raise. 352 353 Parameters: 354 name: Scale name (used in ``p.snap_to_scale(key, name)``). Must not 355 be the name of a built-in scale. 356 intervals: Semitone offsets from the root (e.g. ``[0, 2, 3, 7, 8]`` 357 for Hirajōshi). Must be whole numbers, start with 0, ascend 358 strictly, and stay within 0–11. 359 qualities: Optional chord quality per scale degree (e.g. 360 ``["minor", "major", "minor", "major", "diminished"]``). 361 Required only if you want to use the scale with 362 ``diatonic_chords()`` or ``diatonic_chord_sequence()``. 363 364 Raises: 365 ValueError: If *name* is a built-in scale, or *intervals* / 366 *qualities* fail the rules above. 367 368 Example:: 369 370 import subsequence 371 372 subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11]) 373 374 @comp.pattern(channel=0, length=4) 375 def melody (p): 376 p.note(60, beat=0) 377 p.snap_to_scale("C", "raga_bhairav") 378 """ 379 380 if name in _BUILTIN_SCALE_NAMES: 381 raise ValueError( 382 f"Cannot overwrite built-in scale '{name}'. " 383 "Choose a different name for your custom scale." 384 ) 385 386 if not intervals: 387 raise ValueError("intervals must not be empty") 388 if not all(isinstance(i, int) for i in intervals): 389 raise ValueError("intervals must be whole numbers (semitone offsets)") 390 if intervals[0] != 0: 391 raise ValueError("intervals must start with 0") 392 if any(b <= a for a, b in zip(intervals, intervals[1:])): 393 raise ValueError("intervals must be strictly ascending") 394 if any(i < 0 or i > 11 for i in intervals): 395 raise ValueError("intervals must contain values between 0 and 11") 396 if qualities is not None and len(qualities) != len(intervals): 397 raise ValueError( 398 f"qualities length ({len(qualities)}) must match " 399 f"intervals length ({len(intervals)})" 400 ) 401 402 INTERVAL_DEFINITIONS[name] = intervals 403 SCALE_MODE_MAP[name] = (name, qualities) 404 405 406def get_diatonic_intervals ( 407 scale_notes: typing.List[int], 408 intervals: typing.Optional[typing.List[int]] = None, 409 mode: str = "scale" 410) -> typing.List[typing.List[int]]: 411 412 """ 413 Construct diatonic chords from a scale. 414 """ 415 416 if intervals is None: 417 intervals = [0, 2, 4] 418 419 if mode not in ("scale", "chromatic"): 420 raise ValueError("mode must be 'scale' or 'chromatic'") 421 422 diatonic_intervals: typing.List[typing.List[int]] = [] 423 num_scale_notes = len(scale_notes) 424 425 for i in range(num_scale_notes): 426 427 if mode == "scale": 428 chord = [scale_notes[(i + offset) % num_scale_notes] for offset in intervals] 429 430 else: 431 root = scale_notes[i] 432 chord = [(root + offset) % 12 for offset in intervals] 433 434 diatonic_intervals.append(chord) 435 436 return diatonic_intervals
160def scale_pitch_classes (key_pc: int, mode: str = "ionian") -> typing.List[int]: 161 162 """ 163 Return the pitch classes (0–11) that belong to a key and mode. 164 165 Parameters: 166 key_pc: Root pitch class (0 = C, 1 = C#/Db, …, 11 = B). 167 mode: Scale mode name. Supports all keys of ``DIATONIC_MODE_MAP`` 168 (e.g. ``"ionian"``, ``"dorian"``, ``"minor"``, ``"harmonic_minor"``). 169 170 Returns: 171 Pitch classes in scale-degree order, starting from the root 172 (length varies by mode). Values wrap mod-12, so the list is 173 not numerically sorted for non-C roots. 174 175 Example: 176 ```python 177 # C major pitch classes 178 scale_pitch_classes(0, "ionian") # → [0, 2, 4, 5, 7, 9, 11] 179 180 # A minor pitch classes 181 scale_pitch_classes(9, "aeolian") # → [9, 11, 0, 2, 4, 5, 7] (mod-12) 182 ``` 183 """ 184 185 if mode not in SCALE_MODE_MAP: 186 raise ValueError( 187 f"Unknown mode '{mode}'. Available: {sorted(SCALE_MODE_MAP)}. " 188 "Use register_scale() to add custom scales." 189 ) 190 191 scale_key, _ = SCALE_MODE_MAP[mode] 192 intervals = get_intervals(scale_key) 193 return [(key_pc + i) % 12 for i in intervals]
Return the pitch classes (0–11) that belong to a key and mode.
Arguments:
- key_pc: Root pitch class (0 = C, 1 = C#/Db, …, 11 = B).
- mode: Scale mode name. Supports all keys of
DIATONIC_MODE_MAP(e.g."ionian","dorian","minor","harmonic_minor").
Returns:
Pitch classes in scale-degree order, starting from the root (length varies by mode). Values wrap mod-12, so the list is not numerically sorted for non-C roots.
Example:
# C major pitch classes scale_pitch_classes(0, "ionian") # → [0, 2, 4, 5, 7, 9, 11] # A minor pitch classes scale_pitch_classes(9, "aeolian") # → [9, 11, 0, 2, 4, 5, 7] (mod-12)
196def scale_notes ( 197 key: str, 198 mode: str = "ionian", 199 low: int = 60, 200 high: int = 72, 201 count: typing.Optional[int] = None, 202) -> typing.List[int]: 203 204 """Return MIDI note numbers for a scale within a pitch range. 205 206 Parameters: 207 key: Scale root as a note name (``"C"``, ``"F#"``, ``"Bb"``, etc.). 208 This acts as a **pitch-class filter only** — it determines which 209 semitone positions (0–11) are valid members of the scale, but does 210 not affect which octave notes are drawn from. Notes are selected 211 starting from ``low`` upward; ``key`` controls *which* notes are 212 kept, not where the sequence starts. To guarantee the first 213 returned note is the root, ``low`` must be a MIDI number whose 214 pitch class matches ``key``. When starting from an arbitrary MIDI 215 number, derive the key name with 216 ``subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]``. 217 mode: Scale mode name. Supports all keys of :data:`SCALE_MODE_MAP` 218 (e.g. ``"ionian"``, ``"dorian"``, ``"natural_minor"``, 219 ``"major_pentatonic"``). Use :func:`register_scale` for custom scales. 220 low: Lowest MIDI note (inclusive). When ``count`` is set, this is 221 the starting note from which the scale ascends. **If ``low`` is 222 not a member of the scale defined by ``key``, it is silently 223 skipped** and the first returned note will be the next in-scale 224 pitch above ``low``. 225 high: Highest MIDI note (inclusive). Ignored when ``count`` is set. 226 count: Exact number of notes to return. Notes ascend from ``low`` 227 through successive scale degrees, cycling into higher octaves 228 as needed. When ``None`` (default), all scale tones between 229 ``low`` and ``high`` are returned. 230 231 Returns: 232 Sorted list of MIDI note numbers. 233 234 Examples: 235 ```python 236 import subsequence 237 import subsequence.constants.midi_notes as notes 238 239 # C major: all tones from middle C to C5 240 subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5) 241 # → [60, 62, 64, 65, 67, 69, 71, 72] 242 243 # E natural minor (aeolian) across one octave 244 subsequence.scale_notes("E", "aeolian", low=notes.E2, high=notes.E3) 245 # → [40, 42, 43, 45, 47, 48, 50, 52] 246 247 # 15 notes of A minor pentatonic ascending from A3 248 subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=15) 249 # → [57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88, 91] 250 251 # Misalignment: key="E" but low=C4 — first note is C, not E 252 subsequence.scale_notes("E", "minor", low=60, count=4) 253 # → [60, 62, 64, 66] (C D E F# — all in E natural minor, but starts on C) 254 255 # Fix: derive key name from root_pitch so low is always in the scale 256 root_pitch = 64 # E4 257 key = subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12] # → "E" 258 subsequence.scale_notes(key, "minor", low=root_pitch, count=4) 259 # → [64, 66, 67, 69] (E F# G A — starts on the root) 260 ``` 261 """ 262 263 key_pc = subsequence.chords.key_name_to_pc(key) 264 pcs = set(scale_pitch_classes(key_pc, mode)) 265 266 if count is not None: 267 if not pcs: 268 return [] 269 result: typing.List[int] = [] 270 pitch = low 271 while len(result) < count and pitch <= 127: 272 if pitch % 12 in pcs: 273 result.append(pitch) 274 pitch += 1 275 return result 276 277 return [p for p in range(low, high + 1) if p % 12 in pcs]
Return MIDI note numbers for a scale within a pitch range.
Arguments:
- key: Scale root as a note name (
"C","F#","Bb", etc.). This acts as a pitch-class filter only — it determines which semitone positions (0–11) are valid members of the scale, but does not affect which octave notes are drawn from. Notes are selected starting fromlowupward;keycontrols which notes are kept, not where the sequence starts. To guarantee the first returned note is the root,lowmust be a MIDI number whose pitch class matcheskey. When starting from an arbitrary MIDI number, derive the key name withsubsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]. - mode: Scale mode name. Supports all keys of
SCALE_MODE_MAP(e.g."ionian","dorian","natural_minor","major_pentatonic"). Useregister_scale()for custom scales. - low: Lowest MIDI note (inclusive). When
countis set, this is the starting note from which the scale ascends. Iflowis not a member of the scale defined bykey, it is silently skipped and the first returned note will be the next in-scale pitch abovelow. - high: Highest MIDI note (inclusive). Ignored when
countis set. - count: Exact number of notes to return. Notes ascend from
lowthrough successive scale degrees, cycling into higher octaves as needed. WhenNone(default), all scale tones betweenlowandhighare returned.
Returns:
Sorted list of MIDI note numbers.
Examples:
import subsequence import subsequence.constants.midi_notes as notes # C major: all tones from middle C to C5 subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5) # → [60, 62, 64, 65, 67, 69, 71, 72] # E natural minor (aeolian) across one octave subsequence.scale_notes("E", "aeolian", low=notes.E2, high=notes.E3) # → [40, 42, 43, 45, 47, 48, 50, 52] # 15 notes of A minor pentatonic ascending from A3 subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=15) # → [57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88, 91] # Misalignment: key="E" but low=C4 — first note is C, not E subsequence.scale_notes("E", "minor", low=60, count=4) # → [60, 62, 64, 66] (C D E F# — all in E natural minor, but starts on C) # Fix: derive key name from root_pitch so low is always in the scale root_pitch = 64 # E4 key = subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12] # → "E" subsequence.scale_notes(key, "minor", low=root_pitch, count=4) # → [64, 66, 67, 69] (E F# G A — starts on the root)
280def quantize_pitch (pitch: int, scale_pcs: typing.Sequence[int]) -> int: 281 282 """ 283 Snap a MIDI pitch to the nearest note in the given scale. 284 285 Searches outward in semitone steps from the input pitch. When two 286 notes are equidistant (e.g. C# between C and D in C major), the 287 upward direction is preferred. 288 289 Parameters: 290 pitch: MIDI note number to quantize. 291 scale_pcs: Pitch classes accepted by the scale (0–11). Typically 292 the output of :func:`scale_pitch_classes`. 293 294 Returns: 295 A MIDI note number that lies within the scale. 296 297 Example: 298 ```python 299 # Snap C# (61) to C (60) in C major 300 scale = scale_pitch_classes(0, "ionian") # [0, 2, 4, 5, 7, 9, 11] 301 quantize_pitch(61, scale) # → 60 302 ``` 303 """ 304 305 pc = pitch % 12 306 307 if pc in scale_pcs: 308 return pitch 309 310 for offset in range(1, 7): 311 if (pc + offset) % 12 in scale_pcs: 312 return pitch + offset 313 if (pc - offset) % 12 in scale_pcs: 314 return pitch - offset 315 316 # The search radius of ±6 semitones covers every gap in every scale with 317 # no gap wider than one tritone. A wider gap (unusual custom scale) falls 318 # through here and keeps the original off-scale pitch — warn so the caller 319 # knows the result is not actually snapped to the scale. 320 logger.warning( 321 "quantize_pitch: no scale note within ±6 semitones of MIDI %d (pc=%d); " 322 "returning pitch unquantized. scale_pcs=%s", 323 pitch, pc, sorted(scale_pcs), 324 ) 325 return pitch
Snap a MIDI pitch to the nearest note in the given scale.
Searches outward in semitone steps from the input pitch. When two notes are equidistant (e.g. C# between C and D in C major), the upward direction is preferred.
Arguments:
- pitch: MIDI note number to quantize.
- scale_pcs: Pitch classes accepted by the scale (0–11). Typically
the output of
scale_pitch_classes().
Returns:
A MIDI note number that lies within the scale.
Example:
# Snap C# (61) to C (60) in C major scale = scale_pitch_classes(0, "ionian") # [0, 2, 4, 5, 7, 9, 11] quantize_pitch(61, scale) # → 60
328def get_intervals (name: str) -> typing.List[int]: 329 330 """ 331 Return a named interval list from the registry. 332 """ 333 334 if name not in INTERVAL_DEFINITIONS: 335 raise ValueError(f"Unknown interval set: {name}") 336 337 return list(INTERVAL_DEFINITIONS[name])
Return a named interval list from the registry.
340def register_scale ( 341 name: str, 342 intervals: typing.List[int], 343 qualities: typing.Optional[typing.List[str]] = None 344) -> None: 345 346 """ 347 Register a custom scale for use with ``p.snap_to_scale()`` and 348 ``scale_pitch_classes()``. 349 350 Built-in scale names (e.g. ``"minor"``, ``"hirajoshi"``) cannot be 351 overwritten. Custom names may be re-registered freely — live reload 352 re-runs registration on every save, so this must not raise. 353 354 Parameters: 355 name: Scale name (used in ``p.snap_to_scale(key, name)``). Must not 356 be the name of a built-in scale. 357 intervals: Semitone offsets from the root (e.g. ``[0, 2, 3, 7, 8]`` 358 for Hirajōshi). Must be whole numbers, start with 0, ascend 359 strictly, and stay within 0–11. 360 qualities: Optional chord quality per scale degree (e.g. 361 ``["minor", "major", "minor", "major", "diminished"]``). 362 Required only if you want to use the scale with 363 ``diatonic_chords()`` or ``diatonic_chord_sequence()``. 364 365 Raises: 366 ValueError: If *name* is a built-in scale, or *intervals* / 367 *qualities* fail the rules above. 368 369 Example:: 370 371 import subsequence 372 373 subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11]) 374 375 @comp.pattern(channel=0, length=4) 376 def melody (p): 377 p.note(60, beat=0) 378 p.snap_to_scale("C", "raga_bhairav") 379 """ 380 381 if name in _BUILTIN_SCALE_NAMES: 382 raise ValueError( 383 f"Cannot overwrite built-in scale '{name}'. " 384 "Choose a different name for your custom scale." 385 ) 386 387 if not intervals: 388 raise ValueError("intervals must not be empty") 389 if not all(isinstance(i, int) for i in intervals): 390 raise ValueError("intervals must be whole numbers (semitone offsets)") 391 if intervals[0] != 0: 392 raise ValueError("intervals must start with 0") 393 if any(b <= a for a, b in zip(intervals, intervals[1:])): 394 raise ValueError("intervals must be strictly ascending") 395 if any(i < 0 or i > 11 for i in intervals): 396 raise ValueError("intervals must contain values between 0 and 11") 397 if qualities is not None and len(qualities) != len(intervals): 398 raise ValueError( 399 f"qualities length ({len(qualities)}) must match " 400 f"intervals length ({len(intervals)})" 401 ) 402 403 INTERVAL_DEFINITIONS[name] = intervals 404 SCALE_MODE_MAP[name] = (name, qualities)
Register a custom scale for use with p.snap_to_scale() and
scale_pitch_classes().
Built-in scale names (e.g. "minor", "hirajoshi") cannot be
overwritten. Custom names may be re-registered freely — live reload
re-runs registration on every save, so this must not raise.
Arguments:
- name: Scale name (used in
p.snap_to_scale(key, name)). Must not be the name of a built-in scale. - intervals: Semitone offsets from the root (e.g.
[0, 2, 3, 7, 8]for Hirajōshi). Must be whole numbers, start with 0, ascend strictly, and stay within 0–11. - qualities: Optional chord quality per scale degree (e.g.
["minor", "major", "minor", "major", "diminished"]). Required only if you want to use the scale withdiatonic_chords()ordiatonic_chord_sequence().
Raises:
- ValueError: If name is a built-in scale, or intervals / qualities fail the rules above.
Example::
import subsequence
subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11])
@comp.pattern(channel=0, length=4)
def melody (p):
p.note(60, beat=0)
p.snap_to_scale("C", "raga_bhairav")
407def get_diatonic_intervals ( 408 scale_notes: typing.List[int], 409 intervals: typing.Optional[typing.List[int]] = None, 410 mode: str = "scale" 411) -> typing.List[typing.List[int]]: 412 413 """ 414 Construct diatonic chords from a scale. 415 """ 416 417 if intervals is None: 418 intervals = [0, 2, 4] 419 420 if mode not in ("scale", "chromatic"): 421 raise ValueError("mode must be 'scale' or 'chromatic'") 422 423 diatonic_intervals: typing.List[typing.List[int]] = [] 424 num_scale_notes = len(scale_notes) 425 426 for i in range(num_scale_notes): 427 428 if mode == "scale": 429 chord = [scale_notes[(i + offset) % num_scale_notes] for offset in intervals] 430 431 else: 432 root = scale_notes[i] 433 chord = [(root + offset) % 12 for offset in intervals] 434 435 diatonic_intervals.append(chord) 436 437 return diatonic_intervals
Construct diatonic chords from a scale.