#!/usr/bin/env python """ Pitch class - a point in harmonic space. Represented as an array of exponents on prime dimensions. Example: (0, 1, 0, 0) represents 3/2 (perfect fifth) in CHS_7 """ from __future__ import annotations from fractions import Fraction from math import log from operator import add from typing import Iterator DIMS_8 = (2, 3, 5, 7, 11, 13, 17, 19) DIMS_7 = (2, 3, 5, 7, 11, 13, 17) DIMS_5 = (2, 3, 5, 7, 11) DIMS_4 = (2, 3, 5, 7) class Pitch: def __init__(self, hs_array: tuple[int, ...], dims: tuple[int, ...] | None = None): self.hs_array = hs_array self.dims = dims if dims is not None else DIMS_7 def __hash__(self) -> int: return hash(self.hs_array) def __eq__(self, other: object) -> bool: if not isinstance(other, Pitch): return NotImplemented return self.hs_array == other.hs_array def __repr__(self) -> str: return f"Pitch({self.hs_array})" def __iter__(self): return iter(self.hs_array) def __len__(self) -> int: return len(self.hs_array) def __getitem__(self, index: int) -> int: return self.hs_array[index] def to_fraction(self) -> Fraction: """Convert to frequency ratio (e.g., 3/2).""" from math import prod return Fraction( prod(pow(self.dims[d], self.hs_array[d]) for d in range(len(self.dims))) ) def to_cents(self) -> float: """Convert to cents (relative to 1/1 = 0 cents).""" fr = self.to_fraction() return 1200 * log(float(fr), 2) def collapse(self) -> Pitch: """ Collapse pitch so frequency ratio is in [1, 2). This removes octave information, useful for pitch classes. """ collapsed = list(self.hs_array) fr = self.to_fraction() if fr < 1: while fr < 1: fr *= 2 collapsed[0] += 1 elif fr >= 2: while fr >= 2: fr /= 2 collapsed[0] -= 1 return Pitch(tuple(collapsed), self.dims) def project(self) -> Pitch: """Project pitch to [1, 2) range - same as collapse.""" return self.collapse() def transpose(self, trans: Pitch) -> Pitch: """Transpose by another pitch (add exponents element-wise).""" return Pitch(tuple(map(add, self.hs_array, trans.hs_array)), self.dims) def pitch_difference(self, other: Pitch) -> Pitch: """Calculate the pitch difference (self - other).""" return Pitch( tuple(self.hs_array[d] - other.hs_array[d] for d in range(len(self.dims))), self.dims, )