mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9624609f3b |
@@ -25,11 +25,8 @@ jobs:
|
||||
- name: Set up Python
|
||||
run: uv python install 3.13
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-groups
|
||||
|
||||
- name: Build docs
|
||||
run: uv run sphinx-build -b html docs docs/_build/html
|
||||
run: uv run --group docs sphinx-build -b html docs docs/_build/html
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
@@ -1,145 +1,57 @@
|
||||
# PyTheory: Music Theory for Humans
|
||||
|
||||
This library makes exploring music theory approachable and fun, treating Python as a musical instrument.
|
||||
This (work in progress) library attempts to make exploring music theory approachable to humans.
|
||||
|
||||
## Installation
|
||||

|
||||
|
||||
```
|
||||
$ pip install pytheory
|
||||
```
|
||||
|
||||
## Tones
|
||||
|
||||
```pycon
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> c4.frequency
|
||||
261.63
|
||||
|
||||
>>> c4 + 7 # perfect fifth
|
||||
<Tone G4>
|
||||
|
||||
>>> c4.interval_to(c4 + 7)
|
||||
'perfect 5th'
|
||||
|
||||
>>> c4.midi
|
||||
60
|
||||
|
||||
>>> Tone.from_frequency(440)
|
||||
<Tone A4>
|
||||
|
||||
>>> Tone.from_midi(69)
|
||||
<Tone A4>
|
||||
```
|
||||
|
||||
## Scales and Modes
|
||||
## True Scale -> Pitch Evaluation
|
||||
|
||||
```pycon
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> c_major = TonedScale(tonic="C4")["major"]
|
||||
>>> c_major.note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
>>> c_minor = TonedScale(tonic='C4')['minor']
|
||||
|
||||
>>> TonedScale(tonic="C4")["dorian"].note_names
|
||||
['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C']
|
||||
>>> c_minor
|
||||
<Scale I=C4 II=D4 III=Eb4 IV=F4 V=G4 VI=Ab4 VII=Bb5 VIII=C5>
|
||||
|
||||
>>> c_minor[0].pitch()
|
||||
523.251130601197
|
||||
|
||||
>>> c_minor["I"].pitch(symbolic=True)
|
||||
440*2**(1/4)
|
||||
|
||||
>>> c_minor["tonic"].pitch(temperament='pythagorean', symbolic=True)
|
||||
14080/27
|
||||
```
|
||||
|
||||
## Diatonic Harmony
|
||||
## Audibly play a note (or chord)
|
||||
|
||||
>>> from pytheory import play
|
||||
play(c_minor[0], t=1_000)
|
||||
|
||||
|
||||
## Chord Fingerings for Custom Tunings
|
||||
|
||||
```pycon
|
||||
>>> c_major.triad(0).identify()
|
||||
'C major'
|
||||
>>> from pytheory import Tone, Fretboard, CHARTS
|
||||
|
||||
>>> c_major.seventh(4).identify()
|
||||
'G dominant 7th'
|
||||
>>> tones = (
|
||||
... Tone.from_string("F2"),
|
||||
... Tone.from_string("C3"),
|
||||
... Tone.from_string("G3"),
|
||||
... Tone.from_string("D4"),
|
||||
... Tone.from_string("A5"),
|
||||
... Tone.from_string("E5")
|
||||
... )
|
||||
|
||||
>>> [c.identify() for c in c_major.harmonize()]
|
||||
['C major', 'D minor', 'E minor', 'F major', 'G major', 'A minor', 'B diminished']
|
||||
>>> fretboard = Fretboard(tones=tones)
|
||||
>>>
|
||||
>>> c_chord = CHARTS['western']["C"]
|
||||
|
||||
>>> [c.identify() for c in c_major.progression("I", "V", "vi", "IV")]
|
||||
['C major', 'G major', 'A minor', 'F major']
|
||||
>>> print(c_chord.fingering(fretboard=fretboard))
|
||||
(0, 0, 0, 3, 3, 3)
|
||||
```
|
||||
|
||||
## Chord Analysis
|
||||
It can also [generate charts for all known chords](https://gist.github.com/kennethreitz/b363660145064fc330c206294cff92fc) for any instrument (accuracy to be determined!).
|
||||
|
||||
```pycon
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
>>> C4 = Tone.from_string("C4", system="western")
|
||||
>>> G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
>>> g7 = Chord([G4, G4+4, G4+7, G4+10])
|
||||
>>> g7.identify()
|
||||
'G dominant 7th'
|
||||
|
||||
>>> g7.analyze("C")
|
||||
'V7'
|
||||
|
||||
>>> g7.tension
|
||||
{'score': 0.6, 'tritones': 1, 'minor_seconds': 0, 'has_dominant_function': True}
|
||||
|
||||
>>> g7.transpose(-7).identify()
|
||||
'C dominant 7th'
|
||||
```
|
||||
|
||||
## Six Musical Systems
|
||||
|
||||
```pycon
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> TonedScale(tonic="Sa4", system="indian")["bhairav"].note_names
|
||||
['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
|
||||
>>> TonedScale(tonic="Do4", system="arabic")["hijaz"].note_names
|
||||
['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
|
||||
|
||||
>>> TonedScale(tonic="C4", system="japanese")["hirajoshi"].note_names
|
||||
['C', 'D', 'D#', 'G', 'G#', 'C']
|
||||
|
||||
>>> TonedScale(tonic="C4", system="blues")["blues"].note_names
|
||||
['C', 'D#', 'F', 'F#', 'G', 'A#', 'C']
|
||||
```
|
||||
|
||||
## 25 Instrument Presets
|
||||
|
||||
```pycon
|
||||
>>> from pytheory import Fretboard, CHARTS
|
||||
|
||||
>>> Fretboard.guitar() # standard tuning
|
||||
>>> Fretboard.guitar("drop d") # 8 alternate tunings
|
||||
>>> Fretboard.mandolin() # + mandola, octave mandolin, mandocello
|
||||
>>> Fretboard.violin() # + viola, cello, double bass
|
||||
>>> Fretboard.ukulele() # + banjo, harp, charango, erhu...
|
||||
>>> Fretboard.keyboard() # 88-key piano
|
||||
>>> Fretboard.keyboard(25, "C3") # 25-key MIDI controller
|
||||
|
||||
>>> CHARTS['western']['Am'].fingering(fretboard=Fretboard.guitar())
|
||||
(0, 1, 2, 2, 0, 0)
|
||||
```
|
||||
|
||||
## Audio Playback
|
||||
|
||||
```pycon
|
||||
>>> from pytheory import play, Synth, Tone
|
||||
|
||||
>>> tone = Tone.from_string("A4", system="western")
|
||||
>>> play(tone, t=1_000) # sine wave, 1 second
|
||||
>>> play(tone, synth=Synth.SAW, t=1_000) # sawtooth wave
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **6 musical systems**: Western, Indian (Hindustani), Arabic (Maqam), Japanese, Blues/Pentatonic, Javanese Gamelan
|
||||
- **40+ scales**: major, minor, harmonic minor, 7 modes, 10 thaats, 10 maqamat, pentatonic, blues, hirajoshi, pelog, slendro, and more
|
||||
- **Chord analysis**: identification (17 types), Roman numeral analysis, tension scoring, voice leading, Plomp-Levelt dissonance, beat frequencies
|
||||
- **Diatonic harmony**: triads, seventh chords, harmonize entire scales, build progressions from Roman numerals
|
||||
- **25 instrument presets**: guitar (8 tunings), 12-string, bass, mandolin family, violin family, banjo, harp, oud, sitar, shamisen, erhu, charango, pipa, balalaika, lute, pedal steel, keyboard
|
||||
- **Pitch tools**: frequency ↔ tone conversion, MIDI ↔ tone, interval naming, circle of fifths, overtone series, transposition
|
||||
- **3 temperaments**: equal, Pythagorean, quarter-comma meantone
|
||||
- **Audio synthesis**: sine, sawtooth, and triangle wave playback
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation with music theory guides: **[pytheory.kennethreitz.org](https://pytheory.kennethreitz.org)**
|
||||
✨🍰✨
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pytheory.kennethreitz.org
|
||||
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 78 KiB |
Vendored
-18
@@ -1,18 +0,0 @@
|
||||
{% extends "!layout.html" %}
|
||||
{% block footer %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
var _gauges = _gauges || [];
|
||||
(function() {
|
||||
var t = document.createElement('script');
|
||||
t.type = 'text/javascript';
|
||||
t.async = true;
|
||||
t.id = 'gauges-tracker';
|
||||
t.setAttribute('data-site-id', '69bfc431e7e47c1200fc74bc');
|
||||
t.setAttribute('data-track-path', 'https://track.gaug.es/track.gif');
|
||||
t.src = 'https://d2fuc4clr7gvcn.cloudfront.net/track.js';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(t, s);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
+3
-13
@@ -1,16 +1,12 @@
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
# Mock sounddevice so Sphinx can import pytheory.play without PortAudio
|
||||
sys.modules["sounddevice"] = MagicMock()
|
||||
|
||||
project = "PyTheory"
|
||||
copyright = "2026, Kenneth Reitz"
|
||||
copyright = "2024, Kenneth Reitz"
|
||||
author = "Kenneth Reitz"
|
||||
release = "0.3.2"
|
||||
release = "0.2.0"
|
||||
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
@@ -34,11 +30,5 @@ templates_path = ["_templates"]
|
||||
exclude_patterns = ["_build"]
|
||||
|
||||
html_theme = "alabaster"
|
||||
html_theme_options = {
|
||||
"github_user": "kennethreitz",
|
||||
"github_repo": "pytheory",
|
||||
"github_banner": True,
|
||||
"description": "Music Theory for Humans",
|
||||
}
|
||||
html_title = "PyTheory"
|
||||
html_static_path = ["_static"]
|
||||
html_extra_path = ["CNAME"]
|
||||
|
||||
+33
-308
@@ -1,93 +1,19 @@
|
||||
Working with Chords
|
||||
===================
|
||||
|
||||
A `chord <https://en.wikipedia.org/wiki/Chord_(music)>`_ is two or more tones sounding simultaneously. Chords are the
|
||||
vertical dimension of music — while melody moves horizontally through
|
||||
time, harmony stacks tones on top of each other.
|
||||
Chords and Chord Charts
|
||||
-----------------------
|
||||
|
||||
Chord Construction
|
||||
------------------
|
||||
PyTheory provides two chord-related classes:
|
||||
|
||||
Chords are built by stacking **intervals** above a **root** note. The
|
||||
most common chord type is the `triad <https://en.wikipedia.org/wiki/Triad_(music)>`_ — three notes built from
|
||||
alternating scale degrees (root, 3rd, 5th).
|
||||
|
||||
The four triad types::
|
||||
|
||||
Major root + major 3rd (4) + perfect 5th (7) Bright, stable
|
||||
Minor root + minor 3rd (3) + perfect 5th (7) Dark, sad
|
||||
Diminished root + minor 3rd (3) + diminished 5th (6) Tense, unstable
|
||||
Augmented root + major 3rd (4) + augmented 5th (8) Eerie, unresolved
|
||||
|
||||
Adding a 7th creates a `seventh chord <https://en.wikipedia.org/wiki/Seventh_chord>`_ — the foundation of jazz
|
||||
harmony::
|
||||
|
||||
Dominant 7th root + 4 + 7 + 10 Bluesy, wants to resolve (G7)
|
||||
Major 7th root + 4 + 7 + 11 Dreamy, sophisticated (Cmaj7)
|
||||
Minor 7th root + 3 + 7 + 10 Warm, mellow (Am7)
|
||||
Diminished 7th root + 3 + 6 + 9 Dramatic, symmetrical
|
||||
|
||||
Inversions
|
||||
----------
|
||||
|
||||
A chord is in **root position** when the root is the lowest note.
|
||||
When a different chord tone is in the bass, the chord is `inverted <https://en.wikipedia.org/wiki/Inversion_(music)>`_:
|
||||
|
||||
- **Root position**: C E G (root in bass)
|
||||
- **First inversion**: E G C (3rd in bass) — notated C/E
|
||||
- **Second inversion**: G C E (5th in bass) — notated C/G
|
||||
|
||||
Inversions change the color and weight of a chord without changing its
|
||||
identity. First inversion sounds lighter; second inversion sounds
|
||||
suspended, often used as a passing chord.
|
||||
|
||||
For seventh chords, there's also **third inversion** (7th in bass):
|
||||
|
||||
- G7 in third inversion: F G B D (notated G7/F)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Chord, Tone
|
||||
|
||||
# All three are "C major" — identify() finds the root
|
||||
root = Chord([Tone.from_string(n, system="western") for n in ["C4", "E4", "G4"]])
|
||||
first = Chord([Tone.from_string(n, system="western") for n in ["E3", "G3", "C4"]])
|
||||
second = Chord([Tone.from_string(n, system="western") for n in ["G3", "C4", "E4"]])
|
||||
|
||||
root.identify() # 'C major'
|
||||
first.identify() # 'C major'
|
||||
second.identify() # 'C major'
|
||||
|
||||
Extended Chords
|
||||
---------------
|
||||
|
||||
Beyond seventh chords, jazz harmony builds `extended chords <https://en.wikipedia.org/wiki/Extended_chord>`_ by
|
||||
continuing to stack thirds:
|
||||
|
||||
- **9th chord**: adds the 9th (= 2nd, one octave up)
|
||||
- **11th chord**: adds the 9th and 11th (= 4th)
|
||||
- **13th chord**: adds the 9th, 11th, and 13th (= 6th)
|
||||
|
||||
A full 13th chord contains all 7 notes of the scale! In practice,
|
||||
tones are usually omitted — the 5th is typically dropped first, then
|
||||
the 11th (which clashes with the 3rd in dominant chords).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
scale = TonedScale(tonic="C4")["major"]
|
||||
|
||||
# Build a Cmaj9 from the scale: C E G B D
|
||||
cmaj9 = scale.chord(0, 2, 4, 6, 8)
|
||||
|
||||
# Build a full C13 (in theory): C E G B D F A
|
||||
c13 = scale.chord(0, 2, 4, 6, 8, 10, 12)
|
||||
- :class:`~pytheory.chords.Chord` — a collection of tones played together
|
||||
- :class:`~pytheory.charts.NamedChord` — a chord from the chart database with
|
||||
fingering support
|
||||
|
||||
Using the Chord Chart
|
||||
---------------------
|
||||
|
||||
PyTheory includes 144 pre-built chords (12 roots x 12 qualities):
|
||||
The built-in chart contains 144 chords (12 roots x 12 qualities):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -95,37 +21,29 @@ PyTheory includes 144 pre-built chords (12 roots x 12 qualities):
|
||||
|
||||
chart = CHARTS["western"]
|
||||
|
||||
c_major = chart["C"] # C major (root position)
|
||||
a_minor = chart["Am"] # A minor
|
||||
g_seven = chart["G7"] # G dominant 7th
|
||||
d_dim = chart["Ddim"] # D diminished
|
||||
# Access a chord
|
||||
c_major = chart["C"]
|
||||
a_minor = chart["Am"]
|
||||
g_seven = chart["G7"]
|
||||
|
||||
Available qualities:
|
||||
# Available qualities: "", "maj", "m", "5", "7", "9",
|
||||
# "dim", "m6", "m7", "m9", "maj7", "maj9"
|
||||
|
||||
============ ================ ================================
|
||||
Quality Intervals Example tones (from C)
|
||||
============ ================ ================================
|
||||
``""`` 4, 7 C E G (major triad)
|
||||
``"maj"`` 4, 7 C E G (explicit major)
|
||||
``"m"`` 3, 7 C Eb G (minor triad)
|
||||
``"5"`` 7 C G (power chord)
|
||||
``"7"`` 4, 7, 10 C E G Bb (dominant 7th)
|
||||
``"9"`` 4, 7, 10, 14 C E G Bb D (dominant 9th)
|
||||
``"dim"`` 3, 6 C Eb Gb (diminished)
|
||||
``"m6"`` 3, 7, 9 C Eb G A (minor 6th)
|
||||
``"m7"`` 3, 7, 10 C Eb G Bb (minor 7th)
|
||||
``"m9"`` 3, 7, 10, 14 C Eb G Bb D (minor 9th)
|
||||
``"maj7"`` 4, 7, 11 C E G B (major 7th)
|
||||
``"maj9"`` 4, 7, 11, 14 C E G B D (major 9th)
|
||||
============ ================ ================================
|
||||
Chord Tones
|
||||
-----------
|
||||
|
||||
Each named chord knows which tones it contains:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> chart["C"].acceptable_tone_names
|
||||
('C', 'E', 'G')
|
||||
|
||||
>>> chart["Cm7"].acceptable_tone_names
|
||||
('C', 'D#', 'G', 'A#') # Eb and Bb shown as sharps
|
||||
>>> chart["Am"].acceptable_tone_names
|
||||
('A', 'C', 'E')
|
||||
|
||||
>>> chart["G7"].acceptable_tone_names
|
||||
('G', 'B', 'D', 'F')
|
||||
|
||||
Building Chords Manually
|
||||
-------------------------
|
||||
@@ -140,219 +58,26 @@ Building Chords Manually
|
||||
Tone.from_string("G4", system="western"),
|
||||
])
|
||||
|
||||
# Iteration
|
||||
for tone in c_major:
|
||||
print(tone)
|
||||
|
||||
len(c_major) # 3
|
||||
"C" in c_major # True
|
||||
|
||||
Intervals
|
||||
---------
|
||||
|
||||
The ``intervals`` property returns semitone distances between adjacent
|
||||
tones — these are musically meaningful and octave-invariant:
|
||||
Chord Properties
|
||||
----------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> c_major.intervals
|
||||
[4, 3] # major 3rd (4) + minor 3rd (3) = major triad
|
||||
# Frequency intervals between adjacent tones (Hz)
|
||||
c_major.intervals
|
||||
|
||||
>>> Chord(tones=[C4, Eb4, G4]).intervals
|
||||
[3, 4] # minor 3rd + major 3rd = minor triad
|
||||
# Harmony score (higher = more consonant intervals)
|
||||
c_major.harmony
|
||||
|
||||
Consonance and Dissonance
|
||||
-------------------------
|
||||
# Dissonance score (higher = wider intervals)
|
||||
c_major.dissonance
|
||||
|
||||
**Consonance** is the perception of stability and "pleasantness" when
|
||||
tones sound together. **Dissonance** is the perception of tension and
|
||||
roughness. Neither is inherently good or bad — music needs both.
|
||||
|
||||
Harmony Score
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The ``harmony`` property measures consonance using **frequency ratio
|
||||
simplicity**. The insight dates back to Pythagoras (6th century BC):
|
||||
intervals whose frequencies form simple integer ratios sound consonant.
|
||||
|
||||
=========== ===== ====================
|
||||
Interval Ratio Why it sounds "good"
|
||||
=========== ===== ====================
|
||||
Octave 2:1 Every 2nd wave aligns
|
||||
Perfect 5th 3:2 Every 3rd wave aligns
|
||||
Perfect 4th 4:3 Every 4th wave aligns
|
||||
Major 3rd 5:4 Every 5th wave aligns
|
||||
Minor 3rd 6:5 Every 6th wave aligns
|
||||
Tritone 45:32 Waves rarely align
|
||||
=========== ===== ====================
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
fifth = Chord([C4, G4])
|
||||
tritone = Chord([C4, F_sharp_4])
|
||||
|
||||
fifth.harmony > tritone.harmony # True
|
||||
# The perfect fifth's 3:2 ratio scores higher
|
||||
|
||||
Dissonance Score
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``dissonance`` property uses the Plomp-Levelt `roughness <https://en.wikipedia.org/wiki/Roughness_(psychoacoustics)>`_ model
|
||||
(1965). When two frequencies are close together, their sound waves
|
||||
interfere and produce rapid amplitude fluctuations called `beating <https://en.wikipedia.org/wiki/Beat_(acoustics)>`_.
|
||||
This beating is perceived as roughness — the physiological basis of
|
||||
dissonance.
|
||||
|
||||
The roughness depends on the frequency difference relative to the
|
||||
**critical bandwidth** of the human ear (~25% of the frequency at
|
||||
that register). Maximum roughness occurs when the difference equals
|
||||
the critical bandwidth.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Octave: frequencies far apart → low roughness
|
||||
octave = Chord([C4, C5])
|
||||
# Major 3rd: closer frequencies → higher roughness
|
||||
third = Chord([C4, E4])
|
||||
|
||||
octave.dissonance < third.dissonance # True
|
||||
|
||||
Beat Frequencies
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
When two tones with slightly different frequencies are played together,
|
||||
you hear a pulsing at the **beat frequency**: ``|f1 - f2|`` Hz.
|
||||
|
||||
- **< 1 Hz**: Slow pulsing, used for tuning instruments
|
||||
- **1–15 Hz**: Audible rhythmic beating
|
||||
- **15–30 Hz**: Perceived as buzzing/roughness
|
||||
- **> 30 Hz**: No longer beating — becomes part of the timbre
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
chord = Chord(tones=[A4, E5, A5])
|
||||
|
||||
# All pairwise beat frequencies, sorted ascending
|
||||
chord.beat_frequencies
|
||||
# [(A4, E5, 189.6), (E5, A5, 220.0), (A4, A5, 440.0)]
|
||||
|
||||
# The slowest (most perceptible) beat
|
||||
chord.beat_pulse # 189.6 Hz
|
||||
|
||||
Chord Identification
|
||||
--------------------
|
||||
|
||||
Give PyTheory any set of tones and it will tell you what chord it is.
|
||||
It tries every tone as a potential root and matches the interval pattern
|
||||
against 17 known chord types (triads, 7ths, 9ths, sus, power chords).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Chord, Tone
|
||||
|
||||
# Build a chord and identify it
|
||||
chord = Chord([
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("C5", system="western"),
|
||||
Tone.from_string("E5", system="western"),
|
||||
])
|
||||
chord.identify() # 'A minor'
|
||||
|
||||
# Works with any voicing or inversion
|
||||
chord2 = Chord([
|
||||
Tone.from_string("E4", system="western"),
|
||||
Tone.from_string("G4", system="western"),
|
||||
Tone.from_string("C5", system="western"),
|
||||
])
|
||||
chord2.identify() # 'C major' (first inversion detected)
|
||||
|
||||
Harmonic Analysis
|
||||
-----------------
|
||||
|
||||
`Roman numeral analysis <https://en.wikipedia.org/wiki/Roman_numeral_analysis>`_ labels each chord by its function within a
|
||||
key. This is how musicians describe chord progressions independent of
|
||||
key — "I-IV-V" means the same thing in C major (C-F-G) as in G major
|
||||
(G-C-D).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Chord, Tone
|
||||
|
||||
C4 = Tone.from_string("C4", system="western")
|
||||
D4 = Tone.from_string("D4", system="western")
|
||||
E4 = Tone.from_string("E4", system="western")
|
||||
F4 = Tone.from_string("F4", system="western")
|
||||
G4 = Tone.from_string("G4", system="western")
|
||||
A4 = Tone.from_string("A4", system="western")
|
||||
B4 = Tone.from_string("B4", system="western")
|
||||
|
||||
Chord([C4, E4, G4]).analyze("C") # 'I' (tonic)
|
||||
Chord([D4, F4, A4]).analyze("C") # 'ii' (supertonic minor)
|
||||
Chord([G4, B4, G4+5]).analyze("C") # 'V' (dominant)
|
||||
Chord([G4, B4, G4+5, G4+10]).analyze("C") # 'V7' (dominant 7th)
|
||||
|
||||
Tension and Resolution
|
||||
----------------------
|
||||
|
||||
**Tension** is what makes music move forward. Without it, there's no
|
||||
desire to resolve — no drama, no narrative. The ``tension`` property
|
||||
quantifies this based on:
|
||||
|
||||
- **Tritones** (6 semitones): the most unstable interval. The tritone
|
||||
between the 3rd and 7th of a dominant chord (e.g. B and F in G7)
|
||||
creates the strongest pull toward resolution.
|
||||
- **Minor 2nds**: semitone clashes that add bite and urgency.
|
||||
- **Dominant function**: the specific combination of a major 3rd and
|
||||
minor 7th above the root — the hallmark of the V7 chord.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# A C major triad is fully resolved — no tension
|
||||
c_major = Chord([C4, E4, G4])
|
||||
c_major.tension['score'] # 0.0
|
||||
c_major.tension['tritones'] # 0
|
||||
|
||||
# G7 is loaded with tension — it wants to resolve to C
|
||||
g7 = Chord([G4, B4, G4+5, G4+10])
|
||||
g7.tension['score'] # 0.6
|
||||
g7.tension['tritones'] # 1
|
||||
g7.tension['has_dominant_function'] # True
|
||||
|
||||
Voice Leading
|
||||
-------------
|
||||
|
||||
`Voice leading <https://en.wikipedia.org/wiki/Voice_leading>`_ is the art of connecting chords smoothly. Instead of
|
||||
jumping all voices to new positions, good voice leading moves each note
|
||||
the minimum distance to reach the next chord. Bach's chorales are the
|
||||
gold standard — every voice moves by step whenever possible.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
c_maj = Chord([C4, E4, G4])
|
||||
f_maj = Chord([F4, A4, C4+12])
|
||||
|
||||
for src, dst, motion in c_maj.voice_leading(f_maj):
|
||||
print(f"{src} -> {dst} ({motion:+d} semitones)")
|
||||
# Each voice moves the minimum distance to reach the target chord
|
||||
|
||||
The Overtone Series
|
||||
-------------------
|
||||
|
||||
Every musical tone is actually a stack of frequencies — the
|
||||
**fundamental** plus its `overtones <https://en.wikipedia.org/wiki/Overtone>`_ (harmonics). The overtone series
|
||||
is nature's chord: it contains the octave, perfect fifth, perfect
|
||||
fourth, major third, and more, in that order.
|
||||
|
||||
This is *why* consonance exists. When you play C and G together, the
|
||||
overtones of C already contain G. The two tones share acoustic energy,
|
||||
reinforcing each other. A dissonant interval like C and C# shares
|
||||
almost no overtones — the waves clash.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Tone
|
||||
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
a4.overtones(8)
|
||||
# [440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0, 3080.0, 3520.0]
|
||||
# A4 A5 E6 A6 C#7 E7 ~G7 A7
|
||||
# fund. oct. 5th+oct 2oct 3rd 5th ~7th 3oct
|
||||
# Beat frequency between closest tone pair
|
||||
c_major.beat_pulse
|
||||
|
||||
+29
-200
@@ -1,210 +1,58 @@
|
||||
Instruments and Fingerings
|
||||
==========================
|
||||
Fretboard and Fingerings
|
||||
========================
|
||||
|
||||
The :class:`~pytheory.chords.Fretboard` class models any stringed
|
||||
instrument and generates chord fingerings. PyTheory includes **25
|
||||
instrument presets** spanning Western, Asian, Middle Eastern, Latin
|
||||
American, and Russian traditions.
|
||||
The :class:`~pytheory.chords.Fretboard` class represents a fretted instrument's
|
||||
tuning and generates chord fingerings.
|
||||
|
||||
How It Works
|
||||
------------
|
||||
|
||||
Each `fret <https://en.wikipedia.org/wiki/Fret>`_ on a stringed
|
||||
instrument raises the pitch by exactly **one semitone**. The open
|
||||
string is fret 0; fret 1 is one semitone up, and so on. Even fretless
|
||||
instruments (violin, oud, erhu) can be modeled this way — the "fret"
|
||||
positions are just semitone steps along the fingerboard.
|
||||
|
||||
Guitars
|
||||
-------
|
||||
|
||||
`Standard guitar tuning <https://en.wikipedia.org/wiki/Guitar_tunings>`_
|
||||
(high to low)::
|
||||
|
||||
String 1: E4 (highest)
|
||||
String 2: B3
|
||||
String 3: G3
|
||||
String 4: D3
|
||||
String 5: A2
|
||||
String 6: E2 (lowest)
|
||||
|
||||
This tuning uses intervals of a perfect 4th (5 semitones) between most
|
||||
strings, except between G and B which is a major 3rd (4 semitones).
|
||||
Preset Tunings
|
||||
--------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Fretboard
|
||||
|
||||
guitar = Fretboard.guitar() # Standard EADGBE
|
||||
twelve = Fretboard.twelve_string() # 12-string (6 doubled courses)
|
||||
bass = Fretboard.bass() # Standard 4-string EADG
|
||||
bass5 = Fretboard.bass(five_string=True) # 5-string with low B
|
||||
guitar = Fretboard.guitar() # E4 B3 G3 D3 A2 E2
|
||||
bass = Fretboard.bass() # G2 D2 A1 E1
|
||||
ukulele = Fretboard.ukulele() # A4 E4 C4 G4
|
||||
|
||||
**Alternate tunings** — 8 built-in presets:
|
||||
Custom Tunings
|
||||
--------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Fretboard.guitar("drop d") # DADGBE — heavy riffs, metal
|
||||
Fretboard.guitar("open g") # DGDGBD — slide guitar, Keith Richards
|
||||
Fretboard.guitar("open d") # DADF#AD — slide, folk
|
||||
Fretboard.guitar("open e") # EBEG#BE — slide blues
|
||||
Fretboard.guitar("open a") # EAC#EAE
|
||||
Fretboard.guitar("dadgad") # DADGAD — Celtic, fingerstyle
|
||||
Fretboard.guitar("half step down") # Eb standard — Hendrix, SRV
|
||||
from pytheory import Tone, Fretboard
|
||||
|
||||
# Custom tuning with any notes
|
||||
Fretboard.guitar(("C4", "G3", "C3", "G2", "C2", "G1"))
|
||||
|
||||
The Mandolin Family
|
||||
-------------------
|
||||
|
||||
The `mandolin family <https://en.wikipedia.org/wiki/Mandolin_family>`_
|
||||
mirrors the `violin family <https://en.wikipedia.org/wiki/Violin_family>`_
|
||||
— all tuned in perfect fifths, with each member a fifth or octave
|
||||
lower than the last:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Fretboard.mandolin() # E5 A4 D4 G3 — soprano (= violin)
|
||||
Fretboard.mandola() # A4 D4 G3 C3 — alto (= viola)
|
||||
Fretboard.octave_mandolin() # E4 A3 D3 G2 — tenor (octave below mandolin)
|
||||
Fretboard.mandocello() # A3 D3 G2 C2 — bass (= cello)
|
||||
|
||||
The mandolin's doubled courses (pairs of strings) create a natural
|
||||
chorus effect. The `octave mandolin <https://en.wikipedia.org/wiki/Octave_mandolin>`_
|
||||
is popular in Irish and Celtic folk music.
|
||||
|
||||
The Bowed String Family
|
||||
-----------------------
|
||||
|
||||
The orchestral `string family <https://en.wikipedia.org/wiki/String_section>`_
|
||||
is tuned in perfect fifths (except the double bass, which uses fourths):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Fretboard.violin() # E5 A4 D4 G3 — soprano
|
||||
Fretboard.viola() # A4 D4 G3 C3 — alto (5th below violin)
|
||||
Fretboard.cello() # A3 D3 G2 C2 — tenor/bass (octave below viola)
|
||||
Fretboard.double_bass() # G2 D2 A1 E1 — bass (tuned in 4ths!)
|
||||
|
||||
Bowed strings have no frets — the player can produce any pitch along
|
||||
the fingerboard, enabling continuous
|
||||
`vibrato <https://en.wikipedia.org/wiki/Vibrato>`_ and microtonal
|
||||
inflections not possible on fretted instruments.
|
||||
|
||||
The `erhu <https://en.wikipedia.org/wiki/Erhu>`_ — a 2-stringed Chinese
|
||||
bowed instrument with a hauntingly vocal quality:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Fretboard.erhu() # A4 D4 — tuned a 5th apart, no fingerboard
|
||||
|
||||
Plucked Strings
|
||||
---------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Fretboard.ukulele() # A4 E4 C4 G4 — re-entrant tuning
|
||||
Fretboard.banjo() # Open G (bluegrass, 5th string is high drone)
|
||||
Fretboard.banjo("open d") # Open D (clawhammer, old-time)
|
||||
Fretboard.harp() # 47 strings, C1 to G7 (concert pedal harp)
|
||||
|
||||
The `banjo <https://en.wikipedia.org/wiki/Banjo>`_'s short 5th string
|
||||
is a high drone — a defining feature of the instrument's sound.
|
||||
|
||||
The `harp <https://en.wikipedia.org/wiki/Harp>`_ has one string per
|
||||
diatonic note across nearly 7 octaves. Pedals alter each note name
|
||||
by up to two semitones across all octaves simultaneously.
|
||||
|
||||
World Instruments
|
||||
-----------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Middle Eastern
|
||||
Fretboard.oud() # C4 G3 D3 A2 G2 C2 — fretless, ancestor of the lute
|
||||
Fretboard.sitar() # 7 main strings — Indian classical
|
||||
|
||||
# East Asian
|
||||
Fretboard.shamisen() # C4 G3 C3 — 3-string Japanese, honchoshi tuning
|
||||
Fretboard.pipa() # D4 A3 E3 A2 — 4-string Chinese lute
|
||||
Fretboard.erhu() # A4 D4 — 2-string Chinese bowed
|
||||
|
||||
# European
|
||||
Fretboard.bouzouki() # D4 A3 D3 G2 — Irish (Celtic music)
|
||||
Fretboard.bouzouki("greek") # D4 A3 F3 C3 — Greek
|
||||
Fretboard.lute() # G4 D4 A3 F3 C3 G2 — Renaissance (6 courses)
|
||||
Fretboard.balalaika() # A4 E4 E4 — Russian (2 unison strings)
|
||||
|
||||
# Latin American
|
||||
Fretboard.charango() # E5 A4 E5 C5 G4 — Andean (re-entrant tuning)
|
||||
|
||||
# Steel guitar
|
||||
Fretboard.pedal_steel() # 10 strings, E9 Nashville — country music
|
||||
|
||||
The `oud <https://en.wikipedia.org/wiki/Oud>`_ is fretless, allowing
|
||||
the quarter-tone inflections essential to
|
||||
`maqam <https://en.wikipedia.org/wiki/Maqam>`_ performance. The
|
||||
`sitar <https://en.wikipedia.org/wiki/Sitar>`_ has moveable frets and
|
||||
sympathetic strings that resonate in harmony with the played notes.
|
||||
|
||||
Keyboards
|
||||
---------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Fretboard.keyboard() # 88-key piano (A0 to C8)
|
||||
Fretboard.keyboard(61, "C2") # 61-key synth controller
|
||||
Fretboard.keyboard(49, "C2") # 49-key controller
|
||||
Fretboard.keyboard(25, "C3") # 25-key mini MIDI controller
|
||||
|
||||
While keyboards don't have strings or frets, they map naturally to a
|
||||
sequence of tones. A full 88-key piano spans over 7 octaves — the
|
||||
widest range of any standard acoustic instrument.
|
||||
# Open D tuning
|
||||
open_d = Fretboard(tones=[
|
||||
Tone.from_string("D4"),
|
||||
Tone.from_string("A3"),
|
||||
Tone.from_string("F#3"),
|
||||
Tone.from_string("D3"),
|
||||
Tone.from_string("A2"),
|
||||
Tone.from_string("D2"),
|
||||
])
|
||||
|
||||
Getting Fingerings
|
||||
------------------
|
||||
|
||||
The fingering algorithm finds the most playable voicing for any chord
|
||||
on any instrument. It scores each possibility by:
|
||||
|
||||
1. Preferring **open strings** (fret 0) — they ring freely
|
||||
2. Preferring **ascending** fret patterns — easier hand position
|
||||
3. Minimizing the number of **fingers needed**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Fretboard, CHARTS
|
||||
|
||||
fb = Fretboard.guitar()
|
||||
c = CHARTS["western"]["C"]
|
||||
|
||||
# Best single fingering
|
||||
# Best fingering for a chord
|
||||
c = CHARTS["western"]["C"]
|
||||
print(c.fingering(fretboard=fb))
|
||||
# (0, 1, 0, 2, 3, 0)
|
||||
|
||||
# All equally-scored fingerings
|
||||
# All possible fingerings
|
||||
all_c = c.fingering(fretboard=fb, multiple=True)
|
||||
|
||||
# Muted strings appear as None
|
||||
f = CHARTS["western"]["F"]
|
||||
print(f.fingering(fretboard=fb))
|
||||
|
||||
Reading Fingerings
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The tuple ``(0, 1, 0, 2, 3, 0)`` reads from the highest string to the
|
||||
lowest::
|
||||
|
||||
e|--0-- (open — E)
|
||||
B|--1-- (fret 1 — C)
|
||||
G|--0-- (open — G)
|
||||
D|--2-- (fret 2 — E)
|
||||
A|--3-- (fret 3 — C)
|
||||
E|--0-- (open — E)
|
||||
|
||||
A value of ``None`` means the string is muted (not played).
|
||||
|
||||
Generating Full Charts
|
||||
----------------------
|
||||
|
||||
@@ -220,30 +68,11 @@ Generate fingerings for every chord at once:
|
||||
for name, fingering in chart.items():
|
||||
print(f"{name:6s} {fingering}")
|
||||
|
||||
# Works with any instrument
|
||||
uke_chart = charts_for_fretboard(fretboard=Fretboard.ukulele())
|
||||
mando_chart = charts_for_fretboard(fretboard=Fretboard.mandolin())
|
||||
|
||||
Custom Instruments
|
||||
------------------
|
||||
|
||||
Any instrument can be modeled with custom string tunings:
|
||||
Ukulele Example
|
||||
---------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Tone, Fretboard
|
||||
|
||||
# Baritone ukulele (DGBE — top 4 guitar strings)
|
||||
bari_uke = Fretboard(tones=[
|
||||
Tone.from_string("E4"),
|
||||
Tone.from_string("B3"),
|
||||
Tone.from_string("G3"),
|
||||
Tone.from_string("D3"),
|
||||
])
|
||||
|
||||
# Tres cubano (Cuban guitar, 3 doubled courses)
|
||||
tres = Fretboard(tones=[
|
||||
Tone.from_string("E4"),
|
||||
Tone.from_string("B3"),
|
||||
Tone.from_string("G3"),
|
||||
])
|
||||
fb = Fretboard.ukulele()
|
||||
c = CHARTS["western"]["C"]
|
||||
print(c.fingering(fretboard=fb)) # 4-string fingering
|
||||
|
||||
+8
-28
@@ -1,8 +1,7 @@
|
||||
Audio Playback
|
||||
==============
|
||||
|
||||
PyTheory can synthesize and play tones and chords through your speakers
|
||||
using basic `waveform <https://en.wikipedia.org/wiki/Waveform>`_ synthesis.
|
||||
PyTheory can synthesize and play tones and chords through your speakers.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -37,21 +36,7 @@ Playing a Chord
|
||||
Waveform Types
|
||||
--------------
|
||||
|
||||
The waveform shape determines the `timbre <https://en.wikipedia.org/wiki/Timbre>`_ (tonal color) of the sound.
|
||||
Different waveforms contain different combinations of **harmonics** —
|
||||
integer multiples of the fundamental frequency.
|
||||
|
||||
- `Sine wave <https://en.wikipedia.org/wiki/Sine_wave>`_ — the purest tone. Contains only the fundamental
|
||||
frequency with no harmonics. Sounds smooth, clear, and "electronic."
|
||||
This is the building block of all other waveforms (`Fourier's theorem <https://en.wikipedia.org/wiki/Fourier_series>`_).
|
||||
|
||||
- `Sawtooth wave <https://en.wikipedia.org/wiki/Sawtooth_wave>`_ — contains all harmonics (both odd and even),
|
||||
each at amplitude 1/n. Sounds bright, buzzy, and aggressive.
|
||||
Named for its shape. Used extensively in `additive synthesis <https://en.wikipedia.org/wiki/Additive_synthesis>`_ and analog synthesizers.
|
||||
|
||||
- `Triangle wave <https://en.wikipedia.org/wiki/Triangle_wave>`_ — contains only odd harmonics, each at amplitude
|
||||
1/n². Sounds softer and more mellow than sawtooth — somewhere between
|
||||
sine and sawtooth. Often described as "woody" or "hollow."
|
||||
Choose between sine, sawtooth, and triangle wave synthesis:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -59,22 +44,17 @@ integer multiples of the fundamental frequency.
|
||||
|
||||
tone = Tone.from_string("C4", system="western")
|
||||
|
||||
play(tone, synth=Synth.SINE) # Pure, clean
|
||||
play(tone, synth=Synth.SINE) # Smooth, pure tone
|
||||
play(tone, synth=Synth.SAW) # Bright, buzzy
|
||||
play(tone, synth=Synth.TRIANGLE) # Mellow, hollow
|
||||
play(tone, synth=Synth.TRIANGLE) # Softer than sawtooth
|
||||
|
||||
Temperaments
|
||||
------------
|
||||
|
||||
Hear the difference between tuning systems:
|
||||
Play in different tuning systems:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
play(tone, temperament="equal") # Modern standard (since ~1917)
|
||||
play(tone, temperament="pythagorean") # Pure fifths, wolf intervals
|
||||
play(tone, temperament="meantone") # Pure thirds, Renaissance sound
|
||||
|
||||
Try playing a C major chord in each temperament — you'll hear subtle
|
||||
differences in the "color" of the major third. Equal temperament is
|
||||
a compromise; the other systems sacrifice some keys to make the good
|
||||
keys sound better.
|
||||
play(tone, temperament="equal") # Default, modern tuning
|
||||
play(tone, temperament="pythagorean") # Ancient Greek tuning
|
||||
play(tone, temperament="meantone") # Renaissance tuning
|
||||
|
||||
+26
-26
@@ -4,9 +4,15 @@ Quickstart
|
||||
Installation
|
||||
------------
|
||||
|
||||
::
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install pytheory
|
||||
pip install pytheory
|
||||
|
||||
Or with `uv <https://github.com/astral-sh/uv>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
uv add pytheory
|
||||
|
||||
Basic Usage
|
||||
-----------
|
||||
@@ -17,28 +23,28 @@ Create tones, build scales, and explore music theory:
|
||||
|
||||
from pytheory import Tone, TonedScale, Fretboard, CHARTS
|
||||
|
||||
# Create a tone — A4 is the tuning standard (440 Hz)
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
print(a4.frequency) # 440.0
|
||||
# Create a tone
|
||||
c4 = Tone.from_string("C4")
|
||||
print(c4) # C4
|
||||
print(c4.frequency) # 261.63 Hz
|
||||
|
||||
# Tone arithmetic — add semitones to move up the chromatic scale
|
||||
c4 = Tone.from_string("C4", system="western")
|
||||
e4 = c4 + 4 # Major third up (4 semitones)
|
||||
g4 = c4 + 7 # Perfect fifth up (7 semitones)
|
||||
print(e4, g4) # E4 G4
|
||||
# Tone arithmetic
|
||||
e4 = c4 + 4 # Major third up
|
||||
g4 = c4 + 7 # Perfect fifth up
|
||||
print(e4, g4) # E4 G4
|
||||
|
||||
# Measure intervals between tones
|
||||
print(g4 - c4) # 7 (semitones — a perfect fifth)
|
||||
# Measure intervals
|
||||
print(g4 - c4) # 7 (semitones)
|
||||
|
||||
# Build a C major scale
|
||||
# Build a scale
|
||||
c_major = TonedScale(tonic="C4")["major"]
|
||||
print(c_major.note_names)
|
||||
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
# Build diatonic triads from the scale
|
||||
I = c_major.triad(0) # C E G (C major)
|
||||
IV = c_major.triad(3) # F A C (F major)
|
||||
V = c_major.triad(4) # G B D (G major)
|
||||
# Build chords from the scale
|
||||
I = c_major.triad(0) # C major
|
||||
IV = c_major.triad(3) # F major
|
||||
V = c_major.triad(4) # G major
|
||||
|
||||
# Guitar chord fingerings
|
||||
fb = Fretboard.guitar()
|
||||
@@ -48,15 +54,9 @@ Create tones, build scales, and explore music theory:
|
||||
What's Included
|
||||
---------------
|
||||
|
||||
- **6 musical systems**: Western, Indian (Hindustani), Arabic (Maqam),
|
||||
Japanese, Blues/Pentatonic, Javanese Gamelan
|
||||
- **40+ scales**: major, minor, harmonic minor, 7 modes, 10 thaats,
|
||||
10 maqamat, 6 Japanese pentatonic scales, blues, pentatonic,
|
||||
slendro, pelog, and more
|
||||
- **12-tone Western system** with all chromatic notes
|
||||
- **Scales**: major, minor, harmonic minor, and all 7 modes
|
||||
- **Pitch calculation** in equal, Pythagorean, and meantone temperaments
|
||||
- **Chord charts** with 144 pre-built chords (12 roots x 12 qualities)
|
||||
- **Chord analysis**: consonance scoring, Plomp-Levelt dissonance,
|
||||
beat frequency calculation
|
||||
- **Fingering generation** for guitar (8 tunings), bass, ukulele, or
|
||||
any custom fretted instrument
|
||||
- **Fingering generation** for any fretted instrument
|
||||
- **Audio playback** with sine, sawtooth, and triangle wave synthesis
|
||||
|
||||
+39
-200
@@ -1,29 +1,7 @@
|
||||
Working with Scales
|
||||
===================
|
||||
|
||||
A **scale** is an ordered set of tones spanning an octave, defined by a
|
||||
pattern of intervals. Scales are the foundation of melody and harmony —
|
||||
they determine which notes "belong" in a piece of music and shape its
|
||||
emotional character.
|
||||
|
||||
Scale Construction
|
||||
------------------
|
||||
|
||||
Every scale is defined by its **interval pattern** — the sequence of
|
||||
whole steps (W = 2 semitones) and half steps (H = 1 semitone) between
|
||||
consecutive tones.
|
||||
|
||||
The `major scale <https://en.wikipedia.org/wiki/Major_scale>`_::
|
||||
|
||||
W W H W W W H
|
||||
C D E F G A B C
|
||||
2 2 1 2 2 2 1 ← semitones between each note
|
||||
|
||||
The `natural minor scale <https://en.wikipedia.org/wiki/Minor_scale>`_::
|
||||
|
||||
W H W W H W W
|
||||
C D Eb F G Ab Bb C
|
||||
2 1 2 2 1 2 2
|
||||
Scales are sequences of tones following a specific interval pattern.
|
||||
|
||||
Building Scales
|
||||
---------------
|
||||
@@ -36,6 +14,7 @@ Use :class:`~pytheory.scales.TonedScale` to generate scales in any key:
|
||||
|
||||
c = TonedScale(tonic="C4")
|
||||
|
||||
# Access scales by name
|
||||
major = c["major"]
|
||||
minor = c["minor"]
|
||||
harmonic_minor = c["harmonic minor"]
|
||||
@@ -43,117 +22,62 @@ Use :class:`~pytheory.scales.TonedScale` to generate scales in any key:
|
||||
print(major.note_names)
|
||||
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
Major and Minor
|
||||
---------------
|
||||
|
||||
The **major scale** (`Ionian <https://en.wikipedia.org/wiki/Ionian_mode>`_ mode) is the foundation of Western tonal
|
||||
music. Its pattern of whole and half steps creates a bright, resolved
|
||||
sound. Every major key has a `relative minor <https://en.wikipedia.org/wiki/Relative_key>`_ that shares the same
|
||||
notes but starts from the 6th degree:
|
||||
|
||||
- C major → A minor (both use only white keys)
|
||||
- G major → E minor (both have one sharp: F#)
|
||||
- F major → D minor (both have one flat: Bb)
|
||||
Available Scales
|
||||
----------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
c_major = TonedScale(tonic="C4")["major"]
|
||||
a_minor = TonedScale(tonic="A4")["minor"]
|
||||
|
||||
# Same notes, different starting point
|
||||
set(c_major.note_names) == set(a_minor.note_names) # True
|
||||
|
||||
The `harmonic minor <https://en.wikipedia.org/wiki/Harmonic_minor_scale>`_ raises the 7th degree of the natural minor,
|
||||
creating an augmented 2nd interval (3 semitones) between the 6th and
|
||||
7th degrees. This gives it a distinctive "Middle Eastern" or "classical"
|
||||
sound and provides the leading tone needed for dominant harmony::
|
||||
|
||||
Natural minor: C D Eb F G Ab Bb C
|
||||
Harmonic minor: C D Eb F G Ab B C
|
||||
↑ raised 7th
|
||||
>>> c = TonedScale(tonic="C4")
|
||||
>>> c.scales
|
||||
('chromatic', 'major', 'minor', 'harmonic minor',
|
||||
'ionian', 'dorian', 'phrygian', 'lydian',
|
||||
'mixolydian', 'aeolian', 'locrian')
|
||||
|
||||
Modes
|
||||
-----
|
||||
|
||||
The seven `modes <https://en.wikipedia.org/wiki/Mode_(music)>`_ of the major scale are rotations of the same interval
|
||||
pattern, each starting from a different degree. Each mode has a distinct
|
||||
emotional character:
|
||||
All seven modes of the major scale are supported:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
c = TonedScale(tonic="C4")
|
||||
|
||||
**Ionian** (I) — the major scale itself. Bright, happy, resolved::
|
||||
c["ionian"] # Same as major: C D E F G A B C
|
||||
c["dorian"] # C D Eb F G A Bb C
|
||||
c["phrygian"] # C Db Eb F G Ab Bb C
|
||||
c["lydian"] # C D E F# G A B C
|
||||
c["mixolydian"] # C D E F G A Bb C
|
||||
c["aeolian"] # Same as minor: C D Eb F G Ab Bb C
|
||||
c["locrian"] # C Db Eb F Gb Ab Bb C
|
||||
|
||||
c["ionian"] # C D E F G A B C
|
||||
Accessing Degrees
|
||||
-----------------
|
||||
|
||||
`Dorian <https://en.wikipedia.org/wiki/Dorian_mode>`_ (ii) — minor with a raised 6th. Jazzy, soulful (So What,
|
||||
Scarborough Fair)::
|
||||
|
||||
c["dorian"] # C D Eb F G A Bb C
|
||||
|
||||
`Phrygian <https://en.wikipedia.org/wiki/Phrygian_mode>`_ (iii) — minor with a flat 2nd. Spanish, flamenco, dark
|
||||
(White Rabbit)::
|
||||
|
||||
c["phrygian"] # C Db Eb F G Ab Bb C
|
||||
|
||||
`Lydian <https://en.wikipedia.org/wiki/Lydian_mode>`_ (IV) — major with a raised 4th. Dreamy, floating, ethereal
|
||||
(The Simpsons theme, Flying by ET)::
|
||||
|
||||
c["lydian"] # C D E F# G A B C
|
||||
|
||||
`Mixolydian <https://en.wikipedia.org/wiki/Mixolydian_mode>`_ (V) — major with a flat 7th. Bluesy, rock, dominant
|
||||
(Norwegian Wood, Sweet Home Alabama)::
|
||||
|
||||
c["mixolydian"] # C D E F G A Bb C
|
||||
|
||||
`Aeolian <https://en.wikipedia.org/wiki/Aeolian_mode>`_ (vi) — the natural minor scale. Sad, dark, introspective
|
||||
(Stairway to Heaven, Losing My Religion)::
|
||||
|
||||
c["aeolian"] # C D Eb F G Ab Bb C
|
||||
|
||||
`Locrian <https://en.wikipedia.org/wiki/Locrian_mode>`_ (vii) — minor with flat 2nd and flat 5th. Unstable,
|
||||
rarely used as a home key (used in metal and jazz over diminished
|
||||
chords)::
|
||||
|
||||
c["locrian"] # C Db Eb F Gb Ab Bb C
|
||||
|
||||
Scale Degrees
|
||||
-------------
|
||||
|
||||
Each note in a scale has a **degree name** that describes its function:
|
||||
|
||||
============ ====== =======================================
|
||||
Degree Number Function
|
||||
============ ====== =======================================
|
||||
Tonic I Home base — the key center
|
||||
Supertonic II One step above tonic
|
||||
Mediant III Halfway between tonic and dominant
|
||||
Subdominant IV A fifth below tonic (or fourth above)
|
||||
Dominant V The strongest pull back to tonic
|
||||
Submediant VI Root of the relative minor (or major)
|
||||
Leading Tone VII One semitone below tonic — pulls upward
|
||||
============ ====== =======================================
|
||||
|
||||
Access degrees by index, Roman numeral, or name:
|
||||
Scale tones can be accessed by index, Roman numeral, or degree name:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
major = TonedScale(tonic="C4")["major"]
|
||||
|
||||
major[0] # C4 (by index)
|
||||
major["I"] # C4 (by Roman numeral)
|
||||
major["tonic"] # C4 (by degree name)
|
||||
# By index
|
||||
major[0] # C4
|
||||
major[4] # G4
|
||||
|
||||
major["V"] # G4 (dominant)
|
||||
# By Roman numeral
|
||||
major["I"] # C4
|
||||
major["V"] # G4
|
||||
|
||||
# By degree name
|
||||
major["tonic"] # C4
|
||||
major["dominant"] # G4
|
||||
|
||||
major[0:3] # (C4, D4, E4) — slicing works too
|
||||
# Slicing
|
||||
major[0:3] # (C4, D4, E4)
|
||||
|
||||
Iteration
|
||||
---------
|
||||
|
||||
Scales are iterable and support ``len()`` and ``in``:
|
||||
Scales are iterable:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -167,101 +91,16 @@ Scales are iterable and support ``len()`` and ``in``:
|
||||
Building Chords from Scales
|
||||
----------------------------
|
||||
|
||||
`Diatonic <https://en.wikipedia.org/wiki/Diatonic_and_chromatic>`_ harmony builds chords by stacking every other note of the
|
||||
scale. A **triad** takes the 1st, 3rd, and 5th; a **seventh chord** adds
|
||||
the 7th.
|
||||
|
||||
In the C major scale, the diatonic triads are::
|
||||
|
||||
I C E G = C major
|
||||
ii D F A = D minor
|
||||
iii E G B = E minor
|
||||
IV F A C = F major
|
||||
V G B D = G major
|
||||
vi A C E = A minor
|
||||
vii° B D F = B diminished
|
||||
|
||||
Notice the pattern: **major** triads on I, IV, V; **minor** triads on
|
||||
ii, iii, vi; **diminished** on vii°. This pattern holds for every major
|
||||
key.
|
||||
Build chords directly from scale degrees:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
major = TonedScale(tonic="C4")["major"]
|
||||
|
||||
# Build diatonic triads
|
||||
I = major.triad(0) # C E G (C major)
|
||||
ii = major.triad(1) # D F A (D minor)
|
||||
iii = major.triad(2) # E G B (E minor)
|
||||
IV = major.triad(3) # F A C (F major)
|
||||
V = major.triad(4) # G B D (G major)
|
||||
vi = major.triad(5) # A C E (A minor)
|
||||
# Build a triad (root, 3rd, 5th)
|
||||
I = major.triad(0) # C E G (C major)
|
||||
ii = major.triad(1) # D F A (D minor)
|
||||
V = major.triad(4) # G B D (G major)
|
||||
|
||||
# Build seventh chords
|
||||
Imaj7 = major.chord(0, 2, 4, 6) # C E G B = Cmaj7
|
||||
V7 = major.chord(4, 6, 8, 10) # G B D F = G7 (dominant 7th)
|
||||
|
||||
Common Progressions
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Some of the most-used chord progressions in Western music:
|
||||
|
||||
- **I–IV–V–I** — the foundation of blues, rock, country, folk
|
||||
- **I–V–vi–IV** — the "pop progression" (Let It Be, No Woman No Cry,
|
||||
With or Without You, Someone Like You)
|
||||
- **ii–V–I** — the backbone of jazz harmony
|
||||
- **I–vi–IV–V** — the "50s progression" (Stand By Me, Every Breath You Take)
|
||||
- **i–bVI–bIII–bVII** — the "epic" minor progression (Stairway to Heaven,
|
||||
My Heart Will Go On)
|
||||
- **I–IV–vi–V** — axis of awesome (many, many pop songs)
|
||||
|
||||
The 12-Bar Blues
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The `12-bar blues <https://en.wikipedia.org/wiki/Twelve-bar_blues>`_ is the most influential chord progression in
|
||||
American music. It's 12 measures long and uses only three chords
|
||||
(I, IV, V)::
|
||||
|
||||
| I | I | I | I |
|
||||
| IV | IV | I | I |
|
||||
| V | IV | I | V |
|
||||
|
||||
Every blues, early rock and roll, and much of jazz is built on this
|
||||
structure. In the key of A::
|
||||
|
||||
| A | A | A | A |
|
||||
| D | D | A | A |
|
||||
| E | D | A | E |
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
a = TonedScale(tonic="A4")["major"]
|
||||
I = a.triad(0) # A major
|
||||
IV = a.triad(3) # D major
|
||||
V = a.triad(4) # E major
|
||||
|
||||
# The 12-bar blues progression
|
||||
blues_12 = [I, I, I, I, IV, IV, I, I, V, IV, I, V]
|
||||
|
||||
Parallel Major and Minor
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Two scales are **relative** if they share the same notes (C major and
|
||||
A minor). Two scales are `parallel <https://en.wikipedia.org/wiki/Parallel_key>`_ if they share the same tonic but
|
||||
have different notes (C major and C minor).
|
||||
|
||||
Mixing parallel major and minor is a powerful compositional tool —
|
||||
borrowing chords from the parallel minor in a major key creates
|
||||
dramatic color shifts. The bVI and bVII chords (Ab and Bb in C major)
|
||||
are borrowed from C minor and appear constantly in rock and film music.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
c_major = TonedScale(tonic="C4")["major"]
|
||||
c_minor = TonedScale(tonic="C4")["minor"]
|
||||
|
||||
# Compare: same tonic, different notes
|
||||
c_major.note_names # ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
c_minor.note_names # ['C', 'D', 'D#', 'F', 'G', 'G#', 'A#', 'C']
|
||||
# Custom chord voicings
|
||||
cmaj7 = major.chord(0, 2, 4, 6) # C E G B
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
Musical Systems
|
||||
===============
|
||||
|
||||
PyTheory supports four musical systems, each with its own tone names
|
||||
and scale patterns.
|
||||
|
||||
Western
|
||||
-------
|
||||
|
||||
The standard 12-tone equal temperament system with major/minor scales
|
||||
and all seven modes.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
c = TonedScale(tonic="C4")
|
||||
c["major"].note_names
|
||||
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
c["dorian"].note_names
|
||||
# ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C']
|
||||
|
||||
**Scales:** major, minor, harmonic minor, ionian, dorian, phrygian,
|
||||
lydian, mixolydian, aeolian, locrian, chromatic
|
||||
|
||||
Indian Classical (Hindustani)
|
||||
-----------------------------
|
||||
|
||||
The Hindustani system uses **swaras** (Sa, Re, Ga, Ma, Pa, Dha, Ni) and
|
||||
organizes scales into `thaats <https://en.wikipedia.org/wiki/Thaat>`_ — the 10 parent scales from which `ragas <https://en.wikipedia.org/wiki/Raga>`_
|
||||
are derived.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
sa = TonedScale(tonic="Sa4", system="indian")
|
||||
|
||||
sa["bilawal"].note_names # = major scale
|
||||
# ['Sa', 'Re', 'Ga', 'Ma', 'Pa', 'Dha', 'Ni', 'Sa']
|
||||
|
||||
sa["bhairav"].note_names # unique to Indian music
|
||||
# ['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
|
||||
sa["todi"].note_names
|
||||
# ['Sa', 'komal Re', 'komal Ga', 'tivra Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
|
||||
**Thaats:** bilawal, khamaj, kafi, asavari, bhairavi, kalyan, bhairav,
|
||||
poorvi, marwa, todi
|
||||
|
||||
**Swara notation:**
|
||||
|
||||
- Uppercase = shuddha (natural): Sa, Re, Ga, Ma, Pa, Dha, Ni
|
||||
- ``komal`` prefix = flat: komal Re, komal Ga, komal Dha, komal Ni
|
||||
- ``tivra`` prefix = sharp: tivra Ma
|
||||
|
||||
Arabic Maqam
|
||||
------------
|
||||
|
||||
The Arabic system uses **solfège-based names** (Do, Re, Mi, Fa, Sol, La, Si)
|
||||
and organizes scales into **maqamat** (plural of `maqam <https://en.wikipedia.org/wiki/Maqam>`_).
|
||||
|
||||
.. note::
|
||||
|
||||
True maqam music uses quarter-tones that cannot be represented in
|
||||
12-tone equal temperament. These scales are the closest 12-TET
|
||||
approximations.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
do = TonedScale(tonic="Do4", system="arabic")
|
||||
|
||||
do["ajam"].note_names # = major scale
|
||||
# ['Do', 'Re', 'Mi', 'Fa', 'Sol', 'La', 'Si', 'Do']
|
||||
|
||||
do["hijaz"].note_names # characteristic augmented 2nd
|
||||
# ['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
|
||||
|
||||
do["nikriz"].note_names
|
||||
# ['Do', 'Re', 'Mib', 'Fa#', 'Sol', 'La', 'Sib', 'Do']
|
||||
|
||||
**Maqamat:** ajam, nahawand, kurd, hijaz, nikriz, bayati, rast, saba,
|
||||
sikah, jiharkah
|
||||
|
||||
Japanese
|
||||
--------
|
||||
|
||||
The Japanese system uses Western note names with traditional pentatonic
|
||||
and heptatonic scales from Japanese music.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
c = TonedScale(tonic="C4", system="japanese")
|
||||
|
||||
c["hirajoshi"].note_names # most iconic Japanese scale
|
||||
# ['C', 'D', 'D#', 'G', 'G#', 'C']
|
||||
|
||||
c["in"].note_names # Miyako-bushi, used in koto music
|
||||
# ['C', 'C#', 'F', 'G', 'G#', 'C']
|
||||
|
||||
c["yo"].note_names # folk music scale
|
||||
# ['C', 'D', 'F', 'G', 'A#', 'C']
|
||||
|
||||
c["ritsu"].note_names # gagaku court music (= Dorian)
|
||||
# ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C']
|
||||
|
||||
**Pentatonic scales:** hirajoshi, in, yo, iwato, kumoi, insen
|
||||
|
||||
**Heptatonic scales:** ritsu, ryo
|
||||
|
||||
Blues and Pentatonic
|
||||
--------------------
|
||||
|
||||
The blues system provides the scales foundational to blues, rock, jazz,
|
||||
and folk music worldwide. `Pentatonic scales <https://en.wikipedia.org/wiki/Pentatonic_scale>`_ (5 notes) are the oldest
|
||||
known musical scales, found independently in cultures across every
|
||||
continent.
|
||||
|
||||
The `blues scale <https://en.wikipedia.org/wiki/Blues_scale>`_ adds the "`blue note <https://en.wikipedia.org/wiki/Blue_note>`_" (flat 5th / sharp 4th) to the
|
||||
minor pentatonic — this chromatic passing tone is the defining sound
|
||||
of the blues.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
c = TonedScale(tonic="C4", system="blues")
|
||||
|
||||
c["major pentatonic"].note_names # the "happy" pentatonic
|
||||
# ['C', 'D', 'E', 'G', 'A', 'C']
|
||||
|
||||
c["minor pentatonic"].note_names # the "sad" pentatonic
|
||||
# ['C', 'D#', 'F', 'G', 'A#', 'C']
|
||||
|
||||
c["blues"].note_names # minor pentatonic + blue note
|
||||
# ['C', 'D#', 'F', 'F#', 'G', 'A#', 'C']
|
||||
|
||||
c["major blues"].note_names # major pentatonic + blue note
|
||||
# ['C', 'D', 'D#', 'E', 'G', 'A', 'C']
|
||||
|
||||
**Pentatonic:** major pentatonic, minor pentatonic
|
||||
|
||||
**Hexatonic:** blues, major blues
|
||||
|
||||
**Heptatonic:** dominant (Mixolydian — the dominant 7th sound),
|
||||
minor (Dorian — the jazz minor sound)
|
||||
|
||||
|
||||
Javanese Gamelan
|
||||
----------------
|
||||
|
||||
The `gamelan <https://en.wikipedia.org/wiki/Gamelan>`_ system approximates the scales of the Javanese and Balinese
|
||||
gamelan orchestra in 12-tone equal temperament. True gamelan tuning is
|
||||
unique to each ensemble and does not conform to Western intonation —
|
||||
these are the closest 12-TET approximations.
|
||||
|
||||
`Slendro <https://en.wikipedia.org/wiki/Slendro>`_ is a roughly equal 5-tone division of the octave, producing
|
||||
an ethereal, floating quality. `Pelog <https://en.wikipedia.org/wiki/Pelog>`_ is a 7-tone scale with unequal
|
||||
intervals, typically performed using 5-note subsets called *pathet*.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
ji = TonedScale(tonic="ji4", system="gamelan")
|
||||
|
||||
ji["slendro"].note_names # the 5-tone equidistant scale
|
||||
# ['ji', 'ro', 'pat', 'mo', 'pi', 'ji']
|
||||
|
||||
ji["pelog"].note_names # full 7-tone pelog
|
||||
# ['ji', 'ro-', 'lu', 'pat', 'mo', 'nem-', 'barang', 'ji']
|
||||
|
||||
ji["pelog nem"].note_names # pathet nem subset
|
||||
# ['ji', 'ro-', 'lu', 'pat', 'mo', 'ji']
|
||||
|
||||
**Pentatonic:** slendro, pelog nem, pelog barang, pelog lima
|
||||
|
||||
**Heptatonic:** pelog (full 7-tone)
|
||||
|
||||
.. note::
|
||||
|
||||
Gamelan tone names follow Javanese numbering: ji (1), ro (2),
|
||||
lu (3), pat (4), mo (5), nem (6), pi/barang (7). Suffixes
|
||||
indicate microtonal variants approximated to the nearest semitone.
|
||||
|
||||
|
||||
Cross-System Comparison
|
||||
-----------------------
|
||||
|
||||
Since all systems use 12-tone equal temperament, equivalent scales
|
||||
produce the same pitches:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale, Tone
|
||||
|
||||
# These are all the same scale with different names
|
||||
western = TonedScale(tonic="C4")["major"]
|
||||
indian = TonedScale(tonic="Sa4", system="indian")["bilawal"]
|
||||
arabic = TonedScale(tonic="Do4", system="arabic")["ajam"]
|
||||
|
||||
# Same pitches
|
||||
c4 = Tone.from_string("C4", system="western")
|
||||
sa4 = Tone.from_string("Sa4", system="indian")
|
||||
do4 = Tone.from_string("Do4", system="arabic")
|
||||
|
||||
c4.frequency # 261.63
|
||||
sa4.frequency # 261.63
|
||||
do4.frequency # 261.63
|
||||
@@ -1,330 +0,0 @@
|
||||
Music Theory Fundamentals
|
||||
=========================
|
||||
|
||||
This page covers the essential concepts of music theory — the framework
|
||||
behind everything PyTheory does.
|
||||
|
||||
Sound and Pitch
|
||||
---------------
|
||||
|
||||
All sound is vibration. When an object vibrates, it pushes air molecules
|
||||
back and forth, creating pressure waves that travel to your ears. The
|
||||
speed of this vibration — measured in cycles per second
|
||||
(`Hertz <https://en.wikipedia.org/wiki/Hertz>`_, Hz) — determines the
|
||||
`pitch <https://en.wikipedia.org/wiki/Pitch_(music)>`_ you hear.
|
||||
|
||||
- **20 Hz**: the lowest pitch most humans can hear
|
||||
- **60–250 Hz**: the range of the human voice (speaking)
|
||||
- **261.63 Hz**: `middle C <https://en.wikipedia.org/wiki/C_(musical_note)#Middle_C>`_ (C4)
|
||||
- **440 Hz**: the `concert pitch <https://en.wikipedia.org/wiki/Concert_pitch>`_ tuning standard A (A4)
|
||||
- **4186 Hz**: the highest C on a piano (C8)
|
||||
- **20,000 Hz**: the upper limit of `human hearing <https://en.wikipedia.org/wiki/Hearing_range>`_
|
||||
|
||||
The relationship between pitch and frequency is **logarithmic** — each
|
||||
`octave <https://en.wikipedia.org/wiki/Octave>`_ doubles the frequency.
|
||||
This means the distance from A3 (220 Hz) to A4 (440 Hz) is 220 Hz, but
|
||||
the distance from A4 to A5 (880 Hz) is 440 Hz. Both sound like "one
|
||||
octave" to our ears.
|
||||
|
||||
Why Twelve Notes?
|
||||
-----------------
|
||||
|
||||
The Western `chromatic scale <https://en.wikipedia.org/wiki/Chromatic_scale>`_
|
||||
has 12 notes per octave. This isn't arbitrary — it emerges from the
|
||||
physics of vibrating strings and air columns.
|
||||
|
||||
The `harmonic series <https://en.wikipedia.org/wiki/Harmonic_series_(music)>`_
|
||||
is the sequence of frequencies produced when a string vibrates: f, 2f,
|
||||
3f, 4f, 5f... The relationships between these harmonics create the
|
||||
intervals we perceive as `consonant <https://en.wikipedia.org/wiki/Consonance_and_dissonance>`_:
|
||||
|
||||
- 2:1 = `octave <https://en.wikipedia.org/wiki/Octave>`_ (the most fundamental)
|
||||
- 3:2 = `perfect fifth <https://en.wikipedia.org/wiki/Perfect_fifth>`_
|
||||
- 4:3 = `perfect fourth <https://en.wikipedia.org/wiki/Perfect_fourth>`_
|
||||
- 5:4 = `major third <https://en.wikipedia.org/wiki/Major_third>`_
|
||||
- 6:5 = `minor third <https://en.wikipedia.org/wiki/Minor_third>`_
|
||||
|
||||
If you stack perfect fifths (multiply by 3/2 repeatedly) and reduce to
|
||||
within one octave, you get 12 roughly evenly-spaced notes before the
|
||||
cycle almost closes. The tiny gap where it doesn't close perfectly is
|
||||
the `Pythagorean comma <https://en.wikipedia.org/wiki/Pythagorean_comma>`_
|
||||
— the reason we need `temperament <https://en.wikipedia.org/wiki/Musical_temperament>`_.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Tone
|
||||
|
||||
# Walk the circle of fifths — all 12 notes
|
||||
c = Tone.from_string("C4", system="western")
|
||||
[t.name for t in c.circle_of_fifths()]
|
||||
# ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
|
||||
|
||||
Other cultures divide the octave differently: Indonesian
|
||||
`gamelan <https://en.wikipedia.org/wiki/Gamelan>`_ uses 5 or 7 unequal
|
||||
divisions; Indian classical music theoretically has 22
|
||||
`shrutis <https://en.wikipedia.org/wiki/Shruti_(music)>`_ (microtones);
|
||||
Arabic `maqam <https://en.wikipedia.org/wiki/Maqam>`_ uses
|
||||
`quarter-tones <https://en.wikipedia.org/wiki/Quarter_tone>`_.
|
||||
|
||||
Intervals: The Atoms of Music
|
||||
------------------------------
|
||||
|
||||
An `interval <https://en.wikipedia.org/wiki/Interval_(music)>`_ is the
|
||||
distance between two pitches. Intervals are the building blocks of
|
||||
everything — melodies are sequences of intervals, chords are stacks
|
||||
of intervals, and scales are patterns of intervals.
|
||||
|
||||
Every interval has two properties:
|
||||
|
||||
**Size** (how many scale steps)::
|
||||
|
||||
Unison → 2nd → 3rd → 4th → 5th → 6th → 7th → Octave
|
||||
|
||||
**Quality** (exact number of semitones)::
|
||||
|
||||
Perfect: unison (0), 4th (5), 5th (7), octave (12)
|
||||
Major: 2nd (2), 3rd (4), 6th (9), 7th (11)
|
||||
Minor: 2nd (1), 3rd (3), 6th (8), 7th (10)
|
||||
Augmented: one semitone larger than perfect or major
|
||||
Diminished: one semitone smaller than perfect or minor
|
||||
|
||||
The "`perfect <https://en.wikipedia.org/wiki/Perfect_fifth>`_" intervals
|
||||
(unison, 4th, 5th, octave) are called perfect because they appear in
|
||||
both major AND minor scales unchanged. They've been considered consonant
|
||||
across virtually all musical cultures throughout history.
|
||||
|
||||
The `tritone <https://en.wikipedia.org/wiki/Tritone>`_ (augmented 4th /
|
||||
diminished 5th = 6 semitones) divides the octave exactly in half.
|
||||
Medieval theorists called it *diabolus in musica* ("the devil in music")
|
||||
because of its extreme instability. Today it's the foundation of
|
||||
`dominant harmony <https://en.wikipedia.org/wiki/Dominant_(music)>`_
|
||||
and the `blues <https://en.wikipedia.org/wiki/Blue_note>`_.
|
||||
|
||||
Keys and Key Signatures
|
||||
-----------------------
|
||||
|
||||
A `key <https://en.wikipedia.org/wiki/Key_(music)>`_ is a group of
|
||||
notes that form the tonal center of a piece. The key of C major uses
|
||||
only the white keys on the piano: C D E F G A B. The key of G major
|
||||
uses the same notes except F becomes F#.
|
||||
|
||||
`Key signatures <https://en.wikipedia.org/wiki/Key_signature>`_ tell
|
||||
you which notes are sharped or flatted throughout a piece. They follow
|
||||
the `circle of fifths <https://en.wikipedia.org/wiki/Circle_of_fifths>`_:
|
||||
|
||||
**Sharp keys** (add one sharp per step clockwise)::
|
||||
|
||||
C major: no sharps or flats
|
||||
G major: F#
|
||||
D major: F# C#
|
||||
A major: F# C# G#
|
||||
E major: F# C# G# D#
|
||||
B major: F# C# G# D# A#
|
||||
|
||||
**Flat keys** (add one flat per step counter-clockwise)::
|
||||
|
||||
C major: no sharps or flats
|
||||
F major: Bb
|
||||
Bb major: Bb Eb
|
||||
Eb major: Bb Eb Ab
|
||||
Ab major: Bb Eb Ab Db
|
||||
Db major: Bb Eb Ab Db Gb
|
||||
|
||||
The order of sharps is always F C G D A E B (Father Charles Goes Down
|
||||
And Ends Battle). The order of flats is the reverse: B E A D G C F.
|
||||
|
||||
Harmony: How Chords Work
|
||||
-------------------------
|
||||
|
||||
`Harmony <https://en.wikipedia.org/wiki/Harmony>`_ is the art of
|
||||
combining tones simultaneously. While
|
||||
`melody <https://en.wikipedia.org/wiki/Melody>`_ is horizontal (tones
|
||||
in sequence), harmony is vertical (tones stacked).
|
||||
|
||||
The simplest harmony is the `triad <https://en.wikipedia.org/wiki/Triad_(music)>`_
|
||||
— three notes built by stacking `thirds <https://en.wikipedia.org/wiki/Third_(music)>`_.
|
||||
The quality of each third determines the chord type:
|
||||
|
||||
- **Major triad** = major 3rd + minor 3rd (e.g. C-E-G)
|
||||
- **Minor triad** = minor 3rd + major 3rd (e.g. C-Eb-G)
|
||||
- `Diminished triad <https://en.wikipedia.org/wiki/Diminished_triad>`_ = minor 3rd + minor 3rd (e.g. B-D-F)
|
||||
- `Augmented triad <https://en.wikipedia.org/wiki/Augmented_triad>`_ = major 3rd + major 3rd (e.g. C-E-G#)
|
||||
|
||||
In any major key, the triads built on each
|
||||
`scale degree <https://en.wikipedia.org/wiki/Degree_(music)>`_ always
|
||||
follow the same pattern::
|
||||
|
||||
Degree Quality Function
|
||||
I Major Tonic (home)
|
||||
ii Minor Pre-dominant
|
||||
iii Minor Tonic substitute
|
||||
IV Major Subdominant (departure)
|
||||
V Major Dominant (tension, wants to go home)
|
||||
vi Minor Tonic substitute, relative minor
|
||||
vii° Diminished Dominant substitute (leading tone chord)
|
||||
|
||||
This pattern is the DNA of Western harmony. Pop songs, classical
|
||||
sonatas, jazz standards, and church hymns all derive from it.
|
||||
|
||||
Functional Harmony
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Chords don't just have names — they have
|
||||
`functions <https://en.wikipedia.org/wiki/Function_(music)>`_:
|
||||
|
||||
- **Tonic function** (I, iii, vi): stability, rest, home
|
||||
- **Subdominant function** (ii, IV): motion away from home
|
||||
- **Dominant function** (V, vii°): tension, desire to return home
|
||||
|
||||
The most fundamental progression in Western music is **T → S → D → T**
|
||||
(tonic → subdominant → dominant → tonic). The classic
|
||||
`I-IV-V-I <https://en.wikipedia.org/wiki/I%E2%80%93IV%E2%80%93V%E2%80%93I>`_
|
||||
is exactly this pattern. Every "Louie Louie" and every
|
||||
`Bach chorale <https://en.wikipedia.org/wiki/Bach_chorale>`_ follows
|
||||
this basic tonal gravity.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
scale = TonedScale(tonic="C4")["major"]
|
||||
|
||||
# The I-IV-V-I progression
|
||||
I = scale.triad(0) # C major — home
|
||||
IV = scale.triad(3) # F major — departure
|
||||
V = scale.triad(4) # G major — tension
|
||||
# I again # C major — resolution
|
||||
|
||||
The Dominant Seventh
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The most important chord in `tonal music <https://en.wikipedia.org/wiki/Tonality>`_
|
||||
is the `dominant seventh <https://en.wikipedia.org/wiki/Dominant_seventh_chord>`_
|
||||
— the V7 chord. In C major, this is G-B-D-F. It contains:
|
||||
|
||||
- A `leading tone <https://en.wikipedia.org/wiki/Leading-tone>`_ (B) that pulls up to the tonic (C) by half step
|
||||
- A `tritone <https://en.wikipedia.org/wiki/Tritone>`_ (B-F) that wants to resolve inward (B→C, F→E)
|
||||
- The `dominant note <https://en.wikipedia.org/wiki/Dominant_(music)>`_ (G) that falls to the tonic by a fifth
|
||||
|
||||
This combination creates the strongest possible pull toward
|
||||
`resolution <https://en.wikipedia.org/wiki/Resolution_(music)>`_.
|
||||
When you hear V7→I, you feel arrival.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Chord, Tone
|
||||
|
||||
C4 = Tone.from_string("C4", system="western")
|
||||
G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
g7 = Chord([G4, G4+4, G4+7, G4+10]) # G B D F
|
||||
g7.identify() # 'G dominant 7th'
|
||||
g7.tension['has_dominant_function'] # True
|
||||
g7.tension['tritones'] # 1
|
||||
|
||||
c_major = Chord([C4, C4+4, C4+7]) # C E G
|
||||
c_major.tension['score'] # 0.0 — fully resolved
|
||||
|
||||
Rhythm and Meter
|
||||
----------------
|
||||
|
||||
While PyTheory focuses on pitch,
|
||||
`rhythm <https://en.wikipedia.org/wiki/Rhythm>`_ is the other half
|
||||
of music.
|
||||
|
||||
**Rhythm** is the pattern of durations.
|
||||
`Meter <https://en.wikipedia.org/wiki/Metre_(music)>`_ is the recurring
|
||||
pattern of strong and weak beats that organizes rhythm.
|
||||
|
||||
- `4/4 time <https://en.wikipedia.org/wiki/Time_signature#Simple_time_signatures>`_: the most common meter. Strong-weak-medium-weak.
|
||||
Used in rock, pop, hip-hop, most Western music.
|
||||
- `3/4 time <https://en.wikipedia.org/wiki/Triple_metre>`_: waltz time. Strong-weak-weak. A lilting, circular feel.
|
||||
- `6/8 time <https://en.wikipedia.org/wiki/Compound_meter_(music)>`_: compound duple. Two groups of three. Irish jigs, many
|
||||
ballads.
|
||||
- `12/8 time <https://en.wikipedia.org/wiki/Compound_meter_(music)>`_: compound quadruple. Four groups of three. Slow blues,
|
||||
doo-wop, gospel. Has a triplet feel over a 4/4 pulse — the shuffle
|
||||
groove of "Stormy Monday" and "Oh! Darling."
|
||||
- 5/4 time: asymmetric. "`Take Five <https://en.wikipedia.org/wiki/Take_Five>`_"
|
||||
by Dave Brubeck. Creates constant forward momentum because it never
|
||||
fully settles.
|
||||
- `7/8 time <https://en.wikipedia.org/wiki/Additive_rhythm_and_divisive_rhythm>`_: common in Balkan folk music. Often felt as 2+2+3 or
|
||||
3+2+2.
|
||||
|
||||
The Physics of Consonance
|
||||
-------------------------
|
||||
|
||||
Why do some intervals sound "good" and others "bad"? The answer lies
|
||||
in the physics of sound waves and the
|
||||
`Plomp-Levelt <https://en.wikipedia.org/wiki/Consonance_and_dissonance#Physiological_basis>`_
|
||||
model of sensory dissonance.
|
||||
|
||||
When two frequencies are related by a simple ratio (like 3:2 for a
|
||||
perfect fifth), their waveforms align regularly. The combined wave
|
||||
is smooth and periodic — the brain perceives this as consonant.
|
||||
|
||||
When two frequencies are related by a complex ratio (like 45:32 for
|
||||
a tritone), their waveforms rarely align. The combined wave is
|
||||
irregular and the brain perceives
|
||||
`roughness <https://en.wikipedia.org/wiki/Roughness_(psychoacoustics)>`_
|
||||
— dissonance.
|
||||
|
||||
But `consonance and dissonance <https://en.wikipedia.org/wiki/Consonance_and_dissonance>`_
|
||||
are also cultural. The
|
||||
`major third <https://en.wikipedia.org/wiki/Major_third>`_ (5:4) was
|
||||
considered dissonant in medieval European music but consonant since the
|
||||
Renaissance. The tritone was forbidden in church music but is the
|
||||
foundation of blues and jazz. Indonesian gamelan embraces
|
||||
`beating <https://en.wikipedia.org/wiki/Beat_(acoustics)>`_ between
|
||||
paired instruments as a core aesthetic.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Chord, Tone
|
||||
|
||||
C4 = Tone.from_string("C4", system="western")
|
||||
E4 = Tone.from_string("E4", system="western")
|
||||
G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
# The overtone series — the fifth is "built into" every tone
|
||||
C4.overtones(6)
|
||||
# [261.63, 523.25, 784.88, 1046.50, 1308.13, 1569.75]
|
||||
# 3rd harmonic (784.88) ≈ G5 (783.99) — a perfect fifth
|
||||
|
||||
# Consonance: simple frequency ratios score high
|
||||
fifth = Chord([C4, G4]) # 3:2 ratio
|
||||
tritone = Chord([C4, C4 + 6]) # 45:32 ratio
|
||||
fifth.harmony > tritone.harmony # True
|
||||
|
||||
# Dissonance: Plomp-Levelt roughness model
|
||||
# An octave has low roughness (frequencies far apart)
|
||||
# A major 3rd has more roughness (closer frequencies)
|
||||
octave = Chord([C4, C4 + 12])
|
||||
third = Chord([C4, E4])
|
||||
octave.dissonance < third.dissonance # True
|
||||
|
||||
# Tension: tritones and dominant function
|
||||
c_major = Chord([C4, E4, G4])
|
||||
c_major.tension['score'] # 0.0 — fully resolved
|
||||
|
||||
g7 = Chord([G4, G4+4, G4+7, G4+10]) # G dominant 7th
|
||||
g7.tension['score'] # 0.6 — wants to resolve
|
||||
g7.tension['tritones'] # 1 (B-F)
|
||||
g7.tension['has_dominant_function'] # True
|
||||
|
||||
# Beat frequencies — the pulsing between close pitches
|
||||
g7.beat_frequencies
|
||||
# [(tone_a, tone_b, hz), ...] sorted by frequency
|
||||
|
||||
Further Reading
|
||||
---------------
|
||||
|
||||
- `Music theory <https://en.wikipedia.org/wiki/Music_theory>`_ — Wikipedia overview
|
||||
- `Equal temperament <https://en.wikipedia.org/wiki/Equal_temperament>`_ — the modern tuning system
|
||||
- `Circle of fifths <https://en.wikipedia.org/wiki/Circle_of_fifths>`_ — key relationships
|
||||
- `Chord progression <https://en.wikipedia.org/wiki/Chord_progression>`_ — common patterns
|
||||
- `Voice leading <https://en.wikipedia.org/wiki/Voice_leading>`_ — smooth chord connections
|
||||
- `Raga <https://en.wikipedia.org/wiki/Raga>`_ — Indian melodic framework
|
||||
- `Maqam <https://en.wikipedia.org/wiki/Maqam>`_ — Arabic melodic system
|
||||
- `Gamelan <https://en.wikipedia.org/wiki/Gamelan>`_ — Indonesian ensemble music
|
||||
- `Blues <https://en.wikipedia.org/wiki/Blues>`_ — the foundation of American popular music
|
||||
- `Twelve-bar blues <https://en.wikipedia.org/wiki/Twelve-bar_blues>`_ — the most common blues form
|
||||
+12
-187
@@ -2,40 +2,7 @@ Working with Tones
|
||||
==================
|
||||
|
||||
A :class:`~pytheory.tones.Tone` represents a single musical note, optionally
|
||||
with an octave number in `scientific pitch notation <https://en.wikipedia.org/wiki/Scientific_pitch_notation>`_ (e.g. C4 = middle C).
|
||||
|
||||
What is a Tone?
|
||||
---------------
|
||||
|
||||
A musical tone is a sound with a definite pitch — a periodic vibration at
|
||||
a specific frequency. In the Western 12-tone system, the octave (a 2:1
|
||||
frequency ratio) is divided into 12 equal steps called **semitones** or
|
||||
**half steps**. Two semitones make a **whole step** (whole tone).
|
||||
|
||||
The 12 chromatic tones are::
|
||||
|
||||
C C#/Db D D#/Eb E F F#/Gb G G#/Ab A A#/Bb B
|
||||
|
||||
Notes with two names (like C# and Db) are `enharmonic equivalents <https://en.wikipedia.org/wiki/Enharmonic>`_ —
|
||||
different names for the same pitch. Whether you call it C# or Db depends
|
||||
on the musical context (key signature, harmonic function).
|
||||
|
||||
Scientific Pitch Notation
|
||||
-------------------------
|
||||
|
||||
Each tone can be assigned an octave number. The standard is **scientific
|
||||
pitch notation**, where the octave number increments at C::
|
||||
|
||||
... B3 C4 C#4 D4 ... A4 B4 C5 C#5 ...
|
||||
^ ^
|
||||
middle C one octave up
|
||||
|
||||
Key reference points:
|
||||
|
||||
- `A4 = 440 Hz <https://en.wikipedia.org/wiki/A440_(pitch_standard)>`_ — the international tuning standard (ISO 16)
|
||||
- **C4 = 261.63 Hz** — middle C on the piano
|
||||
- **A0 = 27.5 Hz** — the lowest A on a standard piano
|
||||
- **C8 = 4186 Hz** — the highest C on a standard piano
|
||||
with an octave number (scientific pitch notation).
|
||||
|
||||
Creating Tones
|
||||
--------------
|
||||
@@ -44,7 +11,7 @@ Creating Tones
|
||||
|
||||
from pytheory import Tone
|
||||
|
||||
# From a string (most common)
|
||||
# From a string
|
||||
c4 = Tone.from_string("C4")
|
||||
cs4 = Tone.from_string("C#4")
|
||||
|
||||
@@ -72,87 +39,24 @@ Properties
|
||||
Pitch and Frequency
|
||||
-------------------
|
||||
|
||||
Every tone vibrates at a specific frequency measured in Hertz (Hz —
|
||||
cycles per second). The relationship between pitch and frequency is
|
||||
**logarithmic**: each octave doubles the frequency, and each semitone
|
||||
multiplies by the 12th root of 2 (~1.05946).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4.frequency
|
||||
440.0
|
||||
|
||||
>>> Tone.from_string("A3", system="western").frequency
|
||||
220.0 # One octave down = half the frequency
|
||||
|
||||
>>> Tone.from_string("C4", system="western").frequency
|
||||
261.63 # Middle C
|
||||
|
||||
Temperament
|
||||
~~~~~~~~~~~
|
||||
|
||||
**Temperament** is the system used to tune the intervals between notes.
|
||||
Different temperaments produce slightly different frequencies for the
|
||||
same note name:
|
||||
|
||||
- `Equal temperament <https://en.wikipedia.org/wiki/Equal_temperament>`_ (default): Every semitone has an identical
|
||||
frequency ratio of 2^(1/12). This is the modern standard — it allows
|
||||
free modulation between all keys but no interval is acoustically
|
||||
"pure" except the octave.
|
||||
|
||||
- `Pythagorean temperament <https://en.wikipedia.org/wiki/Pythagorean_tuning>`_: Built entirely from pure perfect fifths
|
||||
(3:2 ratio). Produces beatless fifths but introduces the "Pythagorean
|
||||
comma" — a small discrepancy when 12 fifths don't quite equal 7
|
||||
octaves. Used in medieval European music.
|
||||
|
||||
- `Quarter-comma meantone <https://en.wikipedia.org/wiki/Quarter-comma_meantone>`_: Tunes major thirds to the pure ratio of
|
||||
5:4, distributing the resulting error across the fifths. Dominant in
|
||||
Renaissance and Baroque music (15th–18th century). Sounds beautiful
|
||||
in closely related keys but "wolf intervals" make distant keys
|
||||
unusable.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> a4.pitch(temperament="equal")
|
||||
>>> a4.pitch()
|
||||
440.0
|
||||
|
||||
# Different temperaments
|
||||
>>> a4.pitch(temperament="pythagorean")
|
||||
440.0 # A4 is always 440 (it's the reference)
|
||||
440.0
|
||||
|
||||
>>> c5 = Tone.from_string("C5", system="western")
|
||||
>>> c5.pitch(temperament="equal")
|
||||
523.25
|
||||
>>> c5.pitch(temperament="pythagorean")
|
||||
521.48 # Slightly different!
|
||||
|
||||
# Symbolic output (SymPy expression)
|
||||
# Symbolic (SymPy expression)
|
||||
>>> a4.pitch(symbolic=True)
|
||||
440
|
||||
|
||||
Intervals and Arithmetic
|
||||
-------------------------
|
||||
|
||||
An **interval** is the distance between two pitches, measured in
|
||||
semitones. Intervals have both a **quantity** (number of scale steps)
|
||||
and a **quality** (perfect, major, minor, augmented, diminished).
|
||||
|
||||
Common intervals::
|
||||
|
||||
Semitones Name Sound
|
||||
───────── ──── ─────
|
||||
0 Unison Same note
|
||||
1 Minor 2nd Tense, dissonant (Jaws theme)
|
||||
2 Major 2nd A whole step (Do-Re)
|
||||
3 Minor 3rd Sad, dark (Greensleeves)
|
||||
4 Major 3rd Happy, bright (Kumbaya)
|
||||
5 Perfect 4th Open, hollow (Here Comes the Bride)
|
||||
6 Tritone Unstable, tense (The Simpsons)
|
||||
7 Perfect 5th Strong, stable (Star Wars)
|
||||
8 Minor 6th Bittersweet
|
||||
9 Major 6th Warm (My Bonnie)
|
||||
10 Minor 7th Bluesy (Star Trek TOS)
|
||||
11 Major 7th Dreamy, yearning
|
||||
12 Octave Same note, higher
|
||||
Arithmetic
|
||||
----------
|
||||
|
||||
Tones support ``+`` and ``-`` operators for semitone math:
|
||||
|
||||
@@ -171,17 +75,13 @@ Subtracting two tones gives the semitone distance:
|
||||
.. code-block:: python
|
||||
|
||||
>>> g4 = Tone.from_string("G4", system="western")
|
||||
>>> g4 - c4 # Perfect fifth = 7 semitones
|
||||
>>> g4 - c4 # Semitone distance
|
||||
7
|
||||
|
||||
>>> c5 = Tone.from_string("C5", system="western")
|
||||
>>> c5 - c4 # Octave = 12 semitones
|
||||
12
|
||||
|
||||
Comparison and Sorting
|
||||
----------------------
|
||||
|
||||
Tones can be compared and sorted by pitch frequency:
|
||||
Tones can be compared and sorted by pitch:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -194,82 +94,7 @@ Equality checks note name and octave:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> c4 == "C" # Compare with string (name only)
|
||||
>>> c4 == "C" # Compare with string
|
||||
True
|
||||
>>> c4 == Tone(name="C", octave=4)
|
||||
True
|
||||
|
||||
The Overtone Series
|
||||
-------------------
|
||||
|
||||
Every tone you hear is actually a composite of many frequencies. When
|
||||
a string vibrates, it doesn't just vibrate as a whole — it also vibrates
|
||||
in halves, thirds, quarters, and so on, producing the `harmonic series <https://en.wikipedia.org/wiki/Harmonic_series_(music)>`_:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4.overtones(8)
|
||||
[440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0, 3080.0, 3520.0]
|
||||
|
||||
These harmonics correspond to musical intervals::
|
||||
|
||||
Harmonic Frequency Interval from fundamental
|
||||
1st 440 Hz Unison (A4)
|
||||
2nd 880 Hz Octave (A5)
|
||||
3rd 1320 Hz Octave + perfect 5th (E6)
|
||||
4th 1760 Hz Two octaves (A6)
|
||||
5th 2200 Hz Two octaves + major 3rd (C#7)
|
||||
6th 2640 Hz Two octaves + perfect 5th (E7)
|
||||
7th 3080 Hz Two octaves + minor 7th (≈G7, slightly flat)
|
||||
8th 3520 Hz Three octaves (A7)
|
||||
|
||||
The overtone series is why a perfect fifth sounds consonant — the 3rd
|
||||
harmonic of the lower note matches the 2nd harmonic of the upper note.
|
||||
It's also why the major triad (root, major 3rd, perfect 5th) feels
|
||||
"natural" — these intervals appear in the first 6 harmonics.
|
||||
|
||||
Different instruments emphasize different harmonics, which is why a
|
||||
violin and a flute playing the same note sound different. This quality
|
||||
is called `timbre <https://en.wikipedia.org/wiki/Timbre>`_.
|
||||
|
||||
Enharmonic Equivalents
|
||||
----------------------
|
||||
|
||||
In equal temperament, C# and Db are the same pitch (they have the
|
||||
same frequency). They're called **enharmonic equivalents**. Which name
|
||||
you use depends on context:
|
||||
|
||||
- In the key of **D major** (2 sharps), you write **C#**
|
||||
- In the key of **Gb major** (6 flats), you write **Db**
|
||||
|
||||
The rule: each letter name should appear exactly once in a scale. The
|
||||
D major scale is D E F# G A B C# — not D E Gb G A B Db, even though
|
||||
F#=Gb and C#=Db.
|
||||
|
||||
PyTheory uses sharps by default (following the tone list ordering), but
|
||||
tones carry their enharmonic equivalents:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> Tone.from_tuple(("C#", "Db")).names()
|
||||
['C#', 'Db']
|
||||
|
||||
The Circle of Fifths
|
||||
--------------------
|
||||
|
||||
The `circle of fifths <https://en.wikipedia.org/wiki/Circle_of_fifths>`_ is the most important diagram in Western music
|
||||
theory. Starting from any note and ascending by perfect fifths (7
|
||||
semitones), you pass through all 12 chromatic tones before returning
|
||||
to the starting note:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> t = Tone.from_string("C4", system="western")
|
||||
>>> for i in range(12):
|
||||
... print(t.name, end=" ")
|
||||
... t = t + 7
|
||||
C G D A E B F# C# G# D# A# F
|
||||
|
||||
Each step clockwise adds one sharp to the key signature; each step
|
||||
counter-clockwise (ascending by fourths = 5 semitones) adds one flat.
|
||||
|
||||
@@ -27,12 +27,10 @@ Work with tones, scales, chords, and fretboards using a clean, Pythonic API.
|
||||
:caption: User Guide
|
||||
|
||||
guide/quickstart
|
||||
guide/theory
|
||||
guide/tones
|
||||
guide/scales
|
||||
guide/chords
|
||||
guide/fretboard
|
||||
guide/systems
|
||||
guide/playback
|
||||
|
||||
.. toctree::
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
"""Explore music theory with PyTheory."""
|
||||
|
||||
from pytheory import Key, Chord, Tone, Interval, PROGRESSIONS, Fretboard
|
||||
|
||||
# ── Keys and Scales ──────────────────────────────────────────────────────
|
||||
|
||||
key = Key("C", "major")
|
||||
print(f"Key: {key}")
|
||||
print(f"Notes: {key.note_names}")
|
||||
print()
|
||||
|
||||
# ── Diatonic Harmony ─────────────────────────────────────────────────────
|
||||
|
||||
print("Diatonic triads:")
|
||||
for i, chord in enumerate(key.scale.harmonize()):
|
||||
analysis = chord.analyze("C")
|
||||
print(f" {analysis:4s} {chord}")
|
||||
|
||||
print()
|
||||
print("Diatonic seventh chords:")
|
||||
for name in key.seventh_chords:
|
||||
print(f" {name}")
|
||||
|
||||
# ── Progressions ─────────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Common progressions in C major:")
|
||||
for name, numerals in PROGRESSIONS.items():
|
||||
chords = key.progression(*numerals)
|
||||
chord_names = [str(c) for c in chords]
|
||||
print(f" {name:20s} {' → '.join(chord_names)}")
|
||||
|
||||
# ── Intervals ────────────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
c4 = Tone.from_string("C4", system="western")
|
||||
print("Intervals from C4:")
|
||||
for semitones in range(13):
|
||||
tone = c4 + semitones
|
||||
name = c4.interval_to(tone)
|
||||
print(f" {semitones:2d} semitones = {tone.name:3s} ({name})")
|
||||
|
||||
# ── Circle of Fifths ─────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
print("Circle of fifths:", " → ".join(t.name for t in c4.circle_of_fifths()))
|
||||
|
||||
# ── Chord Analysis ───────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
g7 = Chord.from_name("G7")
|
||||
print(f"Chord: {g7}")
|
||||
print(f" Intervals: {g7.intervals}")
|
||||
print(f" Tension: {g7.tension}")
|
||||
print(f" Analysis in C: {g7.analyze('C')}")
|
||||
|
||||
# ── Guitar Fingerings ────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
fb = Fretboard.guitar()
|
||||
print("Guitar fingerings:")
|
||||
for name in ["C", "G", "Am", "F", "Dm", "E7"]:
|
||||
from pytheory import CHARTS
|
||||
fingering = CHARTS["western"][name].fingering(fretboard=fb)
|
||||
print(f" {name:4s} {fingering}")
|
||||
|
||||
# ── Overtone Series ──────────────────────────────────────────────────────
|
||||
|
||||
print()
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
print(f"Overtone series of {a4}:")
|
||||
for i, hz in enumerate(a4.overtones(8), 1):
|
||||
nearest = Tone.from_frequency(hz)
|
||||
print(f" Harmonic {i}: {hz:8.1f} Hz ≈ {nearest.full_name}")
|
||||
+2
-27
@@ -1,25 +1,10 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.3.2"
|
||||
version = "0.2.0"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "Kenneth Reitz", email = "me@kennethreitz.org" },
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: Education",
|
||||
"Topic :: Multimedia :: Sound/Audio",
|
||||
"Topic :: Multimedia :: Sound/Audio :: Analysis",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"pytuning",
|
||||
"numeral",
|
||||
@@ -27,19 +12,9 @@ dependencies = [
|
||||
"scipy",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/kennethreitz/pytheory"
|
||||
Documentation = "https://pytheory.kennethreitz.org"
|
||||
Repository = "https://github.com/kennethreitz/pytheory"
|
||||
Issues = "https://github.com/kennethreitz/pytheory/issues"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest"]
|
||||
docs = ["sphinx"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["pytheory"]
|
||||
|
||||
+4
-16
@@ -1,25 +1,13 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
from math import ceil, floor
|
||||
|
||||
__version__ = "0.3.2"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .tones import Tone
|
||||
from .systems import System, SYSTEMS
|
||||
from .scales import Scale, TonedScale, Key, PROGRESSIONS
|
||||
from .scales import Scale, TonedScale
|
||||
from .chords import Chord, Fretboard
|
||||
from .charts import CHARTS, charts_for_fretboard
|
||||
|
||||
try:
|
||||
from .play import play, Synth
|
||||
except OSError:
|
||||
# sounddevice requires PortAudio; gracefully degrade if unavailable
|
||||
play = None
|
||||
Synth = None
|
||||
|
||||
# Aliases for discoverability.
|
||||
Note = Tone
|
||||
|
||||
__all__ = [
|
||||
"Tone", "Note", "Interval", "Scale", "TonedScale", "Key",
|
||||
"PROGRESSIONS", "Chord", "Fretboard",
|
||||
"System", "SYSTEMS", "CHARTS", "charts_for_fretboard",
|
||||
"play", "Synth",
|
||||
]
|
||||
|
||||
+2
-301
@@ -21,84 +21,7 @@ TONES = {
|
||||
("F#", "Gb"),
|
||||
("G",),
|
||||
("G#", "Ab"),
|
||||
],
|
||||
# Indian classical (Hindustani) system.
|
||||
# Ordered A-based to match Western index positions (Sa = index 3 = C).
|
||||
"indian": [
|
||||
("Dha",), # A — shuddha dhaivat
|
||||
("komal Ni",), # Bb — komal nishad
|
||||
("Ni",), # B — shuddha nishad
|
||||
("Sa",), # C — shadja
|
||||
("komal Re",), # Db — komal rishabh
|
||||
("Re",), # D — shuddha rishabh
|
||||
("komal Ga",), # Eb — komal gandhar
|
||||
("Ga",), # E — shuddha gandhar
|
||||
("Ma",), # F — shuddha madhyam
|
||||
("tivra Ma",), # F# — tivra madhyam
|
||||
("Pa",), # G — pancham
|
||||
("komal Dha",), # Ab — komal dhaivat
|
||||
],
|
||||
# Arabic maqam system — Arabic solfège names.
|
||||
"arabic": [
|
||||
("La",), # A
|
||||
("Sib",), # Bb — Si bemol
|
||||
("Si",), # B
|
||||
("Do",), # C
|
||||
("Reb",), # Db — Re bemol
|
||||
("Re",), # D
|
||||
("Mib",), # Eb — Mi bemol
|
||||
("Mi",), # E
|
||||
("Fa",), # F
|
||||
("Fa#",), # F#
|
||||
("Sol",), # G
|
||||
("Solb",), # Ab — Sol bemol
|
||||
],
|
||||
# Japanese system — uses Western names; scales are the unique part.
|
||||
"japanese": [
|
||||
("A",),
|
||||
("A#", "Bb"),
|
||||
("B",),
|
||||
("C",),
|
||||
("C#", "Db"),
|
||||
("D",),
|
||||
("D#", "Eb"),
|
||||
("E",),
|
||||
("F",),
|
||||
("F#", "Gb"),
|
||||
("G",),
|
||||
("G#", "Ab"),
|
||||
],
|
||||
# Blues/Pentatonic — Western names with blues and pentatonic scales.
|
||||
"blues": [
|
||||
("A",),
|
||||
("A#", "Bb"),
|
||||
("B",),
|
||||
("C",),
|
||||
("C#", "Db"),
|
||||
("D",),
|
||||
("D#", "Eb"),
|
||||
("E",),
|
||||
("F",),
|
||||
("F#", "Gb"),
|
||||
("G",),
|
||||
("G#", "Ab"),
|
||||
],
|
||||
# Javanese gamelan — pelog approximation in 12-TET.
|
||||
# True gamelan uses non-Western intonation; these are closest 12-TET fits.
|
||||
"gamelan": [
|
||||
("nem",), # A — 6
|
||||
("pi",), # Bb — 7 (barang in some)
|
||||
("barang",), # B — 7
|
||||
("ji",), # C — 1
|
||||
("ro-",), # Db — 2b
|
||||
("ro",), # D — 2
|
||||
("lu-",), # Eb — 3b
|
||||
("lu",), # E — 3
|
||||
("pat",), # F — 4
|
||||
("pat+",), # F# — 4#
|
||||
("mo",), # G — 5
|
||||
("nem-",), # Ab — 6b
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
DEGREES = {
|
||||
@@ -111,53 +34,7 @@ DEGREES = {
|
||||
("submediant", ("aeolian", "lydian")),
|
||||
("leading tone", ("locrian", "mixolydian")),
|
||||
("octave", ("ionian", "aeolian")),
|
||||
],
|
||||
"indian": [
|
||||
("shadja", ()), # Sa — the tonic
|
||||
("rishabh", ()), # Re — 2nd
|
||||
("gandhar", ()), # Ga — 3rd
|
||||
("madhyam", ()), # Ma — 4th
|
||||
("pancham", ()), # Pa — 5th
|
||||
("dhaivat", ()), # Dha — 6th
|
||||
("nishad", ()), # Ni — 7th
|
||||
("saptak", ()), # Sa — octave
|
||||
],
|
||||
"arabic": [
|
||||
("qarar", ()), # 1st — root
|
||||
("nawa", ()), # 2nd
|
||||
("thalth", ()), # 3rd
|
||||
("arba", ()), # 4th
|
||||
("khamis", ()), # 5th
|
||||
("sadis", ()), # 6th
|
||||
("sabi", ()), # 7th
|
||||
("jawab", ()), # octave
|
||||
],
|
||||
"japanese": [
|
||||
("ichi", ()), # 1st
|
||||
("ni", ()), # 2nd
|
||||
("san", ()), # 3rd
|
||||
("shi", ()), # 4th
|
||||
("go", ()), # 5th
|
||||
("roku", ()), # 6th
|
||||
],
|
||||
"blues": [
|
||||
("tonic", ()),
|
||||
("supertonic", ()),
|
||||
("mediant", ()),
|
||||
("subdominant", ()),
|
||||
("dominant", ()),
|
||||
("submediant", ()),
|
||||
("subtonic", ()),
|
||||
],
|
||||
"gamelan": [
|
||||
("ji", ()), # 1
|
||||
("ro", ()), # 2
|
||||
("lu", ()), # 3
|
||||
("pat", ()), # 4
|
||||
("mo", ()), # 5
|
||||
("nem", ()), # 6
|
||||
("pi", ()), # 7
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
SCALES = {
|
||||
@@ -192,182 +69,6 @@ SCALES = {
|
||||
}
|
||||
}
|
||||
|
||||
# Indian scales — the 10 thaats of Hindustani classical music.
|
||||
# Each thaat defines a parent scale from which ragas are derived.
|
||||
INDIAN_SCALES = {
|
||||
12: {
|
||||
"chromatic": (12, {}),
|
||||
"thaat": [
|
||||
7,
|
||||
{
|
||||
# Bilawal = Western major / Ionian
|
||||
"bilawal": {"intervals": (2, 2, 1, 2, 2, 2, 1)},
|
||||
# Khamaj = Western Mixolydian
|
||||
"khamaj": {"intervals": (2, 2, 1, 2, 2, 1, 2)},
|
||||
# Kafi = Western Dorian
|
||||
"kafi": {"intervals": (2, 1, 2, 2, 2, 1, 2)},
|
||||
# Asavari = Western natural minor / Aeolian
|
||||
"asavari": {"intervals": (2, 1, 2, 2, 1, 2, 2)},
|
||||
# Bhairavi = Western Phrygian
|
||||
"bhairavi": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
|
||||
# Kalyan = Western Lydian
|
||||
"kalyan": {"intervals": (2, 2, 2, 1, 2, 2, 1)},
|
||||
# Bhairav — unique to Indian music (no Western equivalent)
|
||||
# Sa re Ga Ma Pa dha Ni
|
||||
"bhairav": {"intervals": (1, 3, 1, 2, 1, 3, 1)},
|
||||
# Poorvi — unique to Indian music
|
||||
# Sa re Ga tivra-Ma Pa dha Ni
|
||||
"poorvi": {"intervals": (1, 3, 2, 1, 1, 3, 1)},
|
||||
# Marwa — unique to Indian music
|
||||
# Sa re Ga tivra-Ma Pa Dha Ni
|
||||
"marwa": {"intervals": (1, 3, 2, 1, 2, 2, 1)},
|
||||
# Todi — unique to Indian music
|
||||
# Sa re komal-Ga tivra-Ma Pa dha Ni
|
||||
"todi": {"intervals": (1, 2, 3, 1, 1, 3, 1)},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
# Arabic maqam scales (12-TET approximations).
|
||||
# True maqam uses quarter-tones; these are the closest 12-tone equivalents.
|
||||
ARABIC_SCALES = {
|
||||
12: {
|
||||
"chromatic": (12, {}),
|
||||
"maqam": [
|
||||
7,
|
||||
{
|
||||
# Ajam = Western major
|
||||
"ajam": {"intervals": (2, 2, 1, 2, 2, 2, 1)},
|
||||
# Nahawand = Western harmonic minor
|
||||
"nahawand": {"intervals": (2, 1, 2, 2, 1, 3, 1)},
|
||||
# Kurd = Western Phrygian
|
||||
"kurd": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
|
||||
# Hijaz — augmented 2nd between 2nd and 3rd degrees
|
||||
"hijaz": {"intervals": (1, 3, 1, 2, 1, 2, 2)},
|
||||
# Nikriz — augmented 2nd between 3rd and 4th
|
||||
"nikriz": {"intervals": (2, 1, 3, 1, 2, 1, 2)},
|
||||
# Bayati (12-TET approx) — true bayati has quarter-flat 2nd
|
||||
"bayati": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
|
||||
# Rast (12-TET approx) — true rast has quarter-flat 3rd and 7th
|
||||
"rast": {"intervals": (2, 1, 2, 2, 2, 1, 2)},
|
||||
# Saba (12-TET approx) — true saba has quarter-flat 2nd
|
||||
"saba": {"intervals": (1, 2, 1, 3, 1, 2, 2)},
|
||||
# Sikah (12-TET approx) — true sikah starts on quarter-flat
|
||||
"sikah": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
|
||||
# Jiharkah
|
||||
"jiharkah": {"intervals": (2, 2, 1, 2, 2, 1, 2)},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
# Japanese pentatonic scales.
|
||||
JAPANESE_SCALES = {
|
||||
12: {
|
||||
"chromatic": (12, {}),
|
||||
"pentatonic": [
|
||||
5,
|
||||
{
|
||||
# Hirajoshi — the most well-known Japanese scale
|
||||
# C D Eb G Ab
|
||||
"hirajoshi": {"intervals": (2, 1, 4, 1, 4)},
|
||||
# In (Miyako-bushi) — used in koto music
|
||||
# C Db F G Ab
|
||||
"in": {"intervals": (1, 4, 2, 1, 4)},
|
||||
# Yo — folk music scale
|
||||
# C D F G Bb
|
||||
"yo": {"intervals": (2, 3, 2, 3, 2)},
|
||||
# Iwato — dark, dissonant pentatonic
|
||||
# C Db F Gb Bb
|
||||
"iwato": {"intervals": (1, 4, 1, 4, 2)},
|
||||
# Kumoi — similar to minor pentatonic
|
||||
# C D Eb G A
|
||||
"kumoi": {"intervals": (2, 1, 4, 2, 3)},
|
||||
# Insen — modern Japanese scale
|
||||
# C Db F G Bb
|
||||
"insen": {"intervals": (1, 4, 2, 3, 2)},
|
||||
},
|
||||
],
|
||||
"heptatonic": [
|
||||
7,
|
||||
{
|
||||
# Ritsu — gagaku court music scale
|
||||
# C D Eb F G A Bb (= Dorian)
|
||||
"ritsu": {"intervals": (2, 1, 2, 2, 2, 1, 2)},
|
||||
# Ryo — gagaku court music scale
|
||||
# C D E F# G A B (= Lydian)
|
||||
"ryo": {"intervals": (2, 2, 2, 1, 2, 2, 1)},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
# Blues and pentatonic scales — foundational to American music.
|
||||
BLUES_SCALES = {
|
||||
12: {
|
||||
"chromatic": (12, {}),
|
||||
"pentatonic": [
|
||||
5,
|
||||
{
|
||||
# Major pentatonic — C D E G A
|
||||
"major pentatonic": {"intervals": (2, 2, 3, 2, 3)},
|
||||
# Minor pentatonic — C Eb F G Bb
|
||||
"minor pentatonic": {"intervals": (3, 2, 2, 3, 2)},
|
||||
},
|
||||
],
|
||||
"hexatonic": [
|
||||
6,
|
||||
{
|
||||
# Blues scale — C Eb F F# G Bb
|
||||
"blues": {"intervals": (3, 2, 1, 1, 3, 2)},
|
||||
# Major blues — C D D# E G A
|
||||
"major blues": {"intervals": (2, 1, 1, 3, 2, 3)},
|
||||
},
|
||||
],
|
||||
"heptatonic": [
|
||||
7,
|
||||
{
|
||||
# Mixolydian (dominant blues sound) — C D E F G A Bb
|
||||
"dominant": {"intervals": (2, 2, 1, 2, 2, 1, 2)},
|
||||
# Dorian (minor blues/jazz) — C D Eb F G A Bb
|
||||
"minor": {"intervals": (2, 1, 2, 2, 2, 1, 2)},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
# Javanese gamelan scales — 12-TET approximations.
|
||||
# True gamelan tuning varies between ensembles and does not conform
|
||||
# to equal temperament. These approximations capture the melodic
|
||||
# character of the scales.
|
||||
GAMELAN_SCALES = {
|
||||
12: {
|
||||
"chromatic": (12, {}),
|
||||
"pentatonic": [
|
||||
5,
|
||||
{
|
||||
# Slendro — roughly equal 5-tone division of the octave
|
||||
# Approximated as: C D F G Bb
|
||||
"slendro": {"intervals": (2, 3, 2, 3, 2)},
|
||||
# Pelog pathet nem — C Db E F G (approx)
|
||||
"pelog nem": {"intervals": (1, 3, 1, 2, 5)},
|
||||
# Pelog pathet barang — C Db E F# B (approx)
|
||||
"pelog barang": {"intervals": (1, 3, 3, 4, 1)},
|
||||
# Pelog pathet lima — C Db E F Ab (approx)
|
||||
"pelog lima": {"intervals": (1, 3, 1, 3, 4)},
|
||||
},
|
||||
],
|
||||
"heptatonic": [
|
||||
7,
|
||||
{
|
||||
# Full pelog — all 7 tones: C Db E F G Ab B (approx)
|
||||
"pelog": {"intervals": (1, 3, 1, 2, 1, 3, 1)},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
SYSTEMS = NotImplemented
|
||||
|
||||
# Modes are rotations of the major scale pattern.
|
||||
|
||||
@@ -154,29 +154,6 @@ class NamedChord:
|
||||
else:
|
||||
return tuple([self.fix_fingering(f) for f in best_fingerings])
|
||||
|
||||
def tab(self, *, fretboard):
|
||||
"""Render this chord as ASCII guitar tablature.
|
||||
|
||||
Example::
|
||||
|
||||
>>> print(CHARTS["western"]["C"].tab(fretboard=Fretboard.guitar()))
|
||||
C
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
A|--3--
|
||||
E|--0--
|
||||
"""
|
||||
fingering = self.fingering(fretboard=fretboard)
|
||||
string_names = [t.name for t in fretboard.tones]
|
||||
lines = [self.name]
|
||||
max_name = max(len(n) for n in string_names)
|
||||
for i, (name, fret) in enumerate(zip(string_names, fingering)):
|
||||
fret_str = "x" if fret is None else str(fret)
|
||||
lines.append(f"{name:>{max_name}}|--{fret_str}--")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
western_chart = {}
|
||||
for tone_titles in SYSTEMS["western"].tone_names:
|
||||
|
||||
+64
-1097
File diff suppressed because it is too large
Load Diff
+17
-429
@@ -1,25 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
import numeral
|
||||
|
||||
from .systems import SYSTEMS, System
|
||||
from .systems import SYSTEMS
|
||||
from .tones import Tone
|
||||
|
||||
|
||||
class Scale:
|
||||
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*.
|
||||
"""
|
||||
def __init__(self, *, tones, degrees=None, system='western'):
|
||||
self.tones = tones
|
||||
self.degrees = degrees
|
||||
|
||||
@@ -35,18 +21,14 @@ class Scale:
|
||||
raise ValueError("The number of tones and degrees must be equal!")
|
||||
|
||||
@property
|
||||
def system(self) -> Optional[System]:
|
||||
"""Return the tone system for this scale.
|
||||
|
||||
Resolves a system name to a :class:`System` object on first access.
|
||||
"""
|
||||
def system(self):
|
||||
if self._system:
|
||||
return self._system
|
||||
|
||||
if self.system_name:
|
||||
return SYSTEMS[self.system_name]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
def __repr__(self):
|
||||
r = []
|
||||
for (i, tone) in enumerate(self.tones):
|
||||
degree = numeral.int2roman(i + 1, only_ascii=True)
|
||||
@@ -56,25 +38,22 @@ class Scale:
|
||||
return f"<Scale {r}>"
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the tones in this scale."""
|
||||
return iter(self.tones)
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of tones in this scale (including the octave)."""
|
||||
def __len__(self):
|
||||
return len(self.tones)
|
||||
|
||||
def __contains__(self, item: Union[str, Tone]) -> bool:
|
||||
"""Check whether a tone or note name belongs to this scale."""
|
||||
def __contains__(self, item):
|
||||
if isinstance(item, str):
|
||||
return any(item == t.name for t in self.tones)
|
||||
return item in self.tones
|
||||
|
||||
@property
|
||||
def note_names(self) -> list[str]:
|
||||
def note_names(self):
|
||||
"""List of note names in this scale."""
|
||||
return [t.name for t in self.tones]
|
||||
|
||||
def chord(self, *degrees: int) -> Chord:
|
||||
def chord(self, *degrees):
|
||||
"""Build a Chord from scale degrees (0-indexed).
|
||||
|
||||
Wraps around if degrees exceed the scale length, transposing
|
||||
@@ -96,146 +75,14 @@ class Scale:
|
||||
result.append(tone)
|
||||
return Chord(tones=result)
|
||||
|
||||
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
|
||||
scale's interval pattern.
|
||||
|
||||
Example::
|
||||
|
||||
>>> c_major = TonedScale(tonic="C4")["major"]
|
||||
>>> d_major = c_major.transpose(2)
|
||||
>>> d_major.note_names
|
||||
['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D']
|
||||
"""
|
||||
from .chords import Chord
|
||||
new_tones = tuple(t.add(semitones) for t in self.tones)
|
||||
return Scale(tones=new_tones)
|
||||
|
||||
def triad(self, root: int = 0) -> Chord:
|
||||
def triad(self, root=0):
|
||||
"""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: 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: str) -> list[Chord]:
|
||||
"""Build a chord progression from Roman numeral strings.
|
||||
|
||||
Accepts Roman numerals like ``"I"``, ``"IV"``, ``"V"``,
|
||||
``"ii"``, ``"vi"``. Lowercase = minor triad, uppercase = major
|
||||
triad. Add ``"7"`` suffix for seventh chords.
|
||||
|
||||
Example::
|
||||
|
||||
>>> scale.progression("I", "IV", "V", "I")
|
||||
[<Chord (C,E,G)>, <Chord (F,A,C)>, <Chord (G,B,D)>, <Chord (C,E,G)>]
|
||||
"""
|
||||
import numeral as numeral_mod
|
||||
chords = []
|
||||
for num in numerals:
|
||||
is_seventh = num.endswith("7")
|
||||
clean = num.rstrip("7")
|
||||
degree = numeral_mod.roman2int(clean.upper()) - 1
|
||||
if is_seventh:
|
||||
chords.append(self.seventh(degree))
|
||||
else:
|
||||
chords.append(self.triad(degree))
|
||||
return chords
|
||||
|
||||
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>`_
|
||||
uses Arabic numerals instead of Roman numerals.
|
||||
It's the standard chart system in Nashville recording studios.
|
||||
|
||||
Numbers 1-7 build diatonic triads. Suffix ``"7"`` for seventh
|
||||
chords, ``"m"`` to force minor.
|
||||
|
||||
Example::
|
||||
|
||||
>>> scale.nashville(1, 4, 5, 1)
|
||||
[<Chord C major>, <Chord F major>, <Chord G major>, <Chord C major>]
|
||||
"""
|
||||
from .chords import Chord
|
||||
chords = []
|
||||
for num in numbers:
|
||||
s = str(num)
|
||||
is_seventh = s.endswith("7")
|
||||
clean = s.rstrip("7m")
|
||||
degree = int(clean) - 1
|
||||
if is_seventh:
|
||||
chords.append(self.seventh(degree))
|
||||
else:
|
||||
chords.append(self.triad(degree))
|
||||
return chords
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
match as a ``(tonic, scale_name, match_count)`` tuple.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Scale.detect("C", "D", "E", "F", "G", "A", "B")
|
||||
('C', 'major', 7)
|
||||
>>> Scale.detect("C", "D", "Eb", "F", "G", "Ab", "Bb")
|
||||
('C', 'minor', 7)
|
||||
"""
|
||||
if not note_names:
|
||||
return None
|
||||
|
||||
notes = set(note_names)
|
||||
best = None
|
||||
|
||||
chromatic = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
||||
scale_names = ["major", "minor", "harmonic minor",
|
||||
"dorian", "phrygian", "lydian", "mixolydian",
|
||||
"aeolian", "locrian"]
|
||||
|
||||
for tonic in chromatic:
|
||||
ts = TonedScale(tonic=f"{tonic}4")
|
||||
for scale_name in ts.scales:
|
||||
try:
|
||||
scale = ts[scale_name]
|
||||
scale_notes = set(scale.note_names)
|
||||
match = len(notes & scale_notes)
|
||||
score = (match, 1 if scale_name == "major" else 0)
|
||||
if best is None or score > best[0]:
|
||||
best = (score, tonic, scale_name, match)
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
|
||||
if best:
|
||||
return (best[1], best[2], best[3])
|
||||
return None
|
||||
|
||||
def harmonize(self) -> list[Chord]:
|
||||
"""Build diatonic triads on every scale degree.
|
||||
|
||||
Returns a list of Chords — one triad for each degree of the
|
||||
scale. In a major scale this produces: I, ii, iii, IV, V, vi, vii°.
|
||||
|
||||
Example::
|
||||
|
||||
>>> [c.identify() for c in TonedScale(tonic="C4")["major"].harmonize()]
|
||||
['C major', 'D minor', 'E minor', 'F major', 'G major', 'A minor', 'B diminished']
|
||||
"""
|
||||
unique = len(self.tones) - 1
|
||||
return [self.triad(i) for i in range(unique)]
|
||||
|
||||
def degree(self, item: Union[str, int, slice], major: Optional[bool] = None, minor: bool = False) -> Optional[Union[Tone, tuple[Tone, ...]]]:
|
||||
def degree(self, item, major=None, minor=False):
|
||||
# TODO: cleanup degrees.
|
||||
|
||||
# Ensure that both major and minor aren't passed.
|
||||
@@ -268,301 +115,43 @@ class Scale:
|
||||
if isinstance(item, int) or isinstance(item, slice):
|
||||
return self.tones[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.
|
||||
"""
|
||||
def __getitem__(self, item):
|
||||
result = self.degree(item)
|
||||
if result is None:
|
||||
raise KeyError(item)
|
||||
return result
|
||||
|
||||
|
||||
PROGRESSIONS = {
|
||||
# Rock / Pop / Folk
|
||||
"I-IV-V-I": ("I", "IV", "V", "I"),
|
||||
"I-V-vi-IV": ("I", "V", "vi", "IV"),
|
||||
"I-vi-IV-V": ("I", "vi", "IV", "V"),
|
||||
"I-IV-vi-V": ("I", "IV", "vi", "V"),
|
||||
"vi-IV-I-V": ("vi", "IV", "I", "V"),
|
||||
# Blues
|
||||
"12-bar blues": ("I", "I", "I", "I", "IV", "IV", "I", "I", "V", "IV", "I", "V"),
|
||||
# Jazz
|
||||
"ii-V-I": ("ii", "V7", "I"),
|
||||
"I-vi-ii-V": ("I", "vi", "ii", "V"), # rhythm changes A section
|
||||
"iii-vi-ii-V": ("iii", "vi", "ii", "V"), # jazz turnaround
|
||||
# Classical / Film
|
||||
"i-bVI-bIII-bVII": ("i", "VI", "III", "VII"),
|
||||
"Pachelbel": ("I", "V", "vi", "iii", "IV", "I", "IV", "V"),
|
||||
# Flamenco / Spanish
|
||||
"Andalusian": ("i", "VII", "VI", "V"),
|
||||
# Modal
|
||||
"Dorian vamp": ("i", "IV"),
|
||||
"Mixolydian vamp": ("I", "VII"),
|
||||
}
|
||||
"""Common chord progressions as Roman numeral tuples.
|
||||
|
||||
Use with :meth:`Scale.progression` or :meth:`Key.progression`::
|
||||
|
||||
Key("C", "major").progression(*PROGRESSIONS["I-V-vi-IV"])
|
||||
"""
|
||||
|
||||
|
||||
class Key:
|
||||
"""A musical key — a convenient entry point for scales and harmony.
|
||||
|
||||
A Key represents a tonic note and a mode. It provides quick access
|
||||
to the scale, diatonic chords, and common progressions.
|
||||
|
||||
Example::
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> key.scale.note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
>>> key.chords
|
||||
['C major', 'D minor', 'E minor', 'F major', ...]
|
||||
>>> key.progression("I", "V", "vi", "IV")
|
||||
[<Chord (C,E,G)>, <Chord (G,B,D)>, ...]
|
||||
"""
|
||||
|
||||
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):
|
||||
system = SYSTEMS[system]
|
||||
self.tonic_name = tonic
|
||||
self.mode = mode
|
||||
self._system = system
|
||||
self._toned_scale = TonedScale(tonic=f"{tonic}4", system=system)
|
||||
self._scale = self._toned_scale[mode]
|
||||
|
||||
@classmethod
|
||||
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
|
||||
whose scale contains the most of the given notes.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key.detect("C", "D", "E", "F", "G", "A", "B")
|
||||
<Key C major>
|
||||
>>> Key.detect("A", "B", "C", "D", "E", "F", "G")
|
||||
<Key C major>
|
||||
>>> Key.detect("A", "C", "E")
|
||||
<Key C major>
|
||||
|
||||
Returns:
|
||||
The best-matching Key, or None if no notes given.
|
||||
"""
|
||||
if not note_names:
|
||||
return None
|
||||
|
||||
notes = set(note_names)
|
||||
best_key = None
|
||||
best_score = (-1, 0)
|
||||
|
||||
chromatic = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
||||
for tonic in chromatic:
|
||||
for mode in ("major", "minor"):
|
||||
try:
|
||||
k = cls(tonic, mode)
|
||||
scale_notes = set(k.note_names)
|
||||
match = len(notes & scale_notes)
|
||||
# Tiebreak: prefer major over minor
|
||||
score = (match, 1 if mode == "major" else 0)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_key = k
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
|
||||
return best_key
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Key {self.tonic_name} {self.mode}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.tonic_name} {self.mode}"
|
||||
|
||||
@property
|
||||
def scale(self) -> Scale:
|
||||
"""The scale for this key."""
|
||||
return self._scale
|
||||
|
||||
@property
|
||||
def note_names(self) -> list[str]:
|
||||
"""Note names in this key's scale."""
|
||||
return self._scale.note_names
|
||||
|
||||
@property
|
||||
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) -> 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: int) -> Chord:
|
||||
"""Build a diatonic triad on the given degree (0-indexed)."""
|
||||
return self._scale.triad(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: str) -> list[Chord]:
|
||||
"""Build a chord progression from Roman numerals.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("G", "major").progression("I", "IV", "V7", "I")
|
||||
"""
|
||||
return self._scale.progression(*numerals)
|
||||
|
||||
def nashville(self, *numbers: Union[int, str]) -> list[Chord]:
|
||||
"""Build a chord progression using Nashville numbers.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("G", "major").nashville(1, 4, 5, 1)
|
||||
"""
|
||||
return self._scale.nashville(*numbers)
|
||||
|
||||
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
|
||||
degree. For example, in C major, V/V is D major (the V chord
|
||||
of G). Secondary dominants create momentary tonicizations
|
||||
that add color and forward motion.
|
||||
|
||||
Common secondary dominants:
|
||||
|
||||
- V/V (e.g. D7 in C major) — approaches the dominant
|
||||
- V/ii (e.g. A7 in C major) — approaches the supertonic
|
||||
- V/vi (e.g. E7 in C major) — approaches the relative minor
|
||||
|
||||
Args:
|
||||
degree: Scale degree to target (1-indexed). ``5`` means
|
||||
"build the V of the 5th degree."
|
||||
|
||||
Returns:
|
||||
A dominant 7th Chord that resolves to the given degree.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("C", "major").secondary_dominant(5) # V/V = D7
|
||||
<Chord D dominant 7th>
|
||||
"""
|
||||
target = self._scale.tones[degree - 1]
|
||||
# Build a dominant 7th a perfect 5th above the target
|
||||
from .chords import Chord
|
||||
root = target.add(7)
|
||||
return Chord(tones=[root, root.add(4), root.add(7), root.add(10)])
|
||||
|
||||
@classmethod
|
||||
def all_keys(cls) -> list[Key]:
|
||||
"""Return all 24 major and minor keys.
|
||||
|
||||
Returns:
|
||||
A list of Key objects for all 12 major and 12 minor keys.
|
||||
|
||||
Example::
|
||||
|
||||
>>> for k in Key.all_keys():
|
||||
... print(k)
|
||||
"""
|
||||
chromatic = ["C", "C#", "D", "D#", "E", "F",
|
||||
"F#", "G", "G#", "A", "A#", "B"]
|
||||
keys = []
|
||||
for tonic in chromatic:
|
||||
keys.append(cls(tonic, "major"))
|
||||
keys.append(cls(tonic, "minor"))
|
||||
return keys
|
||||
|
||||
@property
|
||||
def relative(self) -> Optional[Key]:
|
||||
"""The relative major or minor key.
|
||||
|
||||
If this is a major key, returns the relative minor (vi).
|
||||
If this is a minor key, returns the relative major (bIII).
|
||||
"""
|
||||
if self.mode == "major":
|
||||
# Relative minor starts on the 6th degree
|
||||
minor_tonic = self._scale.tones[5].name
|
||||
return Key(minor_tonic, "minor")
|
||||
elif self.mode in ("minor", "aeolian"):
|
||||
# Relative major starts on the 3rd degree
|
||||
major_tonic = self._scale.tones[2].name
|
||||
return Key(major_tonic, "major")
|
||||
return None
|
||||
|
||||
@property
|
||||
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")
|
||||
elif self.mode in ("minor", "aeolian"):
|
||||
return Key(self.tonic_name, "major")
|
||||
return None
|
||||
|
||||
|
||||
class TonedScale:
|
||||
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]
|
||||
def __init__(self, *, system=SYSTEMS["western"], tonic):
|
||||
self.system = system
|
||||
|
||||
if not isinstance(tonic, Tone):
|
||||
tonic = Tone.from_string(tonic, system=self.system)
|
||||
|
||||
self.tonic = tonic
|
||||
self._cached_scales: Optional[dict[str, Scale]] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
def __repr__(self):
|
||||
return f"<TonedScale system={self.system!r} tonic={self.tonic}>"
|
||||
|
||||
def __getitem__(self, scale: str) -> Scale:
|
||||
"""Retrieve a scale by name.
|
||||
|
||||
Raises:
|
||||
KeyError: If the named scale is not found in this system.
|
||||
"""
|
||||
def __getitem__(self, scale):
|
||||
result = self.get(scale)
|
||||
if result is None:
|
||||
raise KeyError(scale)
|
||||
return result
|
||||
|
||||
def get(self, scale: str) -> Optional[Scale]:
|
||||
"""Look up a scale by name, returning ``None`` if not found."""
|
||||
def get(self, scale):
|
||||
try:
|
||||
return self._scales[scale]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def scales(self) -> tuple[str, ...]:
|
||||
"""Tuple of all available scale names in this system."""
|
||||
def scales(self):
|
||||
return tuple(self._scales.keys())
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
def _scales(self):
|
||||
scales = {}
|
||||
|
||||
for scale_type in self.system.scales:
|
||||
@@ -580,5 +169,4 @@ class TonedScale:
|
||||
|
||||
scales[scale] = Scale(tones=tuple(working_scale))
|
||||
|
||||
self._cached_scales = scales
|
||||
return scales
|
||||
|
||||
+3
-21
@@ -1,8 +1,4 @@
|
||||
from ._statics import (
|
||||
TEMPERAMENTS, TONES, DEGREES, SCALES,
|
||||
INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES,
|
||||
BLUES_SCALES, GAMELAN_SCALES, SYSTEMS,
|
||||
)
|
||||
from ._statics import TEMPERAMENTS, TONES, DEGREES, SCALES, SYSTEMS
|
||||
|
||||
|
||||
class System:
|
||||
@@ -59,7 +55,6 @@ class System:
|
||||
*,
|
||||
tones=7,
|
||||
semitones=12,
|
||||
intervals=None,
|
||||
major=False,
|
||||
minor=False,
|
||||
hemitonic=False, # Contains semitones.
|
||||
@@ -68,13 +63,7 @@ class System:
|
||||
offset=None,
|
||||
):
|
||||
"""Generates the primary scale for a given number of semitones/tones."""
|
||||
|
||||
# Direct interval pattern — bypass generation logic.
|
||||
if intervals is not None:
|
||||
scale = list(intervals)
|
||||
if offset:
|
||||
scale = scale[offset:] + scale[:offset]
|
||||
return {"intervals": scale, "hemitonic": 1 in scale, "meta": {}}
|
||||
# TODO: Support minor, support harmonic, support melodic.
|
||||
|
||||
# Sanity check.
|
||||
if major and minor:
|
||||
@@ -130,11 +119,4 @@ class System:
|
||||
def __repr__(self):
|
||||
return f"<System semitones={self.semitones!r}>"
|
||||
|
||||
SYSTEMS = {
|
||||
"western": System(tone_names=TONES["western"], degrees=DEGREES["western"]),
|
||||
"indian": System(tone_names=TONES["indian"], degrees=DEGREES["indian"], scales=INDIAN_SCALES[12]),
|
||||
"arabic": System(tone_names=TONES["arabic"], degrees=DEGREES["arabic"], scales=ARABIC_SCALES[12]),
|
||||
"japanese": System(tone_names=TONES["japanese"], degrees=DEGREES["japanese"], scales=JAPANESE_SCALES[12]),
|
||||
"blues": System(tone_names=TONES["blues"], degrees=DEGREES["blues"], scales=BLUES_SCALES[12]),
|
||||
"gamelan": System(tone_names=TONES["gamelan"], degrees=DEGREES["gamelan"], scales=GAMELAN_SCALES[12]),
|
||||
}
|
||||
SYSTEMS = {"western": System(tone_names=TONES["western"], degrees=DEGREES["western"])}
|
||||
|
||||
+32
-357
@@ -1,47 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
from ._statics import REFERENCE_A, TEMPERAMENTS
|
||||
|
||||
|
||||
class Interval:
|
||||
"""Named constants for common musical intervals (in semitones)."""
|
||||
UNISON = 0
|
||||
MINOR_SECOND = 1
|
||||
MAJOR_SECOND = 2
|
||||
MINOR_THIRD = 3
|
||||
MAJOR_THIRD = 4
|
||||
PERFECT_FOURTH = 5
|
||||
TRITONE = 6
|
||||
PERFECT_FIFTH = 7
|
||||
MINOR_SIXTH = 8
|
||||
MAJOR_SIXTH = 9
|
||||
MINOR_SEVENTH = 10
|
||||
MAJOR_SEVENTH = 11
|
||||
OCTAVE = 12
|
||||
|
||||
|
||||
class Tone:
|
||||
|
||||
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.
|
||||
"""
|
||||
def __init__(self, name, *, alt_names=None, octave=None, system="western"):
|
||||
if alt_names is None:
|
||||
alt_names = []
|
||||
|
||||
@@ -59,7 +21,6 @@ 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
|
||||
@@ -69,16 +30,11 @@ class Tone:
|
||||
self._system = system
|
||||
|
||||
@property
|
||||
def exists(self) -> bool:
|
||||
"""True if this tone's name is found in the associated system."""
|
||||
def exists(self):
|
||||
return self.name in self.system.tones
|
||||
|
||||
@property
|
||||
def system(self) -> object:
|
||||
"""The ``ToneSystem`` associated with this tone.
|
||||
|
||||
Lazily resolved from ``system_name`` on first access and cached.
|
||||
"""
|
||||
def system(self):
|
||||
from .systems import SYSTEMS
|
||||
|
||||
if self._system:
|
||||
@@ -89,65 +45,25 @@ class Tone:
|
||||
return self.system
|
||||
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
"""The tone name with octave appended, e.g. ``'C4'`` or ``'C'``."""
|
||||
if self.octave is not None:
|
||||
def full_name(self):
|
||||
if self.octave:
|
||||
return f"{self.name}{self.octave}"
|
||||
else:
|
||||
return self.name
|
||||
|
||||
def names(self) -> list[str]:
|
||||
"""Return a list containing the primary name and all alternate names."""
|
||||
def names(self):
|
||||
return [self.name] + self.alt_names
|
||||
|
||||
@property
|
||||
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) -> bool:
|
||||
"""True if this tone has a sharp (#)."""
|
||||
return "#" in self.name
|
||||
|
||||
@property
|
||||
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) -> Optional[str]:
|
||||
"""The enharmonic equivalent of this tone, or None if there isn't one.
|
||||
|
||||
Returns the alternate spelling: C# → Db, Db → C#, etc.
|
||||
Natural notes (C, D, E, F, G, A, B) have no enharmonic.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Tone.from_string("C#4").enharmonic
|
||||
'Db'
|
||||
"""
|
||||
if self.alt_names:
|
||||
return self.alt_names[0] if isinstance(self.alt_names, (list, tuple)) else self.alt_names
|
||||
# Check the system for alt names
|
||||
try:
|
||||
for tone in self.system.tones:
|
||||
if tone.name == self.name and tone.alt_names:
|
||||
return tone.alt_names[0]
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
def __repr__(self):
|
||||
return f"<Tone {self.full_name}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
def __str__(self):
|
||||
return self.full_name
|
||||
|
||||
def __add__(self, interval: int) -> Tone:
|
||||
def __add__(self, interval):
|
||||
return self.add(interval)
|
||||
|
||||
def __sub__(self, other: Union[int, Tone]) -> Union[Tone, int]:
|
||||
def __sub__(self, other):
|
||||
# Tone - int: subtract semitones
|
||||
if isinstance(other, int):
|
||||
return self.subtract(other)
|
||||
@@ -163,27 +79,27 @@ class Tone:
|
||||
return self_from_c0 - other_from_c0
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other: Tone) -> bool:
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() < other.pitch()
|
||||
|
||||
def __le__(self, other: Tone) -> bool:
|
||||
def __le__(self, other):
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() <= other.pitch()
|
||||
|
||||
def __gt__(self, other: Tone) -> bool:
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() > other.pitch()
|
||||
|
||||
def __ge__(self, other: Tone) -> bool:
|
||||
def __ge__(self, other):
|
||||
if not isinstance(other, Tone):
|
||||
return NotImplemented
|
||||
return self.pitch() >= other.pitch()
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
def __eq__(self, other):
|
||||
|
||||
# Comparing string literals.
|
||||
if isinstance(other, str):
|
||||
@@ -198,20 +114,11 @@ class Tone:
|
||||
|
||||
return False
|
||||
|
||||
def __hash__(self) -> int:
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.octave))
|
||||
|
||||
@classmethod
|
||||
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.
|
||||
"""
|
||||
def from_string(klass, s, system=None):
|
||||
try:
|
||||
octave = int("".join([c for c in filter(str.isdigit, s)]))
|
||||
except ValueError:
|
||||
@@ -225,16 +132,7 @@ class Tone:
|
||||
return klass(name=tone, octave=octave)
|
||||
|
||||
@classmethod
|
||||
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.
|
||||
"""
|
||||
def from_tuple(klass, t):
|
||||
if len(t) == 1:
|
||||
return klass.from_string(s=t[0])
|
||||
else:
|
||||
@@ -243,88 +141,18 @@ class Tone:
|
||||
return tone
|
||||
|
||||
@classmethod
|
||||
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).
|
||||
|
||||
Example::
|
||||
|
||||
>>> Tone.from_frequency(440)
|
||||
<Tone A4>
|
||||
>>> Tone.from_frequency(261.63)
|
||||
<Tone C4>
|
||||
"""
|
||||
import math
|
||||
if hz <= 0:
|
||||
raise ValueError("Frequency must be positive")
|
||||
# Semitones from A4
|
||||
semitones_from_a4 = 12 * math.log2(hz / REFERENCE_A)
|
||||
semitones = round(semitones_from_a4)
|
||||
# A4 is index 0 in the Western system, octave 4
|
||||
# Convert to absolute position from C0
|
||||
c_index = 3
|
||||
a4_from_c0 = ((0 - c_index) % 12) + (4 * 12) # = 57
|
||||
abs_pos = a4_from_c0 + semitones
|
||||
octave = abs_pos // 12
|
||||
relative = abs_pos % 12
|
||||
index = (relative + c_index) % 12
|
||||
if isinstance(system, str):
|
||||
from .systems import SYSTEMS
|
||||
system = SYSTEMS[system]
|
||||
return klass.from_index(index, octave=octave, system=system)
|
||||
|
||||
@classmethod
|
||||
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).
|
||||
|
||||
Example::
|
||||
|
||||
>>> Tone.from_midi(60)
|
||||
<Tone C4>
|
||||
>>> Tone.from_midi(69)
|
||||
<Tone A4>
|
||||
"""
|
||||
c_index = 3
|
||||
adjusted = note_number - 12 # MIDI C0=12
|
||||
octave = adjusted // 12
|
||||
relative = adjusted % 12
|
||||
index = (relative + c_index) % 12
|
||||
if isinstance(system, str):
|
||||
from .systems import SYSTEMS
|
||||
system = SYSTEMS[system]
|
||||
return klass.from_index(index, octave=octave, system=system)
|
||||
|
||||
@classmethod
|
||||
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.
|
||||
"""
|
||||
def from_index(klass, i, *, octave, system):
|
||||
tone = system.tones[i].name
|
||||
return klass(name=tone, octave=octave, system=system)
|
||||
|
||||
@property
|
||||
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.
|
||||
"""
|
||||
def _index(self):
|
||||
try:
|
||||
return self.system.tones.index(self.name)
|
||||
except AttributeError:
|
||||
raise ValueError("Tone index cannot be referenced without a system!")
|
||||
|
||||
def _math(self, interval: int) -> tuple[int, int]:
|
||||
def _math(self, interval):
|
||||
"""Returns (new index, new octave).
|
||||
|
||||
Octave boundaries follow scientific pitch notation, where the
|
||||
@@ -354,186 +182,33 @@ class Tone:
|
||||
|
||||
return (new_index, new_octave)
|
||||
|
||||
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.
|
||||
"""
|
||||
def add(self, interval):
|
||||
index, octave = self._math(interval)
|
||||
return self.from_index(index, octave=octave, system=self.system)
|
||||
|
||||
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.
|
||||
"""
|
||||
def subtract(self, interval):
|
||||
return self.add((-1 * interval))
|
||||
|
||||
_INTERVAL_NAMES = {
|
||||
0: "unison", 1: "minor 2nd", 2: "major 2nd", 3: "minor 3rd",
|
||||
4: "major 3rd", 5: "perfect 4th", 6: "tritone", 7: "perfect 5th",
|
||||
8: "minor 6th", 9: "major 6th", 10: "minor 7th", 11: "major 7th",
|
||||
12: "octave",
|
||||
}
|
||||
|
||||
def interval_to(self, other: Tone) -> str:
|
||||
"""Name the interval between this tone and another.
|
||||
|
||||
Returns a string like ``"perfect 5th"``, ``"major 3rd"``, or
|
||||
``"octave"``. For intervals larger than an octave, returns
|
||||
the compound form (e.g. ``"minor 2nd + 1 octave"``).
|
||||
|
||||
Example::
|
||||
|
||||
>>> C4.interval_to(G4)
|
||||
'perfect 5th'
|
||||
>>> C4.interval_to(C5)
|
||||
'octave'
|
||||
"""
|
||||
semitones = abs(self - other)
|
||||
octaves = semitones // 12
|
||||
remainder = semitones % 12
|
||||
name = self._INTERVAL_NAMES.get(remainder, f"{remainder} semitones")
|
||||
if octaves == 0:
|
||||
return name
|
||||
if remainder == 0:
|
||||
if octaves == 1:
|
||||
return "octave"
|
||||
return f"{octaves} octaves"
|
||||
if octaves == 1:
|
||||
return f"{name} + 1 octave"
|
||||
return f"{name} + {octaves} octaves"
|
||||
|
||||
@property
|
||||
def midi(self) -> Optional[int]:
|
||||
"""MIDI note number (C4 = 60, A4 = 69).
|
||||
|
||||
The MIDI standard assigns integer note numbers from 0–127.
|
||||
Middle C (C4) is 60, and each semitone increments by 1.
|
||||
|
||||
Returns:
|
||||
int: the MIDI note number, or None if no octave is set.
|
||||
"""
|
||||
if self.octave is None:
|
||||
return None
|
||||
c_index = 3
|
||||
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: int) -> Tone:
|
||||
"""Return a new Tone transposed by the given number of semitones.
|
||||
|
||||
Alias for ``tone + semitones`` / ``tone - semitones``. Positive
|
||||
values transpose up, negative values transpose down.
|
||||
"""
|
||||
return self.add(semitones)
|
||||
|
||||
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
|
||||
steps you return to the starting tone. The circle of fifths
|
||||
is the backbone of Western harmony — it determines key
|
||||
signatures, chord relationships, and modulation paths.
|
||||
|
||||
Clockwise = add sharps: C → G → D → A → E → B → F# → ...
|
||||
Counter-clockwise = add flats (see ``circle_of_fourths``).
|
||||
|
||||
Returns:
|
||||
A list of 12 Tones.
|
||||
"""
|
||||
tones: list[Tone] = []
|
||||
t = self
|
||||
for _ in range(12):
|
||||
tones.append(t)
|
||||
t = t.add(7)
|
||||
return tones
|
||||
|
||||
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
|
||||
reverse direction of the circle of fifths.
|
||||
|
||||
Clockwise = add flats: C → F → Bb → Eb → Ab → ...
|
||||
|
||||
Returns:
|
||||
A list of 12 Tones.
|
||||
"""
|
||||
tones: list[Tone] = []
|
||||
t = self
|
||||
for _ in range(12):
|
||||
tones.append(t)
|
||||
t = t.add(5)
|
||||
return tones
|
||||
|
||||
@property
|
||||
def frequency(self) -> float:
|
||||
"""The frequency of this tone in Hz (equal temperament, A4=440).
|
||||
|
||||
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.
|
||||
When a string or air column vibrates, it produces not just the
|
||||
fundamental frequency but also integer multiples: 2f, 3f, 4f...
|
||||
|
||||
The intervals between consecutive harmonics form the basis of
|
||||
Western harmony::
|
||||
|
||||
Harmonic Ratio Interval from fundamental
|
||||
1 1:1 Unison (the fundamental)
|
||||
2 2:1 Octave
|
||||
3 3:1 Octave + perfect 5th
|
||||
4 4:1 Two octaves
|
||||
5 5:1 Two octaves + major 3rd
|
||||
6 6:1 Two octaves + perfect 5th
|
||||
7 7:1 Two octaves + minor 7th (slightly flat)
|
||||
8 8:1 Three octaves
|
||||
|
||||
The reason a perfect fifth sounds consonant is that the 3rd
|
||||
harmonic of the lower note aligns with the 2nd harmonic of the
|
||||
upper note (when the upper note is a fifth above). More shared
|
||||
harmonics = more consonance.
|
||||
|
||||
Args:
|
||||
n: Number of harmonics to return (default 8).
|
||||
|
||||
Returns:
|
||||
List of frequencies in Hz.
|
||||
"""
|
||||
f = self.pitch()
|
||||
return [f * i for i in range(1, n + 1)]
|
||||
def frequency(self):
|
||||
"""The frequency of this tone in Hz (equal temperament, A4=440)."""
|
||||
return self.pitch()
|
||||
|
||||
def pitch(
|
||||
self,
|
||||
*,
|
||||
reference_pitch: float = REFERENCE_A,
|
||||
temperament: str = "equal",
|
||||
symbolic: bool = False,
|
||||
precision: Optional[int] = None,
|
||||
) -> float:
|
||||
reference_pitch=REFERENCE_A,
|
||||
temperament="equal",
|
||||
symbolic=False,
|
||||
precision=None,
|
||||
):
|
||||
try:
|
||||
tones = len(self.system.tones)
|
||||
except AttributeError:
|
||||
raise ValueError("Pitches can only be computed with an associated system!")
|
||||
|
||||
pitch_scale = TEMPERAMENTS[temperament](tones)
|
||||
octave = self.octave if self.octave is not None else 4
|
||||
octave = self.octave or 4
|
||||
|
||||
# C is at index 3; convert to semitones from C0 for both
|
||||
# this note and the reference A4.
|
||||
|
||||
+13
-1583
File diff suppressed because it is too large
Load Diff
@@ -2,38 +2,10 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version >= '3.11'",
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alabaster"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "babel"
|
||||
version = "2.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.2.25"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
@@ -116,111 +88,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@@ -230,31 +97,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.21.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.22.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
@@ -267,24 +109,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imagesize"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
@@ -294,103 +118,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpmath"
|
||||
version = "1.3.0"
|
||||
@@ -478,8 +205,7 @@ name = "numpy"
|
||||
version = "2.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version >= '3.11'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
|
||||
wheels = [
|
||||
@@ -612,8 +338,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytheory"
|
||||
version = "0.3.2"
|
||||
source = { editable = "." }
|
||||
version = "0.2.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "numeral" },
|
||||
{ name = "pytuning" },
|
||||
@@ -626,11 +352,6 @@ dependencies = [
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
@@ -642,7 +363,6 @@ requires-dist = [
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest" }]
|
||||
docs = [{ name = "sphinx" }]
|
||||
|
||||
[[package]]
|
||||
name = "pytuning"
|
||||
@@ -657,30 +377,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/59/e2c2fc91688f788587fb387ef6120c9a1ad3a8b88771fba9fc6a9c9a969d/PyTuning-0.7.3-py3-none-any.whl", hash = "sha256:db0b1231c012c1cf6a3c73aa7d791b4cff79a72f2ec6535f159c873fe302214b", size = 108174, upload-time = "2023-09-02T21:11:00.657Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roman-numerals"
|
||||
version = "4.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scipy"
|
||||
version = "1.15.3"
|
||||
@@ -745,8 +441,7 @@ name = "scipy"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version >= '3.11'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
@@ -815,15 +510,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snowballstemmer"
|
||||
version = "3.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sounddevice"
|
||||
version = "0.5.5"
|
||||
@@ -840,153 +526,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx"
|
||||
version = "8.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "alabaster", marker = "python_full_version < '3.11'" },
|
||||
{ name = "babel", marker = "python_full_version < '3.11'" },
|
||||
{ name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
|
||||
{ name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "imagesize", marker = "python_full_version < '3.11'" },
|
||||
{ name = "jinja2", marker = "python_full_version < '3.11'" },
|
||||
{ name = "packaging", marker = "python_full_version < '3.11'" },
|
||||
{ name = "pygments", marker = "python_full_version < '3.11'" },
|
||||
{ name = "requests", marker = "python_full_version < '3.11'" },
|
||||
{ name = "snowballstemmer", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" },
|
||||
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx"
|
||||
version = "9.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "alabaster", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "babel", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" },
|
||||
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "imagesize", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "jinja2", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "packaging", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "pygments", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "requests", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "roman-numerals", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "snowballstemmer", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx"
|
||||
version = "9.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "alabaster", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "babel", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" },
|
||||
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "imagesize", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "jinja2", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "packaging", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "pygments", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "requests", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "roman-numerals", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "snowballstemmer", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-applehelp"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-devhelp"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-htmlhelp"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-jsmath"
|
||||
version = "1.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-qthelp"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-serializinghtml"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.14.0"
|
||||
@@ -1061,12 +600,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user