mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54b82440c4 | |||
| ef003dbd1d |
+1
-1
@@ -10,7 +10,7 @@ sys.modules["sounddevice"] = MagicMock()
|
||||
project = "PyTheory"
|
||||
copyright = "2026, Kenneth Reitz"
|
||||
author = "Kenneth Reitz"
|
||||
release = "0.3.1"
|
||||
release = "0.3.2"
|
||||
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.3.1"
|
||||
__version__ = "0.3.2"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS
|
||||
|
||||
+119
-58
@@ -1,9 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterator, Optional, Union
|
||||
|
||||
|
||||
class Chord:
|
||||
def __init__(self, tones):
|
||||
def __init__(self, tones: list[Tone]) -> None:
|
||||
"""Initialize a Chord from a list of Tone objects.
|
||||
|
||||
Args:
|
||||
tones: A list of :class:`Tone` instances that make up the chord.
|
||||
"""
|
||||
self.tones = tones
|
||||
self._identify_cache: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def from_tones(cls, *note_names, octave=4):
|
||||
def from_tones(cls, *note_names: str, octave: int = 4) -> Chord:
|
||||
"""Create a Chord from note name strings.
|
||||
|
||||
Example::
|
||||
@@ -20,7 +31,7 @@ class Chord:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def from_name(cls, name, octave=4):
|
||||
def from_name(cls, name: str, octave: int = 4) -> Chord:
|
||||
"""Create a Chord from a chord name like ``"Cmaj7"`` or ``"Am"``.
|
||||
|
||||
Uses the built-in chord chart to find the correct tones,
|
||||
@@ -49,31 +60,34 @@ class Chord:
|
||||
f"{t.name}{octave}", system="western"))
|
||||
return cls(tones=tones)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
name = self.identify()
|
||||
if name:
|
||||
return f"<Chord {name}>"
|
||||
l = tuple([tone.full_name for tone in self.tones])
|
||||
return f"<Chord tones={l!r}>"
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
name = self.identify()
|
||||
if name:
|
||||
return name
|
||||
return " ".join(t.full_name for t in self.tones)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[Tone]:
|
||||
"""Iterate over the tones in this chord."""
|
||||
return iter(self.tones)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of tones in this chord."""
|
||||
return len(self.tones)
|
||||
|
||||
def __contains__(self, item):
|
||||
def __contains__(self, item: Union[str, Tone]) -> bool:
|
||||
"""Check if a tone (by name or Tone object) is in this chord."""
|
||||
if isinstance(item, str):
|
||||
return any(item == t.name for t in self.tones)
|
||||
return item in self.tones
|
||||
|
||||
def __add__(self, other):
|
||||
def __add__(self, other: Chord) -> Chord:
|
||||
"""Merge two chords into one (layer their tones).
|
||||
|
||||
Example::
|
||||
@@ -86,7 +100,7 @@ class Chord:
|
||||
return Chord(tones=list(self.tones) + list(other.tones))
|
||||
return NotImplemented
|
||||
|
||||
def tritone_sub(self):
|
||||
def tritone_sub(self) -> Chord:
|
||||
"""Return the tritone substitution of this chord.
|
||||
|
||||
In jazz harmony, any dominant chord can be replaced by the
|
||||
@@ -98,7 +112,7 @@ class Chord:
|
||||
"""
|
||||
return self.transpose(6)
|
||||
|
||||
def inversion(self, n=1):
|
||||
def inversion(self, n: int = 1) -> Chord:
|
||||
"""Return the nth inversion of this chord.
|
||||
|
||||
An inversion moves the lowest tone(s) up by one octave:
|
||||
@@ -121,9 +135,11 @@ class Chord:
|
||||
break
|
||||
tone = tones.pop(0)
|
||||
tones.append(tone.add(12))
|
||||
return Chord(tones=tones)
|
||||
result = Chord(tones=tones)
|
||||
result._identify_cache = None
|
||||
return result
|
||||
|
||||
def transpose(self, semitones):
|
||||
def transpose(self, semitones: int) -> Chord:
|
||||
"""Return a new Chord transposed by the given number of semitones.
|
||||
|
||||
Every tone in the chord is shifted up (positive) or down
|
||||
@@ -136,10 +152,12 @@ class Chord:
|
||||
>>> c_major.transpose(7).identify()
|
||||
'G major'
|
||||
"""
|
||||
return Chord(tones=[t.add(semitones) for t in self.tones])
|
||||
result = Chord(tones=[t.add(semitones) for t in self.tones])
|
||||
result._identify_cache = None
|
||||
return result
|
||||
|
||||
@property
|
||||
def root(self):
|
||||
def root(self) -> Optional[Tone]:
|
||||
"""The root of this chord (if identifiable).
|
||||
|
||||
Returns the Tone that serves as the root based on chord
|
||||
@@ -155,7 +173,7 @@ class Chord:
|
||||
return None
|
||||
|
||||
@property
|
||||
def quality(self):
|
||||
def quality(self) -> Optional[str]:
|
||||
"""The quality of this chord (e.g. 'major', 'minor 7th').
|
||||
|
||||
Returns the quality string from chord identification, or
|
||||
@@ -168,7 +186,7 @@ class Chord:
|
||||
return parts[1] if len(parts) > 1 else None
|
||||
|
||||
@property
|
||||
def intervals(self):
|
||||
def intervals(self) -> list[int]:
|
||||
"""Semitone distances between adjacent tones in the chord.
|
||||
|
||||
Returns a list of integers, where each value is the absolute
|
||||
@@ -201,7 +219,7 @@ class Chord:
|
||||
for i in range(1, len(self.tones))]
|
||||
|
||||
@property
|
||||
def harmony(self):
|
||||
def harmony(self) -> float:
|
||||
"""Consonance score based on frequency ratio simplicity.
|
||||
|
||||
Computed by examining the frequency ratio between every pair of
|
||||
@@ -243,7 +261,7 @@ class Chord:
|
||||
return score
|
||||
|
||||
@property
|
||||
def dissonance(self):
|
||||
def dissonance(self) -> float:
|
||||
"""Sensory dissonance score using the Plomp-Levelt roughness model.
|
||||
|
||||
When two tones are close in frequency, their waveforms interfere
|
||||
@@ -296,7 +314,7 @@ class Chord:
|
||||
return roughness
|
||||
|
||||
@property
|
||||
def beat_frequencies(self):
|
||||
def beat_frequencies(self) -> list[tuple[Tone, Tone, float]]:
|
||||
"""Beat frequencies (Hz) between all pairs of tones in the chord.
|
||||
|
||||
When two tones with frequencies f1 and f2 are played together,
|
||||
@@ -337,7 +355,7 @@ class Chord:
|
||||
return sorted(beats, key=lambda b: b[2])
|
||||
|
||||
@property
|
||||
def beat_pulse(self):
|
||||
def beat_pulse(self) -> float:
|
||||
"""The slowest (most perceptible) beat frequency in the chord, in Hz.
|
||||
|
||||
This is the beat frequency between the two tones closest in
|
||||
@@ -379,7 +397,7 @@ class Chord:
|
||||
"minor 9th": {0, 2, 3, 7, 10},
|
||||
}
|
||||
|
||||
def identify(self):
|
||||
def identify(self) -> Optional[str]:
|
||||
"""Identify this chord by name (root + quality).
|
||||
|
||||
Tries each tone as a potential root and checks if the remaining
|
||||
@@ -400,6 +418,9 @@ class Chord:
|
||||
>>> Chord([A4, C5, E5]).identify()
|
||||
'A minor'
|
||||
"""
|
||||
if self._identify_cache is not None:
|
||||
return self._identify_cache
|
||||
|
||||
if len(self.tones) < 2:
|
||||
return None
|
||||
|
||||
@@ -413,10 +434,11 @@ class Chord:
|
||||
|
||||
for name, pattern in self._CHORD_PATTERNS.items():
|
||||
if pitch_classes == pattern:
|
||||
return f"{root.name} {name}"
|
||||
self._identify_cache = f"{root.name} {name}"
|
||||
return self._identify_cache
|
||||
return None
|
||||
|
||||
def voice_leading(self, other):
|
||||
def voice_leading(self, other: Chord) -> list[tuple[Tone, Tone, int]]:
|
||||
"""Find the smoothest voice leading to another chord.
|
||||
|
||||
Voice leading is the art of moving individual voices (tones)
|
||||
@@ -471,7 +493,7 @@ class Chord:
|
||||
result.append((src[i], dst[j], movement))
|
||||
return sorted(result, key=lambda v: v[0].pitch(), reverse=True)
|
||||
|
||||
def analyze(self, key_tonic, mode="major"):
|
||||
def analyze(self, key_tonic: Union[str, Tone], mode: str = "major") -> Optional[str]:
|
||||
"""Roman numeral analysis of this chord relative to a key.
|
||||
|
||||
In tonal music, every chord has a **function** determined by
|
||||
@@ -541,7 +563,7 @@ class Chord:
|
||||
return numeral_str + suffix
|
||||
|
||||
@property
|
||||
def tension(self):
|
||||
def tension(self) -> dict:
|
||||
"""Harmonic tension score and resolution suggestions.
|
||||
|
||||
Tension in tonal music arises from specific intervallic
|
||||
@@ -603,7 +625,22 @@ class Chord:
|
||||
"has_dominant_function": has_dominant,
|
||||
}
|
||||
|
||||
def fingering(self, *positions):
|
||||
def fingering(self, *positions: int) -> Chord:
|
||||
"""Apply fret positions to each tone, returning a new Chord.
|
||||
|
||||
Each position value is added (in semitones) to the corresponding
|
||||
tone. The number of positions must match the number of tones.
|
||||
|
||||
Args:
|
||||
*positions: One integer per tone indicating the fret offset.
|
||||
|
||||
Returns:
|
||||
A new :class:`Chord` with each tone shifted by its position.
|
||||
|
||||
Raises:
|
||||
ValueError: If the number of positions doesn't match the
|
||||
number of tones.
|
||||
"""
|
||||
if not len(positions) == len(self.tones):
|
||||
raise ValueError(
|
||||
"The number of positions must match the number of tones (strings)."
|
||||
@@ -617,14 +654,20 @@ class Chord:
|
||||
|
||||
|
||||
class Fretboard:
|
||||
def __init__(self, *, tones):
|
||||
def __init__(self, *, tones: list[Tone]) -> None:
|
||||
"""Initialize a Fretboard from a list of open-string Tone objects.
|
||||
|
||||
Args:
|
||||
tones: A list of :class:`Tone` instances representing the
|
||||
open strings (high to low).
|
||||
"""
|
||||
self.tones = tones
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
l = tuple([tone.full_name for tone in self.tones])
|
||||
return f"<Fretboard tones={l!r}>"
|
||||
|
||||
def capo(self, fret):
|
||||
def capo(self, fret: int) -> Fretboard:
|
||||
"""Return a new Fretboard with a capo at the given fret.
|
||||
|
||||
A `capo <https://en.wikipedia.org/wiki/Capo>`_ clamps across
|
||||
@@ -652,10 +695,12 @@ class Fretboard:
|
||||
"""
|
||||
return Fretboard(tones=[t.add(fret) for t in self.tones])
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[Tone]:
|
||||
"""Iterate over the open-string tones of this fretboard."""
|
||||
return iter(self.tones)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of strings on this fretboard."""
|
||||
return len(self.tones)
|
||||
|
||||
INSTRUMENTS = [
|
||||
@@ -680,7 +725,7 @@ class Fretboard:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def guitar(cls, tuning="standard", capo=0):
|
||||
def guitar(cls, tuning: Union[str, tuple[str, ...]] = "standard", capo: int = 0) -> Fretboard:
|
||||
"""Guitar with the given tuning and optional capo.
|
||||
|
||||
Args:
|
||||
@@ -699,7 +744,7 @@ class Fretboard:
|
||||
return fb
|
||||
|
||||
@classmethod
|
||||
def bass(cls, five_string=False):
|
||||
def bass(cls, five_string: bool = False) -> Fretboard:
|
||||
"""Standard bass guitar tuning.
|
||||
|
||||
Args:
|
||||
@@ -712,7 +757,7 @@ class Fretboard:
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
|
||||
@classmethod
|
||||
def ukulele(cls):
|
||||
def ukulele(cls) -> Fretboard:
|
||||
"""Standard ukulele tuning (A4 E4 C4 G4).
|
||||
|
||||
Re-entrant tuning: the G4 string is higher than C4.
|
||||
@@ -726,7 +771,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def mandolin(cls):
|
||||
def mandolin(cls) -> Fretboard:
|
||||
"""Standard mandolin tuning (E5 A4 D4 G3).
|
||||
|
||||
Tuned in fifths, same as a violin but one octave relationship.
|
||||
@@ -741,7 +786,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def mandola(cls):
|
||||
def mandola(cls) -> Fretboard:
|
||||
"""Standard mandola tuning (A4 D4 G3 C3).
|
||||
|
||||
The mandola (or tenor mandola) is to the mandolin what the
|
||||
@@ -757,7 +802,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def octave_mandolin(cls):
|
||||
def octave_mandolin(cls) -> Fretboard:
|
||||
"""Octave mandolin tuning (E4 A3 D3 G2).
|
||||
|
||||
Also called the octave mandola in European terminology.
|
||||
@@ -774,7 +819,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def mandocello(cls):
|
||||
def mandocello(cls) -> Fretboard:
|
||||
"""Mandocello tuning (A3 D3 G2 C2).
|
||||
|
||||
The bass of the mandolin family. Tuned like a cello — an
|
||||
@@ -790,7 +835,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def violin(cls):
|
||||
def violin(cls) -> Fretboard:
|
||||
"""Standard violin tuning (E5 A4 D4 G3).
|
||||
|
||||
Tuned in perfect fifths. The violin has no frets — intonation
|
||||
@@ -806,7 +851,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def viola(cls):
|
||||
def viola(cls) -> Fretboard:
|
||||
"""Standard viola tuning (A4 D4 G3 C3).
|
||||
|
||||
A perfect fifth below the violin. The viola's darker, warmer
|
||||
@@ -821,7 +866,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def cello(cls):
|
||||
def cello(cls) -> Fretboard:
|
||||
"""Standard cello tuning (A3 D3 G2 C2).
|
||||
|
||||
An octave below the viola. Tuned in fifths. The cello spans
|
||||
@@ -836,7 +881,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def banjo(cls, tuning="open g"):
|
||||
def banjo(cls, tuning: Union[str, tuple[str, ...]] = "open g") -> Fretboard:
|
||||
"""Banjo with the given tuning.
|
||||
|
||||
Args:
|
||||
@@ -858,7 +903,7 @@ class Fretboard:
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in tuning])
|
||||
|
||||
@classmethod
|
||||
def double_bass(cls):
|
||||
def double_bass(cls) -> Fretboard:
|
||||
"""Standard double bass (upright bass) tuning (G2 D2 A1 E1).
|
||||
|
||||
The largest and lowest-pitched bowed string instrument in the
|
||||
@@ -877,7 +922,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def harp(cls):
|
||||
def harp(cls) -> Fretboard:
|
||||
"""Concert harp strings — 47 strings spanning C1 to G7.
|
||||
|
||||
The pedal harp has 7 strings per octave (one per note name),
|
||||
@@ -905,7 +950,7 @@ class Fretboard:
|
||||
return cls(tones=[Tone.from_string(s, system="western") for s in strings])
|
||||
|
||||
@classmethod
|
||||
def pedal_steel(cls):
|
||||
def pedal_steel(cls) -> Fretboard:
|
||||
"""Pedal steel guitar — E9 Nashville tuning (10 strings).
|
||||
|
||||
The standard tuning for country music. The pedal steel has
|
||||
@@ -919,7 +964,7 @@ class Fretboard:
|
||||
return cls(tones=[Tone.from_string(s, system="western") for s in strings])
|
||||
|
||||
@classmethod
|
||||
def bouzouki(cls, variant="irish"):
|
||||
def bouzouki(cls, variant: Union[str, tuple[str, ...]] = "irish") -> Fretboard:
|
||||
"""Bouzouki tuning.
|
||||
|
||||
Args:
|
||||
@@ -939,7 +984,7 @@ class Fretboard:
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in variant])
|
||||
|
||||
@classmethod
|
||||
def oud(cls):
|
||||
def oud(cls) -> Fretboard:
|
||||
"""Standard Arabic oud tuning (C4 G3 D3 A2 G2 C2).
|
||||
|
||||
The oud is the ancestor of the European lute and the defining
|
||||
@@ -953,7 +998,7 @@ class Fretboard:
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
|
||||
@classmethod
|
||||
def sitar(cls):
|
||||
def sitar(cls) -> Fretboard:
|
||||
"""Sitar main playing strings (approximation).
|
||||
|
||||
The sitar typically has 6-7 main strings and 11-13 sympathetic
|
||||
@@ -970,7 +1015,7 @@ class Fretboard:
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
|
||||
@classmethod
|
||||
def shamisen(cls):
|
||||
def shamisen(cls) -> Fretboard:
|
||||
"""Standard shamisen tuning — honchoshi (C4 G3 C3).
|
||||
|
||||
The shamisen is a 3-stringed Japanese instrument played with
|
||||
@@ -988,7 +1033,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def erhu(cls):
|
||||
def erhu(cls) -> Fretboard:
|
||||
"""Standard erhu tuning (A4 D4).
|
||||
|
||||
The erhu is a 2-stringed Chinese bowed instrument with a
|
||||
@@ -1003,7 +1048,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def charango(cls):
|
||||
def charango(cls) -> Fretboard:
|
||||
"""Standard charango tuning (E5 A4 E5 C5 G4).
|
||||
|
||||
A small Andean stringed instrument, traditionally made from
|
||||
@@ -1021,7 +1066,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def pipa(cls):
|
||||
def pipa(cls) -> Fretboard:
|
||||
"""Standard pipa tuning (D4 A3 E3 A2).
|
||||
|
||||
The pipa is a 4-stringed Chinese lute with a pear-shaped
|
||||
@@ -1037,7 +1082,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def balalaika(cls):
|
||||
def balalaika(cls) -> Fretboard:
|
||||
"""Standard balalaika prima tuning (A4 E4 E4).
|
||||
|
||||
The Russian balalaika has a distinctive triangular body and
|
||||
@@ -1052,7 +1097,7 @@ class Fretboard:
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def keyboard(cls, keys=88, start="A0"):
|
||||
def keyboard(cls, keys: int = 88, start: str = "A0") -> Fretboard:
|
||||
"""Piano or keyboard with the given number of keys.
|
||||
|
||||
Args:
|
||||
@@ -1078,7 +1123,7 @@ class Fretboard:
|
||||
return cls(tones=tones)
|
||||
|
||||
@classmethod
|
||||
def lute(cls):
|
||||
def lute(cls) -> Fretboard:
|
||||
"""Renaissance lute in G tuning (6 courses).
|
||||
|
||||
The European lute was the dominant instrument of the
|
||||
@@ -1091,7 +1136,7 @@ class Fretboard:
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
|
||||
@classmethod
|
||||
def twelve_string(cls):
|
||||
def twelve_string(cls) -> Fretboard:
|
||||
"""12-string guitar in standard tuning.
|
||||
|
||||
The lower 4 courses are doubled at the octave; the upper 2
|
||||
@@ -1111,7 +1156,23 @@ class Fretboard:
|
||||
]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
|
||||
def fingering(self, *positions):
|
||||
def fingering(self, *positions: int) -> Chord:
|
||||
"""Apply fret positions to each string, returning a Chord.
|
||||
|
||||
Each position value is added (in semitones) to the corresponding
|
||||
open-string tone. The number of positions must match the number
|
||||
of strings.
|
||||
|
||||
Args:
|
||||
*positions: One integer per string indicating the fret number.
|
||||
|
||||
Returns:
|
||||
A :class:`Chord` with each tone shifted by its fret position.
|
||||
|
||||
Raises:
|
||||
ValueError: If the number of positions doesn't match the
|
||||
number of strings.
|
||||
"""
|
||||
if not len(positions) == len(self.tones):
|
||||
raise ValueError(
|
||||
"The number of positions must match the number of tones (strings)."
|
||||
|
||||
+84
-39
@@ -1,11 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
import numeral
|
||||
|
||||
from .systems import SYSTEMS
|
||||
from .systems import SYSTEMS, System
|
||||
from .tones import Tone
|
||||
|
||||
|
||||
class Scale:
|
||||
def __init__(self, *, tones, degrees=None, system='western'):
|
||||
def __init__(self, *, tones: tuple[Tone, ...], degrees: Optional[tuple[str, ...]] = None, system: Union[str, System] = 'western') -> None:
|
||||
"""Initialize a Scale from a sequence of Tones.
|
||||
|
||||
Args:
|
||||
tones: The tones that make up the scale.
|
||||
degrees: Optional names for each scale degree (must match length of *tones*).
|
||||
system: A tone system name or :class:`System` instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If *degrees* is provided but its length differs from *tones*.
|
||||
"""
|
||||
self.tones = tones
|
||||
self.degrees = degrees
|
||||
|
||||
@@ -21,14 +35,18 @@ class Scale:
|
||||
raise ValueError("The number of tones and degrees must be equal!")
|
||||
|
||||
@property
|
||||
def system(self):
|
||||
def system(self) -> Optional[System]:
|
||||
"""Return the tone system for this scale.
|
||||
|
||||
Resolves a system name to a :class:`System` object on first access.
|
||||
"""
|
||||
if self._system:
|
||||
return self._system
|
||||
|
||||
if self.system_name:
|
||||
return SYSTEMS[self.system_name]
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
r = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degree = numeral.int2roman(i + 1, only_ascii=True)
|
||||
@@ -38,22 +56,25 @@ class Scale:
|
||||
return f"<Scale {r}>"
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the tones in this scale."""
|
||||
return iter(self.tones)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of tones in this scale (including the octave)."""
|
||||
return len(self.tones)
|
||||
|
||||
def __contains__(self, item):
|
||||
def __contains__(self, item: Union[str, Tone]) -> bool:
|
||||
"""Check whether a tone or note name belongs to this scale."""
|
||||
if isinstance(item, str):
|
||||
return any(item == t.name for t in self.tones)
|
||||
return item in self.tones
|
||||
|
||||
@property
|
||||
def note_names(self):
|
||||
def note_names(self) -> list[str]:
|
||||
"""List of note names in this scale."""
|
||||
return [t.name for t in self.tones]
|
||||
|
||||
def chord(self, *degrees):
|
||||
def chord(self, *degrees: int) -> Chord:
|
||||
"""Build a Chord from scale degrees (0-indexed).
|
||||
|
||||
Wraps around if degrees exceed the scale length, transposing
|
||||
@@ -75,7 +96,7 @@ class Scale:
|
||||
result.append(tone)
|
||||
return Chord(tones=result)
|
||||
|
||||
def transpose(self, semitones):
|
||||
def transpose(self, semitones: int) -> Scale:
|
||||
"""Return a new Scale transposed by the given number of semitones.
|
||||
|
||||
Every tone is shifted by the same interval, preserving the
|
||||
@@ -92,21 +113,21 @@ class Scale:
|
||||
new_tones = tuple(t.add(semitones) for t in self.tones)
|
||||
return Scale(tones=new_tones)
|
||||
|
||||
def triad(self, root=0):
|
||||
def triad(self, root: int = 0) -> Chord:
|
||||
"""Build a triad starting from the given scale degree (0-indexed).
|
||||
|
||||
Returns a chord with the root, 3rd, and 5th above it.
|
||||
"""
|
||||
return self.chord(root, root + 2, root + 4)
|
||||
|
||||
def seventh(self, root=0):
|
||||
def seventh(self, root: int = 0) -> Chord:
|
||||
"""Build a seventh chord from the given scale degree (0-indexed).
|
||||
|
||||
Returns a chord with the root, 3rd, 5th, and 7th.
|
||||
"""
|
||||
return self.chord(root, root + 2, root + 4, root + 6)
|
||||
|
||||
def progression(self, *numerals):
|
||||
def progression(self, *numerals: str) -> list[Chord]:
|
||||
"""Build a chord progression from Roman numeral strings.
|
||||
|
||||
Accepts Roman numerals like ``"I"``, ``"IV"``, ``"V"``,
|
||||
@@ -130,7 +151,7 @@ class Scale:
|
||||
chords.append(self.triad(degree))
|
||||
return chords
|
||||
|
||||
def nashville(self, *numbers):
|
||||
def nashville(self, *numbers: Union[int, str]) -> list[Chord]:
|
||||
"""Build a chord progression using Nashville number system.
|
||||
|
||||
The `Nashville number system <https://en.wikipedia.org/wiki/Nashville_Number_System>`_
|
||||
@@ -159,7 +180,7 @@ class Scale:
|
||||
return chords
|
||||
|
||||
@staticmethod
|
||||
def detect(*note_names):
|
||||
def detect(*note_names: str) -> Optional[tuple[str, str, int]]:
|
||||
"""Detect the most likely scale from a set of note names.
|
||||
|
||||
Tries all scales in the Western system and returns the best
|
||||
@@ -200,7 +221,7 @@ class Scale:
|
||||
return (best[1], best[2], best[3])
|
||||
return None
|
||||
|
||||
def harmonize(self):
|
||||
def harmonize(self) -> list[Chord]:
|
||||
"""Build diatonic triads on every scale degree.
|
||||
|
||||
Returns a list of Chords — one triad for each degree of the
|
||||
@@ -214,7 +235,7 @@ class Scale:
|
||||
unique = len(self.tones) - 1
|
||||
return [self.triad(i) for i in range(unique)]
|
||||
|
||||
def degree(self, item, major=None, minor=False):
|
||||
def degree(self, item: Union[str, int, slice], major: Optional[bool] = None, minor: bool = False) -> Optional[Union[Tone, tuple[Tone, ...]]]:
|
||||
# TODO: cleanup degrees.
|
||||
|
||||
# Ensure that both major and minor aren't passed.
|
||||
@@ -247,7 +268,12 @@ class Scale:
|
||||
if isinstance(item, int) or isinstance(item, slice):
|
||||
return self.tones[item]
|
||||
|
||||
def __getitem__(self, item):
|
||||
def __getitem__(self, item: Union[str, int, slice]) -> Union[Tone, tuple[Tone, ...]]:
|
||||
"""Retrieve a tone by scale degree (integer, Roman numeral, or degree name).
|
||||
|
||||
Raises:
|
||||
KeyError: If the given degree is not found in this scale.
|
||||
"""
|
||||
result = self.degree(item)
|
||||
if result is None:
|
||||
raise KeyError(item)
|
||||
@@ -301,7 +327,7 @@ class Key:
|
||||
[<Chord (C,E,G)>, <Chord (G,B,D)>, ...]
|
||||
"""
|
||||
|
||||
def __init__(self, tonic, mode="major", system=None):
|
||||
def __init__(self, tonic: str, mode: str = "major", system: Optional[Union[str, System]] = None) -> None:
|
||||
if system is None:
|
||||
system = SYSTEMS["western"]
|
||||
elif isinstance(system, str):
|
||||
@@ -313,7 +339,7 @@ class Key:
|
||||
self._scale = self._toned_scale[mode]
|
||||
|
||||
@classmethod
|
||||
def detect(cls, *note_names):
|
||||
def detect(cls, *note_names: str) -> Optional[Key]:
|
||||
"""Detect the most likely key from a set of note names.
|
||||
|
||||
Tries every possible major and minor key and returns the one
|
||||
@@ -355,42 +381,42 @@ class Key:
|
||||
|
||||
return best_key
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Key {self.tonic_name} {self.mode}>"
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.tonic_name} {self.mode}"
|
||||
|
||||
@property
|
||||
def scale(self):
|
||||
def scale(self) -> Scale:
|
||||
"""The scale for this key."""
|
||||
return self._scale
|
||||
|
||||
@property
|
||||
def note_names(self):
|
||||
def note_names(self) -> list[str]:
|
||||
"""Note names in this key's scale."""
|
||||
return self._scale.note_names
|
||||
|
||||
@property
|
||||
def chords(self):
|
||||
def chords(self) -> list[str]:
|
||||
"""Names of all diatonic triads in this key."""
|
||||
return [c.identify() for c in self._scale.harmonize()]
|
||||
|
||||
@property
|
||||
def seventh_chords(self):
|
||||
def seventh_chords(self) -> list[str]:
|
||||
"""Names of all diatonic seventh chords in this key."""
|
||||
unique = len(self._scale.tones) - 1
|
||||
return [self._scale.seventh(i).identify() for i in range(unique)]
|
||||
|
||||
def triad(self, degree):
|
||||
def triad(self, degree: int) -> Chord:
|
||||
"""Build a diatonic triad on the given degree (0-indexed)."""
|
||||
return self._scale.triad(degree)
|
||||
|
||||
def seventh(self, degree):
|
||||
def seventh(self, degree: int) -> Chord:
|
||||
"""Build a diatonic seventh chord on the given degree (0-indexed)."""
|
||||
return self._scale.seventh(degree)
|
||||
|
||||
def progression(self, *numerals):
|
||||
def progression(self, *numerals: str) -> list[Chord]:
|
||||
"""Build a chord progression from Roman numerals.
|
||||
|
||||
Example::
|
||||
@@ -399,7 +425,7 @@ class Key:
|
||||
"""
|
||||
return self._scale.progression(*numerals)
|
||||
|
||||
def nashville(self, *numbers):
|
||||
def nashville(self, *numbers: Union[int, str]) -> list[Chord]:
|
||||
"""Build a chord progression using Nashville numbers.
|
||||
|
||||
Example::
|
||||
@@ -408,7 +434,7 @@ class Key:
|
||||
"""
|
||||
return self._scale.nashville(*numbers)
|
||||
|
||||
def secondary_dominant(self, degree):
|
||||
def secondary_dominant(self, degree: int) -> Chord:
|
||||
"""Build a secondary dominant (V/x) for the given scale degree.
|
||||
|
||||
A secondary dominant is the dominant chord of a non-tonic
|
||||
@@ -441,7 +467,7 @@ class Key:
|
||||
return Chord(tones=[root, root.add(4), root.add(7), root.add(10)])
|
||||
|
||||
@classmethod
|
||||
def all_keys(cls):
|
||||
def all_keys(cls) -> list[Key]:
|
||||
"""Return all 24 major and minor keys.
|
||||
|
||||
Returns:
|
||||
@@ -461,7 +487,7 @@ class Key:
|
||||
return keys
|
||||
|
||||
@property
|
||||
def relative(self):
|
||||
def relative(self) -> Optional[Key]:
|
||||
"""The relative major or minor key.
|
||||
|
||||
If this is a major key, returns the relative minor (vi).
|
||||
@@ -478,7 +504,7 @@ class Key:
|
||||
return None
|
||||
|
||||
@property
|
||||
def parallel(self):
|
||||
def parallel(self) -> Optional[Key]:
|
||||
"""The parallel major or minor key (same tonic, different mode)."""
|
||||
if self.mode == "major":
|
||||
return Key(self.tonic_name, "minor")
|
||||
@@ -488,7 +514,13 @@ class Key:
|
||||
|
||||
|
||||
class TonedScale:
|
||||
def __init__(self, *, system=SYSTEMS["western"], tonic):
|
||||
def __init__(self, *, system: Union[str, System] = SYSTEMS["western"], tonic: Union[str, Tone]) -> None:
|
||||
"""Initialize a TonedScale with a tonic note and tone system.
|
||||
|
||||
Args:
|
||||
system: A tone system name or :class:`System` instance.
|
||||
tonic: The tonic note as a string (e.g. ``"C4"``) or :class:`Tone`.
|
||||
"""
|
||||
if isinstance(system, str):
|
||||
system = SYSTEMS[system]
|
||||
self.system = system
|
||||
@@ -497,28 +529,40 @@ class TonedScale:
|
||||
tonic = Tone.from_string(tonic, system=self.system)
|
||||
|
||||
self.tonic = tonic
|
||||
self._cached_scales: Optional[dict[str, Scale]] = None
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<TonedScale system={self.system!r} tonic={self.tonic}>"
|
||||
|
||||
def __getitem__(self, scale):
|
||||
def __getitem__(self, scale: str) -> Scale:
|
||||
"""Retrieve a scale by name.
|
||||
|
||||
Raises:
|
||||
KeyError: If the named scale is not found in this system.
|
||||
"""
|
||||
result = self.get(scale)
|
||||
if result is None:
|
||||
raise KeyError(scale)
|
||||
return result
|
||||
|
||||
def get(self, scale):
|
||||
def get(self, scale: str) -> Optional[Scale]:
|
||||
"""Look up a scale by name, returning ``None`` if not found."""
|
||||
try:
|
||||
return self._scales[scale]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def scales(self):
|
||||
def scales(self) -> tuple[str, ...]:
|
||||
"""Tuple of all available scale names in this system."""
|
||||
return tuple(self._scales.keys())
|
||||
|
||||
@property
|
||||
def _scales(self):
|
||||
def _scales(self) -> dict[str, Scale]:
|
||||
"""Lazily computed (and cached) mapping of scale names to Scale objects."""
|
||||
if self._cached_scales is not None:
|
||||
return self._cached_scales
|
||||
|
||||
scales = {}
|
||||
|
||||
for scale_type in self.system.scales:
|
||||
@@ -536,4 +580,5 @@ class TonedScale:
|
||||
|
||||
scales[scale] = Scale(tones=tuple(working_scale))
|
||||
|
||||
self._cached_scales = scales
|
||||
return scales
|
||||
|
||||
+127
-44
@@ -1,3 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
from ._statics import REFERENCE_A, TEMPERAMENTS
|
||||
|
||||
|
||||
@@ -20,7 +24,24 @@ class Interval:
|
||||
|
||||
class Tone:
|
||||
|
||||
def __init__(self, name, *, alt_names=None, octave=None, system="western"):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
alt_names: Optional[list[str]] = None,
|
||||
octave: Optional[int] = None,
|
||||
system: Union[str, object] = "western",
|
||||
) -> None:
|
||||
"""Initialize a Tone with a name, optional octave, and musical system.
|
||||
|
||||
Args:
|
||||
name: The note name (e.g. ``"C"``, ``"C#4"``). If the name
|
||||
contains a digit, it is parsed as the octave.
|
||||
alt_names: Alternate spellings for this tone (e.g. enharmonics).
|
||||
octave: The octave number. Overrides any octave parsed from *name*.
|
||||
system: The tuning system, either as a string key (``"western"``)
|
||||
or a ``ToneSystem`` instance.
|
||||
"""
|
||||
if alt_names is None:
|
||||
alt_names = []
|
||||
|
||||
@@ -38,6 +59,7 @@ class Tone:
|
||||
self.name = name
|
||||
self.octave = octave
|
||||
self.alt_names = alt_names
|
||||
self._frequency: Optional[float] = None
|
||||
|
||||
if isinstance(system, str):
|
||||
self.system_name = system
|
||||
@@ -47,11 +69,16 @@ class Tone:
|
||||
self._system = system
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
def exists(self) -> bool:
|
||||
"""True if this tone's name is found in the associated system."""
|
||||
return self.name in self.system.tones
|
||||
|
||||
@property
|
||||
def system(self):
|
||||
def system(self) -> object:
|
||||
"""The ``ToneSystem`` associated with this tone.
|
||||
|
||||
Lazily resolved from ``system_name`` on first access and cached.
|
||||
"""
|
||||
from .systems import SYSTEMS
|
||||
|
||||
if self._system:
|
||||
@@ -62,32 +89,34 @@ class Tone:
|
||||
return self.system
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
def full_name(self) -> str:
|
||||
"""The tone name with octave appended, e.g. ``'C4'`` or ``'C'``."""
|
||||
if self.octave is not None:
|
||||
return f"{self.name}{self.octave}"
|
||||
else:
|
||||
return self.name
|
||||
|
||||
def names(self):
|
||||
def names(self) -> list[str]:
|
||||
"""Return a list containing the primary name and all alternate names."""
|
||||
return [self.name] + self.alt_names
|
||||
|
||||
@property
|
||||
def is_natural(self):
|
||||
def is_natural(self) -> bool:
|
||||
"""True if this is a natural note (no sharp or flat)."""
|
||||
return not self.is_sharp and not self.is_flat
|
||||
|
||||
@property
|
||||
def is_sharp(self):
|
||||
def is_sharp(self) -> bool:
|
||||
"""True if this tone has a sharp (#)."""
|
||||
return "#" in self.name
|
||||
|
||||
@property
|
||||
def is_flat(self):
|
||||
def is_flat(self) -> bool:
|
||||
"""True if this tone has a flat (b after the first character)."""
|
||||
return "b" in self.name[1:]
|
||||
|
||||
@property
|
||||
def enharmonic(self):
|
||||
def enharmonic(self) -> Optional[str]:
|
||||
"""The enharmonic equivalent of this tone, or None if there isn't one.
|
||||
|
||||
Returns the alternate spelling: C# → Db, Db → C#, etc.
|
||||
@@ -109,16 +138,16 @@ class Tone:
|
||||
pass
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Tone {self.full_name}>"
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.full_name
|
||||
|
||||
def __add__(self, interval):
|
||||
def __add__(self, interval: int) -> Tone:
|
||||
return self.add(interval)
|
||||
|
||||
def __sub__(self, other):
|
||||
def __sub__(self, other: Union[int, Tone]) -> Union[Tone, int]:
|
||||
# Tone - int: subtract semitones
|
||||
if isinstance(other, int):
|
||||
return self.subtract(other)
|
||||
@@ -134,27 +163,27 @@ class Tone:
|
||||
return self_from_c0 - other_from_c0
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other):
|
||||
def __lt__(self, other: Tone) -> bool:
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() < other.pitch()
|
||||
|
||||
def __le__(self, other):
|
||||
def __le__(self, other: Tone) -> bool:
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() <= other.pitch()
|
||||
|
||||
def __gt__(self, other):
|
||||
def __gt__(self, other: Tone) -> bool:
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() > other.pitch()
|
||||
|
||||
def __ge__(self, other):
|
||||
def __ge__(self, other: Tone) -> bool:
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() >= other.pitch()
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
|
||||
# Comparing string literals.
|
||||
if isinstance(other, str):
|
||||
@@ -169,11 +198,20 @@ class Tone:
|
||||
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.name, self.octave))
|
||||
|
||||
@classmethod
|
||||
def from_string(klass, s, system=None):
|
||||
def from_string(klass, s: str, system: Optional[Union[str, object]] = None) -> Tone:
|
||||
"""Create a Tone by parsing a string like ``'C#4'`` or ``'Bb'``.
|
||||
|
||||
Args:
|
||||
s: A note string, optionally including an octave number.
|
||||
system: The tuning system to associate with the tone.
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
try:
|
||||
octave = int("".join([c for c in filter(str.isdigit, s)]))
|
||||
except ValueError:
|
||||
@@ -187,7 +225,16 @@ class Tone:
|
||||
return klass(name=tone, octave=octave)
|
||||
|
||||
@classmethod
|
||||
def from_tuple(klass, t):
|
||||
def from_tuple(klass, t: tuple[str, ...]) -> Tone:
|
||||
"""Create a Tone from a tuple of ``(name, *alt_names)``.
|
||||
|
||||
Args:
|
||||
t: A tuple where the first element is the primary name and
|
||||
any remaining elements are alternate names (enharmonics).
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
if len(t) == 1:
|
||||
return klass.from_string(s=t[0])
|
||||
else:
|
||||
@@ -196,7 +243,7 @@ class Tone:
|
||||
return tone
|
||||
|
||||
@classmethod
|
||||
def from_frequency(klass, hz, system="western"):
|
||||
def from_frequency(klass, hz: float, system: Union[str, object] = "western") -> Tone:
|
||||
"""Create a Tone from a frequency in Hz.
|
||||
|
||||
Finds the nearest note in 12-TET tuning (A4=440Hz).
|
||||
@@ -228,7 +275,7 @@ class Tone:
|
||||
return klass.from_index(index, octave=octave, system=system)
|
||||
|
||||
@classmethod
|
||||
def from_midi(klass, note_number, system="western"):
|
||||
def from_midi(klass, note_number: int, system: Union[str, object] = "western") -> Tone:
|
||||
"""Create a Tone from a MIDI note number.
|
||||
|
||||
MIDI note 60 = C4 (middle C), 69 = A4 (440 Hz).
|
||||
@@ -251,18 +298,33 @@ class Tone:
|
||||
return klass.from_index(index, octave=octave, system=system)
|
||||
|
||||
@classmethod
|
||||
def from_index(klass, i, *, octave, system):
|
||||
def from_index(klass, i: int, *, octave: int, system: object) -> Tone:
|
||||
"""Create a Tone from its index within a tuning system.
|
||||
|
||||
Args:
|
||||
i: The index of the tone in the system's tone list.
|
||||
octave: The octave number.
|
||||
system: The ``ToneSystem`` instance.
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
tone = system.tones[i].name
|
||||
return klass(name=tone, octave=octave, system=system)
|
||||
|
||||
@property
|
||||
def _index(self):
|
||||
def _index(self) -> int:
|
||||
"""The index of this tone within its associated system's tone list.
|
||||
|
||||
Raises:
|
||||
ValueError: If no system is associated with this tone.
|
||||
"""
|
||||
try:
|
||||
return self.system.tones.index(self.name)
|
||||
except AttributeError:
|
||||
raise ValueError("Tone index cannot be referenced without a system!")
|
||||
|
||||
def _math(self, interval):
|
||||
def _math(self, interval: int) -> tuple[int, int]:
|
||||
"""Returns (new index, new octave).
|
||||
|
||||
Octave boundaries follow scientific pitch notation, where the
|
||||
@@ -292,11 +354,27 @@ class Tone:
|
||||
|
||||
return (new_index, new_octave)
|
||||
|
||||
def add(self, interval):
|
||||
def add(self, interval: int) -> Tone:
|
||||
"""Return a new Tone that is *interval* semitones above this one.
|
||||
|
||||
Args:
|
||||
interval: Number of semitones to add (positive = up).
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
index, octave = self._math(interval)
|
||||
return self.from_index(index, octave=octave, system=self.system)
|
||||
|
||||
def subtract(self, interval):
|
||||
def subtract(self, interval: int) -> Tone:
|
||||
"""Return a new Tone that is *interval* semitones below this one.
|
||||
|
||||
Args:
|
||||
interval: Number of semitones to subtract (positive = down).
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
return self.add((-1 * interval))
|
||||
|
||||
_INTERVAL_NAMES = {
|
||||
@@ -306,7 +384,7 @@ class Tone:
|
||||
12: "octave",
|
||||
}
|
||||
|
||||
def interval_to(self, other):
|
||||
def interval_to(self, other: Tone) -> str:
|
||||
"""Name the interval between this tone and another.
|
||||
|
||||
Returns a string like ``"perfect 5th"``, ``"major 3rd"``, or
|
||||
@@ -335,7 +413,7 @@ class Tone:
|
||||
return f"{name} + {octaves} octaves"
|
||||
|
||||
@property
|
||||
def midi(self):
|
||||
def midi(self) -> Optional[int]:
|
||||
"""MIDI note number (C4 = 60, A4 = 69).
|
||||
|
||||
The MIDI standard assigns integer note numbers from 0–127.
|
||||
@@ -350,7 +428,7 @@ class Tone:
|
||||
semitones_from_c0 = ((self._index - c_index) % 12) + (self.octave * 12)
|
||||
return semitones_from_c0 + 12 # MIDI C0 = 12 (C-1 = 0)
|
||||
|
||||
def transpose(self, semitones):
|
||||
def transpose(self, semitones: int) -> Tone:
|
||||
"""Return a new Tone transposed by the given number of semitones.
|
||||
|
||||
Alias for ``tone + semitones`` / ``tone - semitones``. Positive
|
||||
@@ -358,7 +436,7 @@ class Tone:
|
||||
"""
|
||||
return self.add(semitones)
|
||||
|
||||
def circle_of_fifths(self):
|
||||
def circle_of_fifths(self) -> list[Tone]:
|
||||
"""The 12 tones of the circle of fifths starting from this tone.
|
||||
|
||||
Each step ascends by a perfect fifth (7 semitones). After 12
|
||||
@@ -372,14 +450,14 @@ class Tone:
|
||||
Returns:
|
||||
A list of 12 Tones.
|
||||
"""
|
||||
tones = []
|
||||
tones: list[Tone] = []
|
||||
t = self
|
||||
for _ in range(12):
|
||||
tones.append(t)
|
||||
t = t.add(7)
|
||||
return tones
|
||||
|
||||
def circle_of_fourths(self):
|
||||
def circle_of_fourths(self) -> list[Tone]:
|
||||
"""The 12 tones of the circle of fourths starting from this tone.
|
||||
|
||||
Each step ascends by a perfect fourth (5 semitones) — the
|
||||
@@ -390,7 +468,7 @@ class Tone:
|
||||
Returns:
|
||||
A list of 12 Tones.
|
||||
"""
|
||||
tones = []
|
||||
tones: list[Tone] = []
|
||||
t = self
|
||||
for _ in range(12):
|
||||
tones.append(t)
|
||||
@@ -398,11 +476,16 @@ class Tone:
|
||||
return tones
|
||||
|
||||
@property
|
||||
def frequency(self):
|
||||
"""The frequency of this tone in Hz (equal temperament, A4=440)."""
|
||||
return self.pitch()
|
||||
def frequency(self) -> float:
|
||||
"""The frequency of this tone in Hz (equal temperament, A4=440).
|
||||
|
||||
def overtones(self, n=8):
|
||||
The result is cached after the first computation.
|
||||
"""
|
||||
if self._frequency is None:
|
||||
self._frequency = self.pitch()
|
||||
return self._frequency
|
||||
|
||||
def overtones(self, n: int = 8) -> list[float]:
|
||||
"""The first *n* overtones (harmonic series) of this tone.
|
||||
|
||||
The harmonic series is the foundation of timbre and consonance.
|
||||
@@ -439,11 +522,11 @@ class Tone:
|
||||
def pitch(
|
||||
self,
|
||||
*,
|
||||
reference_pitch=REFERENCE_A,
|
||||
temperament="equal",
|
||||
symbolic=False,
|
||||
precision=None,
|
||||
):
|
||||
reference_pitch: float = REFERENCE_A,
|
||||
temperament: str = "equal",
|
||||
symbolic: bool = False,
|
||||
precision: Optional[int] = None,
|
||||
) -> float:
|
||||
try:
|
||||
tones = len(self.system.tones)
|
||||
except AttributeError:
|
||||
|
||||
+1
-1
@@ -2622,7 +2622,7 @@ def test_tension_empty():
|
||||
|
||||
def test_version():
|
||||
import pytheory
|
||||
assert pytheory.__version__ == "0.3.1"
|
||||
assert pytheory.__version__ == "0.3.2"
|
||||
|
||||
|
||||
def test_all_exports():
|
||||
|
||||
Reference in New Issue
Block a user