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 from low upward; key controls which notes are kept, not where the sequence starts. To guarantee the first returned note is the root, low must be a MIDI number whose pitch class matches key. When starting from an arbitrary MIDI number, derive the key name with subsequence.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"). Use register_scale() for custom scales.
  • low: Lowest MIDI note (inclusive). When count is set, this is the starting note from which the scale ascends. If low is not a member of the scale defined by key, it is silently skipped and the first returned note will be the next in-scale pitch above low.
  • high: Highest MIDI note (inclusive). Ignored when count is set.
  • count: Exact number of notes to return. Notes ascend from low through successive scale degrees, cycling into higher octaves as needed. When None (default), all scale tones between low and high are 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 with diatonic_chords() or diatonic_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.