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