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