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)
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.
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.
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."