mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e1d9e76bd | |||
| 447d03a2d2 | |||
| 7b82d70ad6 | |||
| 44f8b902e2 | |||
| 03eb61cd5d | |||
| eba299d406 | |||
| d11c930308 | |||
| 19663ed6c5 | |||
| f949ca5b45 | |||
| d9f847603a | |||
| ee41691728 |
+10
-1
@@ -10,7 +10,9 @@ sys.modules["sounddevice"] = MagicMock()
|
||||
project = "PyTheory"
|
||||
copyright = "2026, Kenneth Reitz"
|
||||
author = "Kenneth Reitz"
|
||||
release = "0.4.1"
|
||||
import pytheory
|
||||
release = pytheory.__version__
|
||||
version = pytheory.__version__
|
||||
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
@@ -38,7 +40,14 @@ html_theme_options = {
|
||||
"github_user": "kennethreitz",
|
||||
"github_repo": "pytheory",
|
||||
"github_banner": True,
|
||||
"github_button": True,
|
||||
"github_type": "star",
|
||||
"github_count": True,
|
||||
"description": "Music Theory for Humans",
|
||||
"extra_nav_links": {
|
||||
f"v{pytheory.__version__}": "https://pypi.org/project/pytheory/",
|
||||
},
|
||||
"show_powered_by": False,
|
||||
}
|
||||
html_static_path = ["_static"]
|
||||
html_extra_path = ["CNAME"]
|
||||
|
||||
@@ -389,6 +389,29 @@ gold standard — every voice moves by step whenever possible.
|
||||
print(f"{src} -> {dst} ({motion:+d} semitones)")
|
||||
# Each voice moves the minimum distance to reach the target chord
|
||||
|
||||
Tritone Substitution
|
||||
--------------------
|
||||
|
||||
In jazz harmony, any `dominant chord <https://en.wikipedia.org/wiki/Dominant_seventh_chord>`_
|
||||
can be replaced by the dominant chord a
|
||||
`tritone <https://en.wikipedia.org/wiki/Tritone_substitution>`_ (6
|
||||
semitones) away. This works because the two chords share the same
|
||||
tritone interval — the 3rd and 7th simply swap roles.
|
||||
|
||||
Common tritone subs: G7 <-> Db7, C7 <-> F#7, D7 <-> Ab7.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Chord
|
||||
|
||||
g7 = Chord.from_name("G7")
|
||||
sub = g7.tritone_sub()
|
||||
sub.identify() # 'C# dominant 7th' (enharmonic Db7)
|
||||
|
||||
# Both resolve to C — try them in a ii-V-I:
|
||||
# Dm7 → G7 → Cmaj7 (standard)
|
||||
# Dm7 → Db7 → Cmaj7 (with tritone sub — chromatic bass line!)
|
||||
|
||||
The Overtone Series
|
||||
-------------------
|
||||
|
||||
|
||||
+42
-29
@@ -55,6 +55,19 @@ strings, except between G and B which is a major 3rd (4 semitones).
|
||||
# Custom tuning with any notes
|
||||
Fretboard.guitar(("C4", "G3", "C3", "G2", "C2", "G1"))
|
||||
|
||||
**Capo** — a `capo <https://en.wikipedia.org/wiki/Capo>`_ raises all
|
||||
strings by a number of frets, letting you play open chord shapes in
|
||||
higher keys:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Capo on fret 2 — open G shape now sounds as A major
|
||||
fb = Fretboard.guitar(capo=2)
|
||||
|
||||
# Or apply a capo to an existing fretboard
|
||||
fb = Fretboard.guitar()
|
||||
fb_capo3 = fb.capo(3)
|
||||
|
||||
The Mandolin Family
|
||||
-------------------
|
||||
|
||||
@@ -172,46 +185,46 @@ on any instrument. It scores each possibility by:
|
||||
2. Preferring **ascending** fret patterns — easier hand position
|
||||
3. Minimizing the number of **fingers needed**
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Fretboard, CHARTS
|
||||
>>> from pytheory import Fretboard, CHARTS
|
||||
|
||||
fb = Fretboard.guitar()
|
||||
c = CHARTS["western"]["C"]
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> f = fb.chord("C")
|
||||
>>> f
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=0)
|
||||
|
||||
# Fingerings return a Fingering object with labeled strings
|
||||
f = c.fingering(fretboard=fb)
|
||||
print(f)
|
||||
# Fingering(e=0, B=1, G=0, D=2, A=3, E=0)
|
||||
>>> f['A']
|
||||
3
|
||||
>>> f[1]
|
||||
1
|
||||
|
||||
# Access by string name or index
|
||||
f['A'] # 3
|
||||
f[1] # 1 (B string)
|
||||
>>> f.identify()
|
||||
'C major'
|
||||
|
||||
# Identify the chord directly from a fingering
|
||||
f.identify() # 'C major'
|
||||
>>> chord = f.to_chord()
|
||||
>>> chord.identify()
|
||||
'C major'
|
||||
|
||||
# Convert to a Chord for further analysis
|
||||
chord = f.to_chord()
|
||||
chord.harmony # consonance score
|
||||
chord.intervals # [4, 3] — major triad
|
||||
>>> # All equally-scored fingerings via CHARTS
|
||||
>>> CHARTS["western"]["C"].fingering(fretboard=fb, multiple=True)
|
||||
[...]
|
||||
|
||||
# All equally-scored fingerings
|
||||
all_c = c.fingering(fretboard=fb, multiple=True)
|
||||
|
||||
# Muted strings appear as None
|
||||
f = CHARTS["western"]["F"]
|
||||
print(f.fingering(fretboard=fb))
|
||||
>>> # Muted strings appear as None
|
||||
>>> CHARTS["western"]["F"].fingering(fretboard=fb)
|
||||
...
|
||||
|
||||
You can also go from fret positions to chord identification:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
# "What chord am I playing?"
|
||||
fb = Fretboard.guitar()
|
||||
f = fb.fingering(0, 0, 0, 2, 2, 0)
|
||||
print(f) # Fingering(e=0, B=0, G=0, D=2, A=2, E=0)
|
||||
print(f.identify()) # E minor
|
||||
>>> # "What chord am I playing?"
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> f = fb.fingering(0, 0, 0, 2, 2, 0)
|
||||
>>> f
|
||||
Fingering(e=0, B=0, G=0, D=2, A=2, E=0)
|
||||
>>> f.identify()
|
||||
'E minor'
|
||||
|
||||
Reading Fingerings
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -77,3 +77,23 @@ 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.
|
||||
|
||||
Saving to WAV
|
||||
-------------
|
||||
|
||||
Render tones or chords to a WAV file instead of playing them live.
|
||||
This works even without speakers or PortAudio:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import save, Chord, Tone, Synth
|
||||
|
||||
# Save a single tone
|
||||
save(Tone.from_string("A4"), "a440.wav", t=1_000)
|
||||
|
||||
# Save a chord
|
||||
save(Chord.from_name("Am7"), "am7.wav", t=2_000)
|
||||
|
||||
# Choose waveform and temperament
|
||||
save(Chord.from_name("C"), "c_triangle.wav",
|
||||
synth=Synth.TRIANGLE, temperament="meantone", t=3_000)
|
||||
|
||||
+150
-26
@@ -8,46 +8,169 @@ Installation
|
||||
|
||||
$ pip install pytheory
|
||||
|
||||
Basic Usage
|
||||
-----------
|
||||
For audio playback, you'll also need `PortAudio <http://www.portaudio.com/>`_:
|
||||
|
||||
Create tones, build scales, and explore music theory:
|
||||
- macOS: ``brew install portaudio``
|
||||
- Ubuntu: ``apt install libportaudio2``
|
||||
- Windows: included with the ``sounddevice`` package
|
||||
|
||||
Tones
|
||||
-----
|
||||
|
||||
A :class:`~pytheory.tones.Tone` is a single musical note:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Tone, TonedScale, Fretboard, CHARTS
|
||||
from pytheory import Tone
|
||||
|
||||
# Create a tone — A4 is the tuning standard (440 Hz)
|
||||
# Create tones — sharps and flats both work
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
print(a4.frequency) # 440.0
|
||||
a4.frequency # 440.0 Hz — the tuning standard
|
||||
|
||||
# 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
|
||||
c4.midi # 60 — middle C
|
||||
|
||||
# Measure intervals between tones
|
||||
print(g4 - c4) # 7 (semitones — a perfect fifth)
|
||||
# From a frequency or MIDI number
|
||||
Tone.from_frequency(440) # <Tone A4>
|
||||
Tone.from_midi(60) # <Tone C4>
|
||||
|
||||
# Build a C major scale
|
||||
c_major = TonedScale(tonic="C4")["major"]
|
||||
print(c_major.note_names)
|
||||
# Tone arithmetic
|
||||
c4 + 4 # <Tone E4> — major third up
|
||||
c4 + 7 # <Tone G4> — perfect fifth up
|
||||
|
||||
# Interval between two tones
|
||||
g4 = c4 + 7
|
||||
g4 - c4 # 7 semitones
|
||||
c4.interval_to(g4) # 'perfect 5th'
|
||||
|
||||
# Enharmonics
|
||||
Tone.from_string("C#4", system="western").enharmonic # 'Db'
|
||||
|
||||
Scales
|
||||
------
|
||||
|
||||
Build scales in any key and mode:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
c = TonedScale(tonic="C4")
|
||||
|
||||
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)
|
||||
c["minor"].note_names
|
||||
# ['C', 'D', 'D#', 'F', 'G', 'G#', 'A#', 'C']
|
||||
|
||||
# Guitar chord fingerings — labeled with string names
|
||||
fb = Fretboard.guitar()
|
||||
fingering = CHARTS["western"]["Am"].fingering(fretboard=fb)
|
||||
print(fingering) # Fingering(e=0, B=1, G=2, D=2, A=0, E=0)
|
||||
c["dorian"].note_names
|
||||
# ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C']
|
||||
|
||||
# Identify a chord from fret positions
|
||||
f = fb.fingering(0, 1, 0, 2, 3, 0)
|
||||
print(f.identify()) # C major
|
||||
# Access scale degrees by name or numeral
|
||||
major = c["major"]
|
||||
major["tonic"] # C4
|
||||
major["dominant"] # G4
|
||||
major["V"] # G4
|
||||
|
||||
Keys and Chords
|
||||
---------------
|
||||
|
||||
The :class:`~pytheory.scales.Key` class ties everything together —
|
||||
scales, chords, and progressions:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Key
|
||||
|
||||
key = Key("G", "major")
|
||||
key.note_names # ['G', 'A', 'B', 'C', 'D', 'E', 'F#', 'G']
|
||||
|
||||
# All diatonic triads
|
||||
key.chords
|
||||
# ['G major', 'A minor', 'B minor', 'C major',
|
||||
# 'D major', 'E minor', 'F# diminished']
|
||||
|
||||
# Build progressions from Roman numerals
|
||||
chords = key.progression("I", "V", "vi", "IV")
|
||||
[c.identify() for c in chords]
|
||||
# ['G major', 'D major', 'E minor', 'C major']
|
||||
|
||||
# Detect the key from notes
|
||||
Key.detect("C", "E", "G", "A", "D") # <Key C major>
|
||||
|
||||
Build chords directly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Chord
|
||||
|
||||
Chord.from_tones("C", "E", "G") # <Chord C major>
|
||||
Chord.from_name("Am7") # <Chord A minor 7th>
|
||||
Chord.from_intervals("G", 4, 7, 10) # <Chord G dominant 7th>
|
||||
|
||||
# Identify any chord
|
||||
Chord.from_tones("Bb", "D", "F").identify() # 'Bb major'
|
||||
|
||||
# Analyze in a key
|
||||
Chord.from_name("G7").analyze("C") # 'V7'
|
||||
|
||||
Guitar Fingerings
|
||||
-----------------
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
|
||||
>>> fb.chord("C")
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=0)
|
||||
|
||||
>>> fb.chord("C")['A']
|
||||
3
|
||||
|
||||
>>> fb.fingering(0, 0, 0, 2, 2, 0).identify()
|
||||
'E minor'
|
||||
|
||||
>>> from pytheory import CHARTS
|
||||
>>> print(CHARTS["western"]["Am"].tab(fretboard=fb))
|
||||
Am
|
||||
E|--0--
|
||||
B|--1--
|
||||
G|--2--
|
||||
D|--2--
|
||||
A|--0--
|
||||
E|--0--
|
||||
|
||||
Audio Playback
|
||||
--------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Tone, Chord, play, save, Synth
|
||||
|
||||
# Play a tone
|
||||
play(Tone.from_string("A4"), t=1_000)
|
||||
|
||||
# Play a chord with a different waveform
|
||||
play(Chord.from_name("Am7"), synth=Synth.TRIANGLE, t=2_000)
|
||||
|
||||
# Save to a WAV file
|
||||
save(Chord.from_name("C"), "c_major.wav", t=2_000)
|
||||
|
||||
Command Line
|
||||
------------
|
||||
|
||||
PyTheory also works from the terminal::
|
||||
|
||||
$ pytheory tone A4
|
||||
$ pytheory chord C E G
|
||||
$ pytheory key G major
|
||||
$ pytheory scale C dorian
|
||||
$ pytheory fingering Am
|
||||
$ pytheory progression C major I V vi IV
|
||||
$ pytheory detect C E G A D
|
||||
$ pytheory play Am7 --synth triangle
|
||||
|
||||
What's Included
|
||||
---------------
|
||||
@@ -67,3 +190,4 @@ What's Included
|
||||
- **Fingering generation** for 25 instruments with labeled string names,
|
||||
including guitar (8 tunings), bass, ukulele, mandolin, and more
|
||||
- **Audio playback** with sine, sawtooth, and triangle wave synthesis
|
||||
- **WAV export** for saving rendered audio to disk
|
||||
|
||||
+58
-15
@@ -1,26 +1,69 @@
|
||||
PyTheory: Music Theory for Humans
|
||||
=================================
|
||||
|
||||
**PyTheory** is a Python library that makes exploring music theory approachable.
|
||||
Work with tones, scales, chords, and fretboards using a clean, Pythonic API.
|
||||
**PyTheory** is a Python library that makes exploring music theory
|
||||
approachable and fun. Work with tones, scales, chords, keys, and
|
||||
instruments using a clean, Pythonic API.
|
||||
|
||||
.. code-block:: python
|
||||
::
|
||||
|
||||
from pytheory import TonedScale, Fretboard, CHARTS
|
||||
$ pip install pytheory
|
||||
|
||||
# Build a C major scale
|
||||
c_major = TonedScale(tonic="C4")["major"]
|
||||
print(c_major.note_names)
|
||||
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
.. code-block:: pycon
|
||||
|
||||
# Build a triad from the scale
|
||||
chord = c_major.triad(0) # C major triad
|
||||
for tone in chord:
|
||||
print(f"{tone}: {tone.frequency:.1f} Hz")
|
||||
>>> from pytheory import Key, Chord, Tone, Fretboard
|
||||
|
||||
# Get guitar fingerings
|
||||
fb = Fretboard.guitar()
|
||||
print(CHARTS["western"]["C"].fingering(fretboard=fb))
|
||||
>>> key = Key("C", "major")
|
||||
>>> key.chords
|
||||
['C major', 'D minor', 'E minor', 'F major',
|
||||
'G major', 'A minor', 'B diminished']
|
||||
|
||||
>>> [c.identify() for c in key.progression("I", "V", "vi", "IV")]
|
||||
['C major', 'G major', 'A minor', 'F major']
|
||||
|
||||
>>> Chord.from_tones("Bb", "D", "F").identify()
|
||||
'Bb major'
|
||||
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> c4.interval_to(c4 + 7)
|
||||
'perfect 5th'
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("G")
|
||||
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
|
||||
|
||||
It also works from the command line::
|
||||
|
||||
$ pytheory key G major
|
||||
Key: G major
|
||||
Signature: 1 sharps, 0 flats (F#)
|
||||
Scale: G A B C D E F# G
|
||||
...
|
||||
|
||||
$ pytheory chord C E G
|
||||
Chord: C major
|
||||
Tones: C4 E4 G4
|
||||
Intervals: [4, 3]
|
||||
...
|
||||
|
||||
$ pytheory play Am7 --synth triangle
|
||||
Playing: A minor 7th (A4 C4 E4 G4)
|
||||
Synth: triangle
|
||||
|
||||
Highlights
|
||||
----------
|
||||
|
||||
- **Tones**: frequencies, MIDI, intervals, transposition, circle of fifths,
|
||||
overtone series, 3 temperaments (equal, Pythagorean, meantone)
|
||||
- **Scales**: 40+ scales across 6 musical systems — Western, Indian,
|
||||
Arabic, Japanese, Blues, Javanese Gamelan
|
||||
- **Chords**: 17 chord types identified automatically, Roman numeral
|
||||
analysis, tension scoring, voice leading, consonance/dissonance
|
||||
- **Keys**: key detection, signatures, progressions (Roman numerals and
|
||||
Nashville numbers), borrowed chords, secondary dominants
|
||||
- **Instruments**: 25 presets (guitar, bass, ukulele, mandolin, violin,
|
||||
banjo, oud, sitar, erhu, and more) with fingering generation
|
||||
- **Audio**: sine, sawtooth, and triangle wave playback + WAV export
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
@@ -48,8 +48,8 @@ def explore_key(tonic, mode="major"):
|
||||
borrowed = key.borrowed_chords
|
||||
if borrowed:
|
||||
print(f" Borrowed from {key.parallel}:")
|
||||
for chord in borrowed[:4]:
|
||||
print(f" {chord.identify()}")
|
||||
for name in borrowed[:4]:
|
||||
print(f" {name}")
|
||||
print()
|
||||
|
||||
|
||||
|
||||
@@ -28,19 +28,22 @@ systems = [
|
||||
]),
|
||||
("japanese", "C4", [
|
||||
("hirajoshi", "Haunting pentatonic — koto music"),
|
||||
("miyako-bushi", "Urban folk — shamisen music"),
|
||||
("yo", "Bright pentatonic — folk songs, festival music"),
|
||||
("in", "Dark pentatonic — court music, Buddhist chant"),
|
||||
("ritsu", "Elegant pentatonic — gagaku court music"),
|
||||
("yo", "Bright pentatonic — folk songs, festival music"),
|
||||
("iwato", "Sparse, mysterious — zen atmosphere"),
|
||||
("kumoi", "Gentle pentatonic — lyrical, nostalgic"),
|
||||
("ritsu", "Elegant heptatonic — gagaku court music"),
|
||||
]),
|
||||
("blues", "C4", [
|
||||
("blues", "The 6-note blues scale with the 'blue note'"),
|
||||
("minor pentatonic", "The backbone of rock guitar solos"),
|
||||
("major pentatonic", "Bright, open — country, folk, pop"),
|
||||
]),
|
||||
("gamelan", "C4", [
|
||||
("gamelan", "nem4", [
|
||||
("slendro", "5-note near-equal division — metallic, shimmering"),
|
||||
("pelog", "7-note unequal — mysterious, otherworldly"),
|
||||
("pelog nem", "Pelog mode on nem — the most common mode"),
|
||||
("pelog barang", "Pelog mode on barang — bright, festive"),
|
||||
]),
|
||||
]
|
||||
|
||||
@@ -58,7 +61,7 @@ for system_name, tonic, scales in systems:
|
||||
print(f" {scale_name:20s} {notes}")
|
||||
print(f" {'':20s} {description}")
|
||||
print()
|
||||
except (KeyError, IndexError):
|
||||
except (KeyError, IndexError, ValueError):
|
||||
print(f" {scale_name:20s} (not available)")
|
||||
print()
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.6.1"
|
||||
__version__ = "0.7.0"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS
|
||||
|
||||
@@ -1251,6 +1251,25 @@ class Fretboard:
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def chord(self, name: str, *, system: str = "western") -> "Fingering":
|
||||
"""Look up a chord by name and return its best fingering.
|
||||
|
||||
Args:
|
||||
name: Chord name like ``"G"``, ``"Am7"``, ``"Bb"``, ``"Dm"``.
|
||||
system: Tonal system to use (default ``"western"``).
|
||||
|
||||
Returns:
|
||||
A :class:`Fingering` for that chord on this fretboard.
|
||||
|
||||
Example::
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("G")
|
||||
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
|
||||
"""
|
||||
from .charts import CHARTS
|
||||
return CHARTS[system][name].fingering(fretboard=self)
|
||||
|
||||
def fingering(self, *positions: int) -> "Fingering":
|
||||
"""Apply fret positions to each string, returning a Fingering.
|
||||
|
||||
|
||||
@@ -3832,6 +3832,7 @@ def test_cli_main_no_args(capsys):
|
||||
|
||||
# ── Play module tests ─────────────────────────────────────────────────────────
|
||||
|
||||
@needs_portaudio
|
||||
def test_play_render():
|
||||
"""_render produces a numpy array of the right length."""
|
||||
from pytheory.play import _render, Synth, SAMPLE_RATE
|
||||
@@ -3841,6 +3842,7 @@ def test_play_render():
|
||||
assert len(samples) == expected
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_play_render_chord():
|
||||
from pytheory.play import _render, Synth
|
||||
chord = Chord.from_tones("C", "E", "G")
|
||||
@@ -3848,6 +3850,7 @@ def test_play_render_chord():
|
||||
assert len(samples) > 0
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_play_render_all_synths():
|
||||
from pytheory.play import _render, Synth
|
||||
tone = Tone.from_string("C4", system="western")
|
||||
@@ -3856,6 +3859,7 @@ def test_play_render_all_synths():
|
||||
assert len(samples) > 0
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_play_save(tmp_path):
|
||||
"""save() writes a valid WAV file."""
|
||||
from pytheory.play import save, Synth
|
||||
@@ -3866,6 +3870,7 @@ def test_play_save(tmp_path):
|
||||
assert path.stat().st_size > 44 # WAV header is 44 bytes
|
||||
|
||||
|
||||
@needs_portaudio
|
||||
def test_play_save_chord(tmp_path):
|
||||
from pytheory.play import save
|
||||
path = tmp_path / "chord.wav"
|
||||
|
||||
Reference in New Issue
Block a user