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"). 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.
  • 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]
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 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]]:
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.