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