subsequence.harmonic_rhythm

 1import dataclasses
 2import math
 3import random
 4import typing
 5
 6
 7@dataclasses.dataclass(frozen=True)
 8class HarmonicRhythm:
 9
10	"""A bounded, optionally-quantised *harmonic rhythm* — how long each chord lasts.
11
12	Harmonic rhythm is the rate at which the chords change.  It can be regular,
13	irregular, or static; this spec describes the **irregular** case — each chord
14	lasts a fresh random length somewhere between ``low`` and ``high`` beats.  When
15	``step`` is given, those lengths snap to whole multiples of it, so the result
16	is irregular but still lands on a musical grid (e.g. always a whole-note
17	boundary).
18
19	Build one with :func:`between` rather than constructing it directly::
20
21		harmonic_rhythm = between(WHOLE, 3 * WHOLE, step=WHOLE)   # 1, 2, or 3 whole notes
22
23	The other two harmonic-rhythm shapes are expressed without this class:
24	a single ``float`` (static — every chord the same length) and a ``list`` of
25	floats (a *shaped* rhythm such as ``[WHOLE, HALF, HALF]``, cycled per chord).
26	``p.progression()`` / ``comp.chords()`` accept all three.
27	"""
28
29	low: float
30	high: float
31	step: typing.Optional[float] = None
32
33	def __post_init__ (self) -> None:
34
35		"""Validate the bounds at construction so a typo surfaces at the call site."""
36
37		if self.low <= 0:
38			raise ValueError(f"harmonic rhythm low ({self.low:g}) must be positive — lengths are in beats")
39		if self.high < self.low:
40			raise ValueError(f"harmonic rhythm high ({self.high:g}) must be at least low ({self.low:g})")
41		if self.step is not None and self.step <= 0:
42			raise ValueError(f"harmonic rhythm step ({self.step:g}) must be positive")
43
44	def resolve (self, rng: random.Random) -> float:
45
46		"""Draw one chord length in beats from this spec.
47
48		With a ``step``, the length is a whole multiple of it snapped *inside*
49		``[low, high]`` (so a quantised rhythm never strays past its bounds).  If
50		no whole multiple fits within the bounds, the nearest in-range length is
51		used.  Without a ``step``, the draw is continuous and uniform.
52		"""
53
54		if self.step:
55			# Smallest/largest step-multiples that still sit within the bounds.
56			# The epsilon absorbs float dust so e.g. 12 / 4 floors to 3, not 2.
57			lo = max(1, math.ceil(self.low / self.step - 1e-9))
58			hi = math.floor(self.high / self.step + 1e-9)
59			if hi < lo:
60				hi = lo
61			length = rng.randint(lo, hi) * self.step
62
63			# Honour the bounds over the grid: when no whole multiple fits inside
64			# [low, high] (e.g. between(2, 3, step=4)), clamp to the nearest edge so
65			# the result never strays past the bounds the musician asked for.
66			return float(min(self.high, max(self.low, length)))
67
68		return float(rng.uniform(self.low, self.high))
69
70
71def between (low: float, high: float, step: typing.Optional[float] = None) -> HarmonicRhythm:
72
73	"""A harmonic rhythm that varies *between* two lengths (in beats).
74
75	Each chord lasts a random length in ``[low, high]``.  Pass ``step`` to snap
76	those lengths to a grid — e.g. ``between(WHOLE, 3 * WHOLE, step=WHOLE)`` gives
77	one, two, or three whole notes, never anything in between.
78
79	Reads aloud the way you'd describe it: "between one and three whole notes,
80	in whole-note steps."
81	"""
82
83	return HarmonicRhythm(low=low, high=high, step=step)
@dataclasses.dataclass(frozen=True)
class HarmonicRhythm:
 8@dataclasses.dataclass(frozen=True)
 9class HarmonicRhythm:
10
11	"""A bounded, optionally-quantised *harmonic rhythm* — how long each chord lasts.
12
13	Harmonic rhythm is the rate at which the chords change.  It can be regular,
14	irregular, or static; this spec describes the **irregular** case — each chord
15	lasts a fresh random length somewhere between ``low`` and ``high`` beats.  When
16	``step`` is given, those lengths snap to whole multiples of it, so the result
17	is irregular but still lands on a musical grid (e.g. always a whole-note
18	boundary).
19
20	Build one with :func:`between` rather than constructing it directly::
21
22		harmonic_rhythm = between(WHOLE, 3 * WHOLE, step=WHOLE)   # 1, 2, or 3 whole notes
23
24	The other two harmonic-rhythm shapes are expressed without this class:
25	a single ``float`` (static — every chord the same length) and a ``list`` of
26	floats (a *shaped* rhythm such as ``[WHOLE, HALF, HALF]``, cycled per chord).
27	``p.progression()`` / ``comp.chords()`` accept all three.
28	"""
29
30	low: float
31	high: float
32	step: typing.Optional[float] = None
33
34	def __post_init__ (self) -> None:
35
36		"""Validate the bounds at construction so a typo surfaces at the call site."""
37
38		if self.low <= 0:
39			raise ValueError(f"harmonic rhythm low ({self.low:g}) must be positive — lengths are in beats")
40		if self.high < self.low:
41			raise ValueError(f"harmonic rhythm high ({self.high:g}) must be at least low ({self.low:g})")
42		if self.step is not None and self.step <= 0:
43			raise ValueError(f"harmonic rhythm step ({self.step:g}) must be positive")
44
45	def resolve (self, rng: random.Random) -> float:
46
47		"""Draw one chord length in beats from this spec.
48
49		With a ``step``, the length is a whole multiple of it snapped *inside*
50		``[low, high]`` (so a quantised rhythm never strays past its bounds).  If
51		no whole multiple fits within the bounds, the nearest in-range length is
52		used.  Without a ``step``, the draw is continuous and uniform.
53		"""
54
55		if self.step:
56			# Smallest/largest step-multiples that still sit within the bounds.
57			# The epsilon absorbs float dust so e.g. 12 / 4 floors to 3, not 2.
58			lo = max(1, math.ceil(self.low / self.step - 1e-9))
59			hi = math.floor(self.high / self.step + 1e-9)
60			if hi < lo:
61				hi = lo
62			length = rng.randint(lo, hi) * self.step
63
64			# Honour the bounds over the grid: when no whole multiple fits inside
65			# [low, high] (e.g. between(2, 3, step=4)), clamp to the nearest edge so
66			# the result never strays past the bounds the musician asked for.
67			return float(min(self.high, max(self.low, length)))
68
69		return float(rng.uniform(self.low, self.high))

A bounded, optionally-quantised harmonic rhythm — how long each chord lasts.

Harmonic rhythm is the rate at which the chords change. It can be regular, irregular, or static; this spec describes the irregular case — each chord lasts a fresh random length somewhere between low and high beats. When step is given, those lengths snap to whole multiples of it, so the result is irregular but still lands on a musical grid (e.g. always a whole-note boundary).

Build one with between() rather than constructing it directly::

    harmonic_rhythm = between(WHOLE, 3 * WHOLE, step=WHOLE)   # 1, 2, or 3 whole notes

The other two harmonic-rhythm shapes are expressed without this class: a single float (static — every chord the same length) and a list of floats (a shaped rhythm such as [WHOLE, HALF, HALF], cycled per chord). p.progression() / comp.chords() accept all three.

HarmonicRhythm(low: float, high: float, step: Optional[float] = None)
low: float
high: float
step: Optional[float] = None
def resolve(self, rng: random.Random) -> float:
45	def resolve (self, rng: random.Random) -> float:
46
47		"""Draw one chord length in beats from this spec.
48
49		With a ``step``, the length is a whole multiple of it snapped *inside*
50		``[low, high]`` (so a quantised rhythm never strays past its bounds).  If
51		no whole multiple fits within the bounds, the nearest in-range length is
52		used.  Without a ``step``, the draw is continuous and uniform.
53		"""
54
55		if self.step:
56			# Smallest/largest step-multiples that still sit within the bounds.
57			# The epsilon absorbs float dust so e.g. 12 / 4 floors to 3, not 2.
58			lo = max(1, math.ceil(self.low / self.step - 1e-9))
59			hi = math.floor(self.high / self.step + 1e-9)
60			if hi < lo:
61				hi = lo
62			length = rng.randint(lo, hi) * self.step
63
64			# Honour the bounds over the grid: when no whole multiple fits inside
65			# [low, high] (e.g. between(2, 3, step=4)), clamp to the nearest edge so
66			# the result never strays past the bounds the musician asked for.
67			return float(min(self.high, max(self.low, length)))
68
69		return float(rng.uniform(self.low, self.high))

Draw one chord length in beats from this spec.

With a step, the length is a whole multiple of it snapped inside [low, high] (so a quantised rhythm never strays past its bounds). If no whole multiple fits within the bounds, the nearest in-range length is used. Without a step, the draw is continuous and uniform.

def between( low: float, high: float, step: Optional[float] = None) -> HarmonicRhythm:
72def between (low: float, high: float, step: typing.Optional[float] = None) -> HarmonicRhythm:
73
74	"""A harmonic rhythm that varies *between* two lengths (in beats).
75
76	Each chord lasts a random length in ``[low, high]``.  Pass ``step`` to snap
77	those lengths to a grid — e.g. ``between(WHOLE, 3 * WHOLE, step=WHOLE)`` gives
78	one, two, or three whole notes, never anything in between.
79
80	Reads aloud the way you'd describe it: "between one and three whole notes,
81	in whole-note steps."
82	"""
83
84	return HarmonicRhythm(low=low, high=high, step=step)

A harmonic rhythm that varies between two lengths (in beats).

Each chord lasts a random length in [low, high]. Pass step to snap those lengths to a grid — e.g. between(WHOLE, 3 * WHOLE, step=WHOLE) gives one, two, or three whole notes, never anything in between.

Reads aloud the way you'd describe it: "between one and three whole notes, in whole-note steps."