mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 14:50:18 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ed90c72d6 | |||
| 41e8404624 | |||
| 6c83dbe5aa | |||
| b3f3e985b4 | |||
| c1925af69d | |||
| 7883c978f7 | |||
| 36d558573c | |||
| 1e2f09e2ab | |||
| 9404afc1f3 | |||
| 72aa097552 | |||
| 5ebf0bdd97 | |||
| 1d897c6609 |
@@ -7,3 +7,4 @@ t2.py
|
||||
__pycache__
|
||||
pytheory.egg-info
|
||||
docs/_build
|
||||
.claude/worktrees/
|
||||
|
||||
@@ -2,6 +2,74 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.43.1
|
||||
|
||||
- **Fix `Fretboard.scale_diagram()` enharmonic matching.** Scale notes
|
||||
spelled with flats (e.g. the `Eb` blue note in the blues scale) were
|
||||
silently omitted from the diagram, because the fretboard spells that
|
||||
pitch as `D#`. Notes are now matched enharmonically (by pitch) and
|
||||
displayed using the scale's own spelling.
|
||||
|
||||
## 0.43.0
|
||||
|
||||
- **BREAKING — fingerings now read low-to-high by default.** `Fretboard`
|
||||
string lists and `Fingering` positions/string-names now run from the
|
||||
**lowest-pitched string first** (e.g. standard guitar reads `E A D G B E`),
|
||||
matching how chord diagrams and tablature are conventionally written.
|
||||
Previously they ran high-to-low (`E B G D A E`). This affects
|
||||
`Fretboard.tones`, iteration over a fretboard, `repr`, `chord()`, `tab()`,
|
||||
`chart()`, and `fingering()` output.
|
||||
|
||||
To restore the pre-0.43 high-to-low behavior, pass **`high_to_low=True`**
|
||||
to any fretboard constructor — `Fretboard.guitar(high_to_low=True)`,
|
||||
`Fretboard(tones=..., high_to_low=True)`, and likewise on every instrument
|
||||
preset (`bass`, `ukulele`, `mandolin`, … `keyboard`).
|
||||
|
||||
The flip also applies to **input**: a custom tuning tuple passed to
|
||||
`Fretboard.guitar(...)` and manual fret positions passed to
|
||||
`fingering(*positions)` are now read in the board's orientation
|
||||
(low-to-high by default).
|
||||
|
||||
`to_tab()` and `Part.strum()` are unaffected — they sort by pitch
|
||||
internally and produce identical output regardless of orientation.
|
||||
|
||||
## 0.42.1
|
||||
|
||||
- **Fretboard tuning support** — `to_tab()` now accepts `Fretboard` objects as
|
||||
the `tuning` parameter. Works with `Fretboard.guitar()`, `Fretboard.bass()`,
|
||||
`Fretboard.ukulele()`, `Fretboard.mandolin()`, `Fretboard.banjo()`, and any
|
||||
custom Fretboard with capo.
|
||||
|
||||
## 0.42.0
|
||||
|
||||
- **LilyPond export** — `Score.to_lilypond()` generates complete LilyPond source
|
||||
files with multi-staff scores, key/time signatures, tempo markings, and
|
||||
automatic bass clef detection. Output can be compiled to publication-quality
|
||||
PDFs with LilyPond.
|
||||
- **MusicXML export** — `Score.to_musicxml()` generates MusicXML 4.0 documents
|
||||
that can be opened in MuseScore, Sibelius, Finale, and any notation software.
|
||||
Includes proper ties, chords, clef detection, and tempo/time signature metadata.
|
||||
- **Guitar/bass tablature** — `Part.to_tab()` and `Score.to_tab()` generate ASCII
|
||||
tablature. Supports guitar (6-string), bass (4-string), drop D, and custom
|
||||
tunings. Automatically maps notes to the best string/fret positions.
|
||||
|
||||
## 0.41.4
|
||||
|
||||
- **Fix** — `to_abc()` now ties long notes across barlines instead of emitting
|
||||
oversized durations that abcjs can't render (e.g. 16-beat notes become four
|
||||
tied whole notes).
|
||||
|
||||
## 0.41.3
|
||||
|
||||
- **Fix** — `to_abc()` now skips parts with only drum tones or rests (no pitched
|
||||
notes), fixing "pitch is undefined" errors in abcjs. Chords are correctly
|
||||
recognized as pitched content.
|
||||
|
||||
## 0.41.2
|
||||
|
||||
- **Auto bass clef** — `to_abc()` detects low-register parts (808, bass, timpani)
|
||||
and assigns `clef=bass` automatically based on average note octave.
|
||||
|
||||
## 0.41.1
|
||||
|
||||
- **Fix** — `to_abc()` no longer crashes on parts containing drum tones.
|
||||
|
||||
+27
-3
@@ -94,11 +94,11 @@ PyTheory includes 144 pre-built chords (12 roots x 12 qualities):
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("C")
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
Fingering(E=x, A=3, D=2, G=0, B=1, e=0)
|
||||
>>> fb.chord("Am")
|
||||
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
|
||||
Fingering(E=x, A=0, D=2, G=2, B=1, e=0)
|
||||
>>> fb.chord("G7")
|
||||
Fingering(e=1, B=0, G=0, D=0, A=2, E=3)
|
||||
Fingering(E=3, A=2, D=0, G=0, B=0, e=1)
|
||||
|
||||
You can also build chords directly with ``Chord.from_name()``:
|
||||
|
||||
@@ -503,9 +503,16 @@ are standard arranging techniques for spreading chord tones across registers:
|
||||
>>> cmaj7 = Chord.from_symbol("Cmaj7")
|
||||
>>> cmaj7.close_voicing()
|
||||
<Chord C major 7th>
|
||||
>>> cmaj7.open_voicing()
|
||||
<Chord C major 7th>
|
||||
>>> cmaj7.drop2()
|
||||
<Chord C major 7th>
|
||||
|
||||
``open_voicing()`` takes the close voicing and raises every other
|
||||
non-root tone by an octave, spreading the chord across two octaves.
|
||||
The result is a wider, more spacious sound — common in orchestral
|
||||
writing and piano ballads where you want the harmony to breathe.
|
||||
|
||||
Chord Extensions
|
||||
----------------
|
||||
|
||||
@@ -596,6 +603,23 @@ music that doesn't follow traditional harmony, this is the tool.
|
||||
Major and minor triads share the same prime form — they're inversions
|
||||
of each other in pitch class space.
|
||||
|
||||
The **normal form** is the intermediate step — the most compact ascending
|
||||
arrangement of pitch classes before transposition. It preserves the
|
||||
actual pitch classes (not transposed to 0), so it tells you which
|
||||
specific notes are in the set:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").normal_form
|
||||
(0, 4, 7)
|
||||
|
||||
>>> Chord.from_tones("A", "C", "E").normal_form
|
||||
(9, 0, 4)
|
||||
|
||||
Normal form keeps the original pitch classes; prime form transposes to 0
|
||||
for comparison. Use ``normal_form`` when you care about which notes,
|
||||
``prime_form`` when you care about the abstract shape.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").forte_number
|
||||
|
||||
@@ -111,19 +111,19 @@ Generate fingerings for guitar and ukulele with
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("C")
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
Fingering(E=x, A=3, D=2, G=0, B=1, e=0)
|
||||
>>> fb.chord("G")
|
||||
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
|
||||
Fingering(E=3, A=2, D=0, G=0, B=0, e=3)
|
||||
>>> fb.chord("Am")
|
||||
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
|
||||
Fingering(E=x, A=0, D=2, G=2, B=1, e=0)
|
||||
>>> fb.chord("D")
|
||||
Fingering(e=2, B=3, G=2, D=0, A=x, E=x)
|
||||
Fingering(E=x, A=x, D=0, G=2, B=3, e=2)
|
||||
|
||||
>>> uke = Fretboard.ukulele()
|
||||
>>> uke.chord("C")
|
||||
Fingering(A=3, E=0, C=0, G=0)
|
||||
Fingering(G=0, C=0, E=0, A=3)
|
||||
>>> uke.chord("G")
|
||||
Fingering(A=2, E=3, C=2, G=0)
|
||||
Fingering(G=0, C=2, E=3, A=2)
|
||||
|
||||
Explore an Interval
|
||||
-------------------
|
||||
|
||||
+40
-31
@@ -18,19 +18,28 @@ positions are just semitone steps along the fingerboard.
|
||||
Guitars
|
||||
-------
|
||||
|
||||
`Standard guitar tuning <https://en.wikipedia.org/wiki/Guitar_tunings>`_
|
||||
(high to low)::
|
||||
`Standard guitar tuning <https://en.wikipedia.org/wiki/Guitar_tunings>`_::
|
||||
|
||||
String 1: E4 (highest)
|
||||
String 2: B3
|
||||
String 3: G3
|
||||
String 4: D3
|
||||
String 5: A2
|
||||
String 6: E2 (lowest)
|
||||
String 5: A2
|
||||
String 4: D3
|
||||
String 3: G3
|
||||
String 2: B3
|
||||
String 1: E4 (highest)
|
||||
|
||||
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).
|
||||
|
||||
.. note::
|
||||
|
||||
Since **v0.43.0**, fingerings and string lists read **low to high**
|
||||
(lowest-pitched string first) by default — matching how chord
|
||||
diagrams and tab are conventionally written. To get the pre-0.43
|
||||
high-to-low order, pass ``high_to_low=True`` to any fretboard
|
||||
constructor, e.g. ``Fretboard.guitar(high_to_low=True)``. A custom
|
||||
tuning tuple and manual ``fingering()`` positions are likewise read
|
||||
in the board's orientation.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard
|
||||
@@ -192,12 +201,12 @@ on any instrument. It scores each possibility by:
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> f = fb.chord("C")
|
||||
>>> f
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
Fingering(E=x, A=3, D=2, G=0, B=1, e=0)
|
||||
|
||||
>>> f['A']
|
||||
3
|
||||
>>> f[1]
|
||||
1
|
||||
3
|
||||
|
||||
>>> f.identify()
|
||||
'C major'
|
||||
@@ -210,11 +219,11 @@ You can also go from fret positions to chord identification:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # "What chord am I playing?"
|
||||
>>> # "What chord am I playing?" (positions read low to high)
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> f = fb.fingering(0, 0, 0, 2, 2, 0)
|
||||
>>> f = fb.fingering(0, 2, 2, 0, 0, 0)
|
||||
>>> f
|
||||
Fingering(e=0, B=0, G=0, D=2, A=2, E=0)
|
||||
Fingering(E=0, A=2, D=2, G=0, B=0, e=0)
|
||||
>>> f.identify()
|
||||
'E minor'
|
||||
|
||||
@@ -223,14 +232,14 @@ Reading Fingerings
|
||||
|
||||
Each position is labeled with its string name. Duplicate string names
|
||||
are disambiguated — on a standard guitar, high E appears as ``e`` and
|
||||
low E as ``E``::
|
||||
low E as ``E``. Strings read low to high (lowest first)::
|
||||
|
||||
e|--0-- (open — E)
|
||||
B|--1-- (fret 1 — C)
|
||||
G|--0-- (open — G)
|
||||
D|--2-- (fret 2 — E)
|
||||
E|--x-- (muted — low E)
|
||||
A|--3-- (fret 3 — C)
|
||||
E|--x-- (muted)
|
||||
D|--2-- (fret 2 — E)
|
||||
G|--0-- (open — G)
|
||||
B|--1-- (fret 1 — C)
|
||||
e|--0-- (open — high E)
|
||||
|
||||
A value of ``x`` (``None``) means the string is muted (not played).
|
||||
|
||||
@@ -243,12 +252,12 @@ For a more visual representation, use ``tab()``:
|
||||
|
||||
>>> print(fb.tab("C"))
|
||||
C major
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
A|--3--
|
||||
E|--x--
|
||||
A|--3--
|
||||
D|--2--
|
||||
G|--0--
|
||||
B|--1--
|
||||
e|--0--
|
||||
|
||||
Generating Full Charts
|
||||
----------------------
|
||||
@@ -261,7 +270,7 @@ Generate fingerings for every chord at once:
|
||||
>>> chart = fb.chart()
|
||||
|
||||
>>> chart["C"]
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
Fingering(E=x, A=3, D=2, G=0, B=1, e=0)
|
||||
|
||||
>>> # Works with any instrument
|
||||
>>> uke_chart = Fretboard.ukulele().chart()
|
||||
@@ -303,19 +312,19 @@ Any instrument can be modeled with custom string tunings:
|
||||
|
||||
>>> from pytheory import Tone, Fretboard
|
||||
|
||||
>>> # Baritone ukulele (DGBE — top 4 guitar strings)
|
||||
>>> # Baritone ukulele (DGBE — top 4 guitar strings, low to high)
|
||||
>>> bari_uke = Fretboard(tones=[
|
||||
... Tone.from_string("E4"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("G3"),
|
||||
... Tone.from_string("D3"),
|
||||
... Tone.from_string("G3"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("E4"),
|
||||
... ])
|
||||
|
||||
>>> # Tres cubano (Cuban guitar, 3 doubled courses)
|
||||
>>> # Tres cubano (Cuban guitar, 3 doubled courses, low to high)
|
||||
>>> tres = Fretboard(tones=[
|
||||
... Tone.from_string("E4"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("G3"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("E4"),
|
||||
... ])
|
||||
|
||||
If it has strings, you can model it. Define the tuning, and PyTheory handles the rest -- fingerings, charts, scale diagrams, all of it. Got a weird instrument or a custom tuning? That's what the ``Fretboard`` constructor is for.
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
Nashville Numbers, Blues Scales, and Tablature
|
||||
===============================================
|
||||
|
||||
Three tools that work together: the Nashville number system for writing
|
||||
chord charts, blues scales for improvisation, and tablature for seeing
|
||||
where to put your fingers. This guide covers all three and shows how
|
||||
they connect.
|
||||
|
||||
The Nashville Number System
|
||||
---------------------------
|
||||
|
||||
The `Nashville number system <https://en.wikipedia.org/wiki/Nashville_Number_System>`_
|
||||
replaces chord names with Arabic numerals (1, 2, 3...) so that a chart
|
||||
works in **any key**. It's the standard chart format in Nashville
|
||||
recording studios — a session musician can read a number chart and
|
||||
transpose on the fly without rewriting anything.
|
||||
|
||||
The idea is simple: each number refers to a **scale degree**. In any
|
||||
major key, 1 is the tonic chord, 4 is the subdominant, 5 is the
|
||||
dominant, and so on. The chord quality (major, minor, diminished) is
|
||||
determined by the key — you don't need to write it out.
|
||||
|
||||
In C major::
|
||||
|
||||
1 = C major 5 = G major
|
||||
2 = D minor 6 = A minor
|
||||
3 = E minor 7 = B diminished
|
||||
4 = F major
|
||||
|
||||
In G major::
|
||||
|
||||
1 = G major 5 = D major
|
||||
2 = A minor 6 = E minor
|
||||
3 = B minor 7 = F# diminished
|
||||
4 = C major
|
||||
|
||||
Same numbers, different key, different chords — but the same harmonic
|
||||
relationships.
|
||||
|
||||
Using Nashville Numbers in PyTheory
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Both :class:`~pytheory.scales.Key` and :class:`~pytheory.scales.TonedScale`
|
||||
support the ``nashville()`` method:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> [c.identify() for c in key.nashville(1, 4, 5, 1)]
|
||||
['C major', 'F major', 'G major', 'C major']
|
||||
|
||||
>>> # Same progression, different key — just change the Key
|
||||
>>> key_g = Key("G", "major")
|
||||
>>> [c.identify() for c in key_g.nashville(1, 4, 5, 1)]
|
||||
['G major', 'C major', 'D major', 'G major']
|
||||
|
||||
Nashville numbers and Roman numerals produce the same result — they're
|
||||
two notations for the same concept:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key = Key("G", "major")
|
||||
>>> nash = [c.identify() for c in key.nashville(1, 5, 6, 4)]
|
||||
>>> roman = [c.identify() for c in key.progression("I", "V", "vi", "IV")]
|
||||
>>> nash == roman
|
||||
True
|
||||
|
||||
Seventh Chords
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Suffix ``"7"`` to get seventh chords — essential for jazz and blues
|
||||
charts:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> [c.identify() for c in key.nashville("17", "47", "57")]
|
||||
['C major 7th', 'F major 7th', 'G dominant 7th']
|
||||
|
||||
Nashville vs. Roman Numerals
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When should you use which?
|
||||
|
||||
- **Nashville numbers** — faster to type, easier to read at a glance,
|
||||
standard in studio sessions. Use ``key.nashville(1, 4, 5, 1)``.
|
||||
- **Roman numerals** — encode chord quality (uppercase = major,
|
||||
lowercase = minor), standard in theory textbooks. Use
|
||||
``key.progression("I", "IV", "V", "I")``.
|
||||
|
||||
Both are fully supported. Use whichever fits your workflow.
|
||||
|
||||
Blues Scales
|
||||
------------
|
||||
|
||||
The `blues scale <https://en.wikipedia.org/wiki/Blues_scale>`_ is a
|
||||
six-note scale built from the minor pentatonic plus one chromatic
|
||||
passing tone — the **blue note** (flat 5th). That single added note
|
||||
gives the blues its tension and character.
|
||||
|
||||
The blues system in PyTheory includes several related scales:
|
||||
|
||||
==================== ===== ==================================
|
||||
Scale Notes Character
|
||||
==================== ===== ==================================
|
||||
minor pentatonic 5 Foundation of rock and blues soloing
|
||||
major pentatonic 5 Bright, country, pop
|
||||
blues 6 Minor pentatonic + blue note (b5)
|
||||
major blues 6 Major pentatonic + blue note (b3)
|
||||
dominant 7 Mixolydian — dominant 7th sound
|
||||
minor 7 Dorian-like — minor with natural 6th
|
||||
==================== ===== ==================================
|
||||
|
||||
Building Blues Scales
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Use ``system="blues"`` when creating a :class:`~pytheory.scales.TonedScale`:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> c = TonedScale(tonic="C4", system="blues")
|
||||
|
||||
>>> c["minor pentatonic"].note_names
|
||||
['C', 'Eb', 'F', 'G', 'Bb', 'C']
|
||||
|
||||
>>> c["blues"].note_names
|
||||
['C', 'Eb', 'F', 'Gb', 'G', 'Bb', 'C']
|
||||
|
||||
>>> c["major pentatonic"].note_names
|
||||
['C', 'D', 'E', 'G', 'A', 'C']
|
||||
|
||||
>>> c["major blues"].note_names
|
||||
['C', 'D', 'Eb', 'E', 'G', 'A', 'C']
|
||||
|
||||
The Anatomy of a Blues Scale
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The blues scale in C::
|
||||
|
||||
C Eb F Gb G Bb C
|
||||
1 b3 4 b5 5 b7 8
|
||||
|
||||
Root ──┐
|
||||
├── minor 3rd (3 semitones)
|
||||
├── perfect 4th (5 semitones)
|
||||
├── diminished 5th (6 semitones) ← the "blue note"
|
||||
├── perfect 5th (7 semitones)
|
||||
├── minor 7th (10 semitones)
|
||||
└── octave (12 semitones)
|
||||
|
||||
The blue note (Gb/F#) sits between the 4th and 5th — a dissonant,
|
||||
unstable pitch that resolves up or down. It's what makes blues sound
|
||||
like blues.
|
||||
|
||||
The 12-Bar Blues
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The `12-bar blues <https://en.wikipedia.org/wiki/Twelve-bar_blues>`_ is
|
||||
the most important chord progression in American music. It uses the
|
||||
Nashville numbers 1, 4, and 5::
|
||||
|
||||
| 1 | 1 | 1 | 1 |
|
||||
| 4 | 4 | 1 | 1 |
|
||||
| 5 | 4 | 1 | 5 |
|
||||
|
||||
In the key of A:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> key = Key("A", "major")
|
||||
>>> bars = key.nashville(1,1,1,1, 4,4,1,1, 5,4,1,5)
|
||||
>>> [c.identify() for c in bars]
|
||||
['A major', 'A major', 'A major', 'A major', 'D major', 'D major', 'A major', 'A major', 'E major', 'D major', 'A major', 'E major']
|
||||
|
||||
For an authentic blues sound, use dominant 7th chords:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> bars_7 = key.nashville("17","17","17","17", "47","47","17","17", "57","47","17","57")
|
||||
>>> [c.identify() for c in bars_7]
|
||||
['A major 7th', 'A major 7th', 'A major 7th', 'A major 7th', 'D major 7th', 'D major 7th', 'A major 7th', 'A major 7th', 'E dominant 7th', 'D major 7th', 'A major 7th', 'E dominant 7th']
|
||||
|
||||
Or use the built-in named progression:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key = Key("A", "major")
|
||||
>>> blues = key.common_progressions()["12-bar blues"]
|
||||
>>> [c.identify() for c in blues]
|
||||
['A major', 'A major', 'A major', 'A major', 'D major', 'D major', 'A major', 'A major', 'E major', 'D major', 'A major', 'E major']
|
||||
|
||||
Blues Scale on the Fretboard
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Visualize the blues scale on guitar to see the patterns:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard, TonedScale
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> blues = TonedScale(tonic="A4", system="blues")["blues"]
|
||||
>>> print(fb.scale_diagram(blues, frets=12))
|
||||
0 1 2 3 4 5 6 7 8 9 10 11 12
|
||||
E| E | - | - | G | - | A | - | - | C | - | D | Eb| E |
|
||||
A| A | - | - | C | - | D | Eb| E | - | - | G | - | A |
|
||||
D| D | Eb| E | - | - | G | - | A | - | - | C | - | D |
|
||||
G| G | - | A | - | - | C | - | D | Eb| E | - | - | G |
|
||||
B| - | C | - | D | Eb| E | - | - | G | - | A | - | - |
|
||||
E| E | - | - | G | - | A | - | - | C | - | D | Eb| E |
|
||||
|
||||
The minor pentatonic (same scale without the Eb) is the most-played
|
||||
scale in rock guitar. Add the blue note and you have the full blues
|
||||
scale — the same shapes, one extra fret.
|
||||
|
||||
Tablature
|
||||
---------
|
||||
|
||||
`Tablature <https://en.wikipedia.org/wiki/Tablature>`_ (tab) shows
|
||||
**where to put your fingers** rather than what notes to play. Each line
|
||||
represents a string; numbers indicate fret positions. PyTheory generates
|
||||
tabs at three levels:
|
||||
|
||||
1. **Chord tabs** — single chord fingerings
|
||||
2. **Part tabs** — full melody/sequence notation
|
||||
3. **Score tabs** — extract a part from a multi-part score
|
||||
|
||||
Chord Tablature
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Get the tab for any chord on any instrument:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> print(fb.tab("C"))
|
||||
C major
|
||||
E|--x--
|
||||
A|--3--
|
||||
D|--2--
|
||||
G|--0--
|
||||
B|--1--
|
||||
e|--0--
|
||||
|
||||
>>> print(fb.tab("Am"))
|
||||
A minor
|
||||
E|--x--
|
||||
A|--0--
|
||||
D|--2--
|
||||
G|--2--
|
||||
B|--1--
|
||||
e|--0--
|
||||
|
||||
>>> print(fb.tab("E7"))
|
||||
E dominant 7th
|
||||
E|--0--
|
||||
A|--2--
|
||||
D|--0--
|
||||
G|--1--
|
||||
B|--0--
|
||||
e|--0--
|
||||
|
||||
Works with any instrument:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> uke = Fretboard.ukulele()
|
||||
>>> print(uke.tab("C"))
|
||||
C major
|
||||
G|--0--
|
||||
C|--0--
|
||||
E|--0--
|
||||
A|--3--
|
||||
|
||||
Reading Tab Notation
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
E|--x-- ← muted (don't play this string)
|
||||
A|--3-- ← press fret 3
|
||||
D|--2-- ← press fret 2
|
||||
G|--0-- ← open string
|
||||
B|--1-- ← press fret 1
|
||||
e|--0-- ← open string (don't fret, just pluck)
|
||||
|
||||
- Each line is a string. Chord tab from ``fb.tab()`` lists strings
|
||||
low-to-high (lowest pitch at top) by default since v0.43.0; pass
|
||||
``high_to_low=True`` to the fretboard for the traditional
|
||||
highest-pitch-on-top layout.
|
||||
- Numbers are fret positions (0 = open, 1-24 = fretted)
|
||||
- ``x`` means the string is muted / not played
|
||||
- ``|`` marks measure boundaries in sequence tabs
|
||||
|
||||
Part Tablature
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Generate tab from a composed part using ``to_tab()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Key, Duration
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
lead = score.part("lead", synth="saw")
|
||||
|
||||
# A simple blues lick
|
||||
for note in ["A4", "C5", "D5", "Eb5", "E5", "G5", "A5"]:
|
||||
lead.add(note, Duration.QUARTER)
|
||||
|
||||
print(lead.to_tab())
|
||||
|
||||
This outputs standard ASCII tab with measure lines, mapping each note
|
||||
to the most playable string and fret position.
|
||||
|
||||
Tuning Options
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The ``to_tab()`` method supports multiple tunings:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Standard guitar (default)
|
||||
lead.to_tab(tuning="guitar")
|
||||
|
||||
# 4-string bass
|
||||
lead.to_tab(tuning="bass")
|
||||
|
||||
# Drop D guitar
|
||||
lead.to_tab(tuning="drop_d")
|
||||
|
||||
# Any Fretboard object — use any of the 25+ instrument presets
|
||||
from pytheory import Fretboard
|
||||
lead.to_tab(tuning=Fretboard.mandolin())
|
||||
lead.to_tab(tuning=Fretboard.banjo())
|
||||
|
||||
# Custom tuning as MIDI note numbers (low string first)
|
||||
lead.to_tab(tuning=[40, 45, 50, 55, 59, 64])
|
||||
|
||||
Score Tablature
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Extract tab from a multi-part score:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
rhythm = score.part("rhythm", synth="saw")
|
||||
lead = score.part("lead", synth="triangle")
|
||||
bass = score.part("bass", synth="sine")
|
||||
|
||||
# ... compose parts ...
|
||||
|
||||
# Tab the lead part
|
||||
print(score.to_tab("lead"))
|
||||
|
||||
# Tab the first non-drum part (if no name given)
|
||||
print(score.to_tab())
|
||||
|
||||
# Bass tab
|
||||
print(score.to_tab("bass", tuning="bass"))
|
||||
|
||||
Putting It All Together
|
||||
-----------------------
|
||||
|
||||
Here's a complete example that uses all three features — Nashville
|
||||
numbers for the chord progression, the blues scale for the melody, and
|
||||
tab export to see the fingering:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Key, TonedScale, Fretboard, Score, Duration
|
||||
|
||||
# 1. Nashville numbers for the progression
|
||||
key = Key("A", "major")
|
||||
chords = key.nashville(1, 1, 1, 1, 4, 4, 1, 1, 5, 4, 1, 5)
|
||||
|
||||
# 2. Blues scale for the melody
|
||||
blues = TonedScale(tonic="A4", system="blues")["blues"]
|
||||
|
||||
# 3. Compose a score
|
||||
score = Score("4/4", bpm=120)
|
||||
rhythm = score.part("rhythm", synth="saw", envelope="pad")
|
||||
lead = score.part("lead", synth="triangle", envelope="pluck")
|
||||
|
||||
for chord in chords:
|
||||
rhythm.add(chord, Duration.WHOLE)
|
||||
|
||||
for note_name in blues.note_names[:-1]: # walk up the scale
|
||||
lead.add(f"{note_name}4", Duration.HALF)
|
||||
|
||||
# 4. See it as tablature
|
||||
print(lead.to_tab())
|
||||
|
||||
# 5. See the scale on the fretboard
|
||||
fb = Fretboard.guitar()
|
||||
print(fb.scale_diagram(blues, frets=12))
|
||||
|
||||
Nashville numbers tell you *what chords to play*. The blues scale tells you *what notes to solo with*. Tablature tells you *where to put your fingers*. Together, they're everything you need to play the blues.
|
||||
+157
-7
@@ -3,19 +3,21 @@ Playback and Export
|
||||
|
||||
This is the output layer. You've built your theory, composed your
|
||||
arrangement, shaped your sounds -- now you need to hear it. PyTheory
|
||||
gives you three ways to get your music out: speakers, WAV files, and
|
||||
MIDI files.
|
||||
gives you four ways to get your music out: speakers, WAV files, MIDI
|
||||
files, and sheet music.
|
||||
|
||||
Use **speakers** for immediate feedback while you're sketching and
|
||||
experimenting. Use **WAV export** when you want to share actual audio
|
||||
-- post it, send it, drop it into a video. Use **MIDI export** when you
|
||||
want to bring your sketch into a real DAW and finish it with
|
||||
professional instruments, mixing, and mastering. Each output serves a
|
||||
different stage of the creative process.
|
||||
professional instruments, mixing, and mastering. Use **ABC notation
|
||||
export** when you want sheet music -- rendered in the browser or shared
|
||||
as plain text. Each output serves a different stage of the creative
|
||||
process.
|
||||
|
||||
PyTheory can play audio through your speakers, save to WAV, or export
|
||||
to MIDI. Everything is synthesized from waveforms -- no samples or
|
||||
external audio files needed.
|
||||
PyTheory can play audio through your speakers, save to WAV, export to
|
||||
MIDI, or generate sheet music as ABC notation. Everything is synthesized
|
||||
from waveforms -- no samples or external audio files needed.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -171,6 +173,154 @@ Score-based export (with time signature, tempo, and parts):
|
||||
score.add(chord, Duration.WHOLE)
|
||||
score.save_midi("progression.mid")
|
||||
|
||||
to_abc() -- ABC Notation / Sheet Music
|
||||
---------------------------------------
|
||||
|
||||
ABC notation is a human-readable text format for music that tools can
|
||||
turn into staff notation and MIDI. It's widely used for folk tunes,
|
||||
lead sheets, and quick sketches. PyTheory can export any Score as ABC
|
||||
notation -- and optionally wrap it in an HTML page that renders
|
||||
sheet music right in the browser using `abcjs <https://www.abcjs.net/>`_.
|
||||
|
||||
Basic export:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Duration, Key
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
lead = score.part("lead")
|
||||
for chord in Key("C", "major").progression("I", "V", "vi", "IV"):
|
||||
lead.add(chord, Duration.WHOLE)
|
||||
|
||||
print(score.to_abc(title="Pop Chords", key="C"))
|
||||
|
||||
Output:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
X:1
|
||||
T:Pop Chords
|
||||
M:4/4
|
||||
Q:1/4=120
|
||||
L:1/8
|
||||
K:C
|
||||
[CEG]8 | [GBd]8 | [Ace]8 | [FAc]8 |
|
||||
|
||||
Open sheet music in the browser with ``html=True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
html = score.to_abc(title="Pop Chords", key="C", html=True)
|
||||
|
||||
with open("chords.html", "w") as f:
|
||||
f.write(html)
|
||||
|
||||
import webbrowser
|
||||
webbrowser.open("chords.html")
|
||||
|
||||
This generates a self-contained HTML page with an embedded
|
||||
``<script>`` tag that loads abcjs from a CDN and renders the notation
|
||||
as SVG -- no build steps, no dependencies, just open the file.
|
||||
|
||||
Multi-part scores automatically get ``V:`` (voice) directives so each
|
||||
instrument appears on its own staff. Bass parts (average note below C4)
|
||||
get bass clef automatically. Drum-only parts are skipped. Notes longer
|
||||
than one measure are split into tied notes across barlines.
|
||||
|
||||
Parameters:
|
||||
|
||||
- **title** -- Tune title for the ``T:`` header (default ``"Untitled"``).
|
||||
- **key** -- ABC key signature string (default ``"C"``). Use ``"Am"`` for
|
||||
A minor, ``"Bb"`` for B-flat major, ``"F#m"`` for F-sharp minor, etc.
|
||||
- **html** -- If ``True``, return a full HTML document instead of raw ABC
|
||||
(default ``False``).
|
||||
|
||||
to_lilypond() -- LilyPond Export
|
||||
---------------------------------
|
||||
|
||||
`LilyPond <https://lilypond.org/>`_ is the gold standard for
|
||||
publication-quality music engraving. ``to_lilypond()`` generates
|
||||
complete LilyPond source files that you can compile to PDF:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
lead = score.part("lead")
|
||||
for note in ["C4", "D4", "E4", "F4"]:
|
||||
lead.add(note, Duration.QUARTER)
|
||||
|
||||
ly = score.to_lilypond(title="My Score", key="C", mode="major")
|
||||
|
||||
with open("score.ly", "w") as f:
|
||||
f.write(ly)
|
||||
|
||||
Then compile with ``lilypond score.ly`` to get a PDF. Multi-part scores
|
||||
get separate staves in a ``StaffGroup``, bass clef is auto-detected,
|
||||
and long notes are split with ties across barlines.
|
||||
|
||||
Parameters:
|
||||
|
||||
- **title** -- Title for the ``\header`` block (default ``"Untitled"``).
|
||||
- **key** -- Key signature root (default ``"C"``). Use note names like
|
||||
``"Bb"``, ``"F#"``, ``"Eb"``.
|
||||
- **mode** -- LilyPond mode string (default ``"major"``). Use ``"minor"``
|
||||
for minor keys.
|
||||
|
||||
to_musicxml() -- MusicXML Export
|
||||
---------------------------------
|
||||
|
||||
MusicXML is the interchange format for notation software. Export your
|
||||
score and open it in MuseScore, Sibelius, Finale, Dorico, or any
|
||||
other notation app:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
xml = score.to_musicxml(title="My Score")
|
||||
|
||||
with open("score.musicxml", "w") as f:
|
||||
f.write(xml)
|
||||
|
||||
The output is a complete MusicXML 4.0 partwise document with proper
|
||||
time signatures, tempo markings, clef detection, tied notes across
|
||||
barlines, and chord notation. No external dependencies needed.
|
||||
|
||||
to_tab() -- Guitar/Bass Tablature
|
||||
-----------------------------------
|
||||
|
||||
Generate ASCII tablature from any Part or Score:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead")
|
||||
lead.add("E4", Duration.QUARTER)
|
||||
lead.add("B3", Duration.QUARTER)
|
||||
lead.add("G3", Duration.QUARTER)
|
||||
lead.add("D3", Duration.QUARTER)
|
||||
|
||||
print(lead.to_tab())
|
||||
|
||||
Output::
|
||||
|
||||
e|---0---------|
|
||||
B|------0------|
|
||||
G|---------0---|
|
||||
D|------------0|
|
||||
A|-------------|
|
||||
E|-------------|
|
||||
|
||||
Works on Score too -- it picks the first melodic part automatically:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(score.to_tab()) # auto-pick part
|
||||
print(score.to_tab(part_name="bass")) # specific part
|
||||
print(score.to_tab(tuning="bass")) # 4-string bass tab
|
||||
print(score.to_tab(tuning="drop_d")) # drop D guitar
|
||||
|
||||
Supports ``"guitar"`` (6-string standard), ``"bass"`` (4-string),
|
||||
``"drop_d"``, or a custom list of MIDI note numbers for any tuning.
|
||||
|
||||
play_pattern() -- Drum Patterns
|
||||
-------------------------------
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ Guitar fingerings:
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("Am")
|
||||
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
|
||||
Fingering(E=x, A=0, D=2, G=2, B=1, e=0)
|
||||
|
||||
All of the above works without PortAudio, without sounddevice,
|
||||
without any audio setup at all. It's pure Python music theory.
|
||||
|
||||
+27
-1
@@ -269,6 +269,23 @@ easy:
|
||||
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished']
|
||||
>>> key.seventh_chords
|
||||
['G major 7th', 'A minor 7th', 'B minor 7th', 'C major 7th', 'D dominant 7th', 'E minor 7th', 'F# half-diminished 7th']
|
||||
|
||||
Build a seventh chord on any individual degree with ``seventh()``:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key.seventh(0) # I7
|
||||
G major 7th
|
||||
>>> key.seventh(4) # V7
|
||||
D dominant 7th
|
||||
>>> key.seventh(6) # vii7
|
||||
F# half-diminished 7th
|
||||
|
||||
This is the single-degree version of ``seventh_chords`` — useful when
|
||||
you need one specific chord rather than the full list.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key.detect("C", "E", "G", "A", "D")
|
||||
C major
|
||||
|
||||
@@ -440,7 +457,16 @@ alternative scales to improvise over:
|
||||
>>> Scale.recommend("C", "Eb", "F", "Gb", "G", "Bb", top=3)
|
||||
[('C', 'blues', 1.0), ...]
|
||||
|
||||
Chromatic scales are deprioritized since they match everything.
|
||||
How it works: ``recommend()`` tests your notes against every scale in
|
||||
every key (all 12 tonics times all scale types in the Western system).
|
||||
Each candidate is scored using ``fitness()`` — the fraction of your notes
|
||||
that belong to that scale (1.0 = perfect match). Results are ranked by
|
||||
fitness, with chromatic scales deprioritized since they match everything.
|
||||
Scales whose length is closer to the number of input notes are preferred
|
||||
when fitness scores tie.
|
||||
|
||||
Returns a list of ``(tonic, scale_name, fitness)`` tuples. Pass ``top=``
|
||||
to control how many results you get back (default 5).
|
||||
|
||||
Parallel Modes
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -357,6 +357,32 @@ every tone knows its enharmonic spelling:
|
||||
>>> Tone.from_string("C4", system="western").enharmonic is None
|
||||
True
|
||||
|
||||
Accidental Properties
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Check whether a tone is natural, sharp, or flat:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c = Tone.from_string("C4", system="western")
|
||||
>>> c.is_natural
|
||||
True
|
||||
>>> c.is_sharp
|
||||
False
|
||||
|
||||
>>> cs = Tone.from_string("C#4", system="western")
|
||||
>>> cs.is_sharp
|
||||
True
|
||||
>>> cs.is_natural
|
||||
False
|
||||
|
||||
>>> bb = Tone.from_string("Bb4", system="western")
|
||||
>>> bb.is_flat
|
||||
True
|
||||
|
||||
Useful for filtering — for example, finding all natural notes in a
|
||||
scale, or counting accidentals in a melody.
|
||||
|
||||
Extended Enharmonics
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ What's Inside
|
||||
guide/scales
|
||||
guide/chords
|
||||
guide/fretboard
|
||||
guide/nashville-blues-tabs
|
||||
guide/systems
|
||||
guide/sequencing
|
||||
guide/synths
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
from pytheory import Tone, Fretboard, CHARTS
|
||||
from pytheory import Fretboard, CHARTS
|
||||
|
||||
# Create standard tuning (from high E to low E)
|
||||
standard_tuning = [
|
||||
Tone.from_string("E4"), # High E
|
||||
Tone.from_string("B3"), # B
|
||||
Tone.from_string("G3"), # G
|
||||
Tone.from_string("D3"), # D
|
||||
Tone.from_string("A2"), # A
|
||||
Tone.from_string("E2"), # Low E
|
||||
]
|
||||
|
||||
# Create fretboard with standard tuning
|
||||
fretboard = Fretboard(tones=standard_tuning)
|
||||
# Standard guitar fretboard. Since v0.43.0 fingerings read low to high
|
||||
# (low E first) by default — exactly how tab is conventionally written.
|
||||
fretboard = Fretboard.guitar()
|
||||
|
||||
# Define flat to sharp note mappings (updated to include all possible flats)
|
||||
flat_to_sharp = {"Ab": "G#", "Bb": "A#", "Db": "C#", "Eb": "D#", "Gb": "F#"}
|
||||
@@ -26,34 +17,6 @@ print("Standard Guitar Chord Charts:")
|
||||
print("-" * 30)
|
||||
|
||||
|
||||
def fingering_to_tab(fingering):
|
||||
if not fingering:
|
||||
return ""
|
||||
|
||||
# Create 6 strings of dashes, representing the guitar strings
|
||||
strings = ["-" * 15 for _ in range(6)]
|
||||
|
||||
# For each string (starting from high E)
|
||||
for string_num, fret in enumerate(fingering):
|
||||
if fret is not None:
|
||||
# Place the fret number at the correct position
|
||||
if fret == 0:
|
||||
strings[string_num] = "0" + strings[string_num][1:]
|
||||
else:
|
||||
strings[string_num] = (
|
||||
"-" * (fret - 1) + str(fret) + strings[string_num][fret:]
|
||||
)
|
||||
|
||||
# Combine strings with newlines, and add string names
|
||||
tab = "e|" + strings[0] + "\n"
|
||||
tab += "B|" + strings[1] + "\n"
|
||||
tab += "G|" + strings[2] + "\n"
|
||||
tab += "D|" + strings[3] + "\n"
|
||||
tab += "A|" + strings[4] + "\n"
|
||||
tab += "E|" + strings[5] + "\n"
|
||||
return tab
|
||||
|
||||
|
||||
for chord_name in all_chords:
|
||||
# Store original chord name for lookup
|
||||
lookup_name = chord_name
|
||||
@@ -74,7 +37,7 @@ for chord_name in all_chords:
|
||||
try:
|
||||
fingering = chord.fingering(fretboard=fretboard)
|
||||
print(f"\n{display_name}:")
|
||||
print(fingering_to_tab(fingering))
|
||||
print(fingering.tab())
|
||||
except Exception as e:
|
||||
print(f"{display_name}: Unable to calculate fingering - {str(e)}")
|
||||
# Add more detailed debug information
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.41.1"
|
||||
version = "0.43.1"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.41.1"
|
||||
__version__ = "0.43.1"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+77
-42
@@ -13,7 +13,8 @@ STANDARD_GUITAR_TUNING = ("E4", "B3", "G3", "D3", "A2", "E2")
|
||||
|
||||
# Curated override fingerings for common guitar chords in standard tuning.
|
||||
# Key: chord name, Value: tuple of fret positions (-1 = muted string).
|
||||
# Order is high-to-low (matching Fretboard.guitar() string order).
|
||||
# Order is canonical high-to-low (high-E first); Fingering re-orients these
|
||||
# to the board's display orientation (low-to-high by default since v0.43.0).
|
||||
GUITAR_OVERRIDES = {
|
||||
"C": (0, 1, 0, 2, 3, -1),
|
||||
"D": (2, 3, 2, 0, -1, -1),
|
||||
@@ -60,11 +61,16 @@ class Fingering:
|
||||
3
|
||||
"""
|
||||
|
||||
def __init__(self, positions: tuple, string_names: tuple[str, ...], *, fretboard=None) -> None:
|
||||
self.positions = tuple(positions)
|
||||
def __init__(self, positions: tuple, string_names: tuple[str, ...], *,
|
||||
fretboard=None, high_to_low: bool = True) -> None:
|
||||
# `positions` / `string_names` arrive in canonical (high-to-low)
|
||||
# order; `high_to_low` controls only how they're presented. The
|
||||
# default (True) keeps standalone construction high-to-low.
|
||||
self.high_to_low = high_to_low
|
||||
self._positions = tuple(positions)
|
||||
self._fretboard = fretboard
|
||||
# Disambiguate duplicate names: for standard guitar tuning
|
||||
# (high-to-low), the first occurrence of a duplicate becomes
|
||||
# Disambiguate duplicate names in canonical (high-to-low) order:
|
||||
# the first (higher-pitched) occurrence of a duplicate becomes
|
||||
# lowercase (e.g. high E → 'e') while the last keeps uppercase.
|
||||
from collections import Counter
|
||||
name_counts = Counter(string_names)
|
||||
@@ -77,8 +83,22 @@ class Fingering:
|
||||
else:
|
||||
unique_names.append(name)
|
||||
|
||||
self.string_names = tuple(unique_names)
|
||||
self._map = dict(zip(self.string_names, self.positions))
|
||||
self._string_names = tuple(unique_names)
|
||||
self._map = dict(zip(self._string_names, self._positions))
|
||||
|
||||
def _orient(self, seq):
|
||||
"""Re-orient a canonical (high-to-low) sequence for display."""
|
||||
return tuple(seq) if self.high_to_low else tuple(reversed(seq))
|
||||
|
||||
@property
|
||||
def positions(self) -> tuple:
|
||||
"""Fret positions in this fingering's orientation (low-to-high by default)."""
|
||||
return self._orient(self._positions)
|
||||
|
||||
@property
|
||||
def string_names(self) -> tuple:
|
||||
"""String names in this fingering's orientation (low-to-high by default)."""
|
||||
return self._orient(self._string_names)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
pairs = ", ".join(
|
||||
@@ -114,11 +134,12 @@ class Fingering:
|
||||
"""
|
||||
if self._fretboard is None:
|
||||
raise ValueError("Cannot resolve tones without a fretboard reference.")
|
||||
tones = []
|
||||
for pos, tone in zip(self.positions, self._fretboard.tones):
|
||||
if pos is not None:
|
||||
tones.append(tone.add(pos))
|
||||
return tones
|
||||
# Zip canonical positions with canonical open tones so they always
|
||||
# align regardless of orientation, then present in display order.
|
||||
tones = [tone.add(pos)
|
||||
for pos, tone in zip(self._positions, self._fretboard._tones)
|
||||
if pos is not None]
|
||||
return tones if self.high_to_low else list(reversed(tones))
|
||||
|
||||
def to_chord(self, fretboard=None) -> "Chord":
|
||||
"""Apply this fingering to a fretboard, returning a Chord.
|
||||
@@ -131,10 +152,9 @@ class Fingering:
|
||||
fb = fretboard or self._fretboard
|
||||
if fb is None:
|
||||
raise ValueError("No fretboard provided.")
|
||||
tones = []
|
||||
for pos, tone in zip(self.positions, fb.tones):
|
||||
if pos is not None:
|
||||
tones.append(tone.add(pos))
|
||||
tones = [tone.add(pos)
|
||||
for pos, tone in zip(self._positions, fb._tones)
|
||||
if pos is not None]
|
||||
return Chord(tones=tones)
|
||||
|
||||
def identify(self) -> Optional[str]:
|
||||
@@ -151,12 +171,12 @@ class Fingering:
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> print(fb.chord("C").tab())
|
||||
C
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
E|--x--
|
||||
A|--3--
|
||||
E|--0--
|
||||
D|--2--
|
||||
G|--0--
|
||||
B|--1--
|
||||
e|--0--
|
||||
"""
|
||||
if self._fretboard is None:
|
||||
raise ValueError("Cannot render tab without a fretboard reference.")
|
||||
@@ -306,7 +326,7 @@ class NamedChord:
|
||||
return tuple(fingerings)
|
||||
|
||||
fingering = []
|
||||
for i, tone in enumerate(fretboard.tones):
|
||||
for i, tone in enumerate(fretboard._tones):
|
||||
frets = find_fingerings(tone)
|
||||
# Always allow muting as an option
|
||||
if frets:
|
||||
@@ -335,8 +355,13 @@ class NamedChord:
|
||||
return tuple(itertools.product(*self._possible_fingerings(fretboard=fretboard)))
|
||||
|
||||
def _cache_key(self, fretboard):
|
||||
"""Return a hashable key for memoization."""
|
||||
return (self.name, tuple(t.full_name for t in fretboard.tones))
|
||||
"""Return a hashable key for memoization.
|
||||
|
||||
Keyed on canonical tones plus orientation so the two display
|
||||
orderings of the same board don't collide in the result caches.
|
||||
"""
|
||||
return (self.name, tuple(t.full_name for t in fretboard._tones),
|
||||
fretboard.high_to_low)
|
||||
|
||||
def fingering(self, *, fretboard, multiple=False):
|
||||
# Check cache first
|
||||
@@ -348,19 +373,22 @@ class NamedChord:
|
||||
if key in _fingering_cache:
|
||||
return _fingering_cache[key]
|
||||
|
||||
# Check for curated guitar chord overrides in standard tuning
|
||||
tuning = tuple(t.full_name for t in fretboard.tones)
|
||||
# Check for curated guitar chord overrides in standard tuning.
|
||||
# Overrides are written in canonical (high-to-low) order.
|
||||
tuning = tuple(t.full_name for t in fretboard._tones)
|
||||
if tuning == STANDARD_GUITAR_TUNING and self.name in GUITAR_OVERRIDES:
|
||||
string_names = tuple(t.name for t in fretboard.tones)
|
||||
string_names = tuple(t.name for t in fretboard._tones)
|
||||
override = GUITAR_OVERRIDES[self.name]
|
||||
if not multiple:
|
||||
result = Fingering(self.fix_fingering(override), string_names, fretboard=fretboard)
|
||||
result = Fingering(self.fix_fingering(override), string_names,
|
||||
fretboard=fretboard, high_to_low=fretboard.high_to_low)
|
||||
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_cache.clear()
|
||||
_fingering_cache[key] = result
|
||||
return result
|
||||
else:
|
||||
result = (Fingering(self.fix_fingering(override), string_names, fretboard=fretboard),)
|
||||
result = (Fingering(self.fix_fingering(override), string_names,
|
||||
fretboard=fretboard, high_to_low=fretboard.high_to_low),)
|
||||
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_multi_cache.clear()
|
||||
_fingering_multi_cache[key] = result
|
||||
@@ -390,7 +418,7 @@ class NamedChord:
|
||||
sounding_names = set()
|
||||
for i, f in enumerate(fingering):
|
||||
if f != -1:
|
||||
sounding_names.add(fretboard.tones[i].add(f).name)
|
||||
sounding_names.add(fretboard._tones[i].add(f).name)
|
||||
required = set(t.name for t in self.acceptable_tones)
|
||||
missing = required - sounding_names
|
||||
score -= len(missing) * 5.0
|
||||
@@ -433,12 +461,13 @@ class NamedChord:
|
||||
if fingers_needed > 4:
|
||||
score -= (fingers_needed - 4) * 5.0
|
||||
|
||||
# Reward root in bass — the lowest sounding string
|
||||
# Reward root in bass — the lowest sounding string. `fingering`
|
||||
# is canonical (high-to-low), so the last index is the bass.
|
||||
for i in range(len(fingering) - 1, -1, -1):
|
||||
f = fingering[i]
|
||||
if f == -1:
|
||||
continue
|
||||
bass_tone = fretboard.tones[i].add(f)
|
||||
bass_tone = fretboard._tones[i].add(f)
|
||||
if bass_tone.name == self.tone.name:
|
||||
score += 4.0
|
||||
else:
|
||||
@@ -467,17 +496,20 @@ class NamedChord:
|
||||
if s == max_score:
|
||||
yield possible_fingering
|
||||
|
||||
string_names = tuple(t.name for t in fretboard.tones)
|
||||
string_names = tuple(t.name for t in fretboard._tones)
|
||||
best_fingerings = tuple([g for g in gen()])
|
||||
if not multiple:
|
||||
result = Fingering(self.fix_fingering(best_fingerings[0]), string_names, fretboard=fretboard)
|
||||
result = Fingering(self.fix_fingering(best_fingerings[0]), string_names,
|
||||
fretboard=fretboard, high_to_low=fretboard.high_to_low)
|
||||
# Bounded cache: clear entirely if over limit
|
||||
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_cache.clear()
|
||||
_fingering_cache[key] = result
|
||||
return result
|
||||
else:
|
||||
result = tuple([Fingering(self.fix_fingering(f), string_names, fretboard=fretboard) for f in best_fingerings])
|
||||
result = tuple([Fingering(self.fix_fingering(f), string_names,
|
||||
fretboard=fretboard, high_to_low=fretboard.high_to_low)
|
||||
for f in best_fingerings])
|
||||
# Bounded cache: clear entirely if over limit
|
||||
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_multi_cache.clear()
|
||||
@@ -491,18 +523,21 @@ class NamedChord:
|
||||
|
||||
>>> print(CHARTS["western"]["C"].tab(fretboard=Fretboard.guitar()))
|
||||
C
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
E|--x--
|
||||
A|--3--
|
||||
E|--0--
|
||||
D|--2--
|
||||
G|--0--
|
||||
B|--1--
|
||||
e|--0--
|
||||
"""
|
||||
fingering = self.fingering(fretboard=fretboard)
|
||||
string_names = [t.name for t in fretboard.tones]
|
||||
# Use the fingering's oriented, disambiguated string names/positions
|
||||
# so the tab honors the fretboard's orientation.
|
||||
string_names = fingering.string_names
|
||||
positions = fingering.positions
|
||||
lines = [self.name]
|
||||
max_name = max(len(n) for n in string_names)
|
||||
for i, (name, fret) in enumerate(zip(string_names, fingering)):
|
||||
for name, fret in zip(string_names, positions):
|
||||
fret_str = "x" if fret is None else str(fret)
|
||||
lines.append(f"{name:>{max_name}}|--{fret_str}--")
|
||||
return "\n".join(lines)
|
||||
|
||||
+177
-172
@@ -1352,14 +1352,63 @@ class Chord:
|
||||
|
||||
|
||||
class Fretboard:
|
||||
def __init__(self, *, tones: list[Tone]) -> None:
|
||||
def __init__(self, *, tones: list[Tone], high_to_low: bool = False,
|
||||
_canonical: bool = False) -> None:
|
||||
"""Initialize a Fretboard from a list of open-string Tone objects.
|
||||
|
||||
Args:
|
||||
tones: A list of :class:`Tone` instances representing the
|
||||
open strings (high to low).
|
||||
open strings. By default these are read **low to high**
|
||||
(low string first) — pass ``high_to_low=True`` if your
|
||||
list runs high to low instead.
|
||||
high_to_low: Orientation of this fretboard. When ``False``
|
||||
(the default since v0.43.0), strings and fingerings read
|
||||
low to high; when ``True``, they read high to low (the
|
||||
pre-0.43 behavior).
|
||||
_canonical: Internal flag — when ``True``, *tones* are already
|
||||
in canonical (high-to-low) order and are stored as-is.
|
||||
Used by the instrument presets.
|
||||
"""
|
||||
self.tones = tones
|
||||
self.high_to_low = high_to_low
|
||||
# Internally we always store strings high-to-low; this keeps the
|
||||
# fingering scorer and chord-override tables (which assume that
|
||||
# order) untouched. User-facing access is re-oriented on the way out.
|
||||
if _canonical or high_to_low:
|
||||
self._tones = list(tones)
|
||||
else:
|
||||
self._tones = list(reversed(tones))
|
||||
|
||||
def _orient(self, seq):
|
||||
"""Re-orient a canonical (high-to-low) sequence for display.
|
||||
|
||||
Returns *seq* unchanged when this board reads high-to-low, or
|
||||
reversed when it reads low-to-high. Self-inverse, so it also maps
|
||||
user-supplied (oriented) input back to canonical order.
|
||||
"""
|
||||
return list(seq) if self.high_to_low else list(reversed(seq))
|
||||
|
||||
@property
|
||||
def tones(self) -> list[Tone]:
|
||||
"""The open-string tones in this board's orientation.
|
||||
|
||||
Low-to-high by default; high-to-low when ``high_to_low=True``.
|
||||
"""
|
||||
return self._orient(self._tones)
|
||||
|
||||
@classmethod
|
||||
def _from_canonical(cls, tone_strings, high_to_low: bool = False) -> Fretboard:
|
||||
"""Build a board from canonical (high-to-low) tone-name strings.
|
||||
|
||||
Used by the instrument presets, whose tunings are written in the
|
||||
conventional high-to-low order. *high_to_low* sets only the
|
||||
board's display orientation.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(
|
||||
tones=[Tone.from_string(t, system="western") for t in tone_strings],
|
||||
high_to_low=high_to_low,
|
||||
_canonical=True,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
l = tuple([tone.full_name for tone in self.tones])
|
||||
@@ -1391,7 +1440,11 @@ class Fretboard:
|
||||
Returns:
|
||||
A new Fretboard with all strings raised by ``fret`` semitones.
|
||||
"""
|
||||
return Fretboard(tones=[t.add(fret) for t in self.tones])
|
||||
return Fretboard(
|
||||
tones=[t.add(fret) for t in self._tones],
|
||||
high_to_low=self.high_to_low,
|
||||
_canonical=True,
|
||||
)
|
||||
|
||||
def __iter__(self) -> Iterator[Tone]:
|
||||
"""Iterate over the open-string tones of this fretboard."""
|
||||
@@ -1399,7 +1452,7 @@ class Fretboard:
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of strings on this fretboard."""
|
||||
return len(self.tones)
|
||||
return len(self._tones)
|
||||
|
||||
INSTRUMENTS = [
|
||||
"guitar", "twelve_string", "bass", "ukulele",
|
||||
@@ -1423,84 +1476,76 @@ class Fretboard:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def guitar(cls, tuning: Union[str, tuple[str, ...]] = "standard", capo: int = 0) -> Fretboard:
|
||||
def guitar(cls, tuning: Union[str, tuple[str, ...]] = "standard", capo: int = 0,
|
||||
high_to_low: bool = False) -> Fretboard:
|
||||
"""Guitar with the given tuning and optional capo.
|
||||
|
||||
Args:
|
||||
tuning: Tuning name or tuple of tone strings (high to low).
|
||||
tuning: Tuning name, or a tuple of tone strings. A custom
|
||||
tuple is read **low to high** by default (pass
|
||||
``high_to_low=True`` to give it high to low instead).
|
||||
Built-in tunings: standard, drop d, open g, open d,
|
||||
open e, open a, dadgad, half step down.
|
||||
capo: Fret number for the capo (0 = no capo). Raises all
|
||||
strings by this many semitones.
|
||||
high_to_low: When ``True``, the resulting board reads high to
|
||||
low (pre-0.43 behavior); otherwise low to high.
|
||||
"""
|
||||
from .tones import Tone
|
||||
if isinstance(tuning, str):
|
||||
tuning = cls.TUNINGS[tuning]
|
||||
fb = cls(tones=[Tone.from_string(t, system="western") for t in tuning])
|
||||
# Built-in tunings are defined canonically (high to low).
|
||||
canonical = [Tone.from_string(t, system="western") for t in cls.TUNINGS[tuning]]
|
||||
fb = cls(tones=canonical, high_to_low=high_to_low, _canonical=True)
|
||||
else:
|
||||
# A user-supplied tuple is in the board's orientation.
|
||||
fb = cls(tones=[Tone.from_string(t, system="western") for t in tuning],
|
||||
high_to_low=high_to_low)
|
||||
if capo:
|
||||
fb = fb.capo(capo)
|
||||
return fb
|
||||
|
||||
@classmethod
|
||||
def bass(cls, five_string: bool = False) -> Fretboard:
|
||||
def bass(cls, five_string: bool = False, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard bass guitar tuning.
|
||||
|
||||
Args:
|
||||
five_string: If True, adds a low B string (B0).
|
||||
high_to_low: When ``True``, the board reads high to low.
|
||||
"""
|
||||
from .tones import Tone
|
||||
strings = ["G2", "D2", "A1", "E1"]
|
||||
if five_string:
|
||||
strings.append("B0")
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def ukulele(cls) -> Fretboard:
|
||||
def ukulele(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard ukulele tuning (A4 E4 C4 G4).
|
||||
|
||||
Re-entrant tuning: the G4 string is higher than C4.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("E4", system="western"),
|
||||
Tone.from_string("C4", system="western"),
|
||||
Tone.from_string("G4", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A4", "E4", "C4", "G4"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def mandolin(cls) -> Fretboard:
|
||||
def mandolin(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard mandolin tuning (E5 A4 D4 G3).
|
||||
|
||||
Tuned in fifths, same as a violin but one octave relationship.
|
||||
Strings are typically doubled (paired courses).
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("E5", system="western"),
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("D4", system="western"),
|
||||
Tone.from_string("G3", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["E5", "A4", "D4", "G3"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def mandola(cls) -> Fretboard:
|
||||
def mandola(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard mandola tuning (A4 D4 G3 C3).
|
||||
|
||||
The mandola (or tenor mandola) is to the mandolin what the
|
||||
viola is to the violin — a fifth lower, with a warmer,
|
||||
darker tone. Tuned in fifths like all the mandolin family.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("D4", system="western"),
|
||||
Tone.from_string("G3", system="western"),
|
||||
Tone.from_string("C3", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A4", "D4", "G3", "C3"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def octave_mandolin(cls) -> Fretboard:
|
||||
def octave_mandolin(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Octave mandolin tuning (E4 A3 D3 G2).
|
||||
|
||||
Also called the octave mandola in European terminology.
|
||||
@@ -1508,84 +1553,57 @@ class Fretboard:
|
||||
family's cello-to-violin relationship. Popular in Irish
|
||||
and Celtic folk music.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("E4", system="western"),
|
||||
Tone.from_string("A3", system="western"),
|
||||
Tone.from_string("D3", system="western"),
|
||||
Tone.from_string("G2", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["E4", "A3", "D3", "G2"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def mandocello(cls) -> Fretboard:
|
||||
def mandocello(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Mandocello tuning (A3 D3 G2 C2).
|
||||
|
||||
The bass of the mandolin family. Tuned like a cello — an
|
||||
octave below the mandola. Rare but beautiful; used in
|
||||
mandolin orchestras.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A3", system="western"),
|
||||
Tone.from_string("D3", system="western"),
|
||||
Tone.from_string("G2", system="western"),
|
||||
Tone.from_string("C2", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A3", "D3", "G2", "C2"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def violin(cls) -> Fretboard:
|
||||
def violin(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard violin tuning (E5 A4 D4 G3).
|
||||
|
||||
Tuned in perfect fifths. The violin has no frets — intonation
|
||||
is continuous, allowing vibrato and microtonal inflections
|
||||
not possible on fretted instruments.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("E5", system="western"),
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("D4", system="western"),
|
||||
Tone.from_string("G3", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["E5", "A4", "D4", "G3"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def viola(cls) -> Fretboard:
|
||||
def viola(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard viola tuning (A4 D4 G3 C3).
|
||||
|
||||
A perfect fifth below the violin. The viola's darker, warmer
|
||||
tone comes from its larger body and lower register.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("D4", system="western"),
|
||||
Tone.from_string("G3", system="western"),
|
||||
Tone.from_string("C3", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A4", "D4", "G3", "C3"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def cello(cls) -> Fretboard:
|
||||
def cello(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard cello tuning (A3 D3 G2 C2).
|
||||
|
||||
An octave below the viola. Tuned in fifths. The cello spans
|
||||
the range of the human voice — tenor through bass.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A3", system="western"),
|
||||
Tone.from_string("D3", system="western"),
|
||||
Tone.from_string("G2", system="western"),
|
||||
Tone.from_string("C2", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A3", "D3", "G2", "C2"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def banjo(cls, tuning: Union[str, tuple[str, ...]] = "open g") -> Fretboard:
|
||||
def banjo(cls, tuning: Union[str, tuple[str, ...]] = "open g",
|
||||
high_to_low: bool = False) -> Fretboard:
|
||||
"""Banjo with the given tuning.
|
||||
|
||||
Args:
|
||||
tuning: ``"open g"`` (default, bluegrass) or ``"open d"``
|
||||
(old-time, clawhammer). The 5th string is a high
|
||||
drone — a defining feature of the banjo sound.
|
||||
drone — a defining feature of the banjo sound. A custom
|
||||
tuple is read low to high unless ``high_to_low=True``.
|
||||
high_to_low: When ``True``, the board reads high to low.
|
||||
|
||||
Standard open G: G4 D3 G3 B3 D4 (5th string is the short
|
||||
high G4 drone).
|
||||
@@ -1597,11 +1615,12 @@ class Fretboard:
|
||||
"double c": ("D4", "C4", "G3", "C3", "G4"),
|
||||
}
|
||||
if isinstance(tuning, str):
|
||||
tuning = tunings[tuning]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in tuning])
|
||||
return cls._from_canonical(tunings[tuning], high_to_low)
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in tuning],
|
||||
high_to_low=high_to_low)
|
||||
|
||||
@classmethod
|
||||
def double_bass(cls) -> Fretboard:
|
||||
def double_bass(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard double bass (upright bass) tuning (G2 D2 A1 E1).
|
||||
|
||||
The largest and lowest-pitched bowed string instrument in the
|
||||
@@ -1611,16 +1630,10 @@ class Fretboard:
|
||||
|
||||
The 5-string double bass adds a low B0 or C1.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("G2", system="western"),
|
||||
Tone.from_string("D2", system="western"),
|
||||
Tone.from_string("A1", system="western"),
|
||||
Tone.from_string("E1", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["G2", "D2", "A1", "E1"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def harp(cls) -> Fretboard:
|
||||
def harp(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Concert harp strings — 47 strings spanning C1 to G7.
|
||||
|
||||
The pedal harp has 7 strings per octave (one per note name),
|
||||
@@ -1630,7 +1643,6 @@ class Fretboard:
|
||||
This returns the full set of 47 strings in the default
|
||||
Cb (enharmonic B) tuning.
|
||||
"""
|
||||
from .tones import Tone
|
||||
# 47 strings: C1 to G7, one per diatonic note
|
||||
notes = ["C", "D", "E", "F", "G", "A", "B"]
|
||||
strings = []
|
||||
@@ -1643,30 +1655,33 @@ class Fretboard:
|
||||
else:
|
||||
continue
|
||||
break
|
||||
# Harp strings are high to low
|
||||
# Canonical (high to low)
|
||||
strings.reverse()
|
||||
return cls(tones=[Tone.from_string(s, system="western") for s in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def pedal_steel(cls) -> Fretboard:
|
||||
def pedal_steel(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Pedal steel guitar — E9 Nashville tuning (10 strings).
|
||||
|
||||
The standard tuning for country music. The pedal steel has
|
||||
foot pedals and knee levers that change string pitches during
|
||||
play, enabling its signature swooping, crying sound.
|
||||
"""
|
||||
from .tones import Tone
|
||||
# E9 Nashville tuning (high to low)
|
||||
# E9 Nashville tuning (canonical: high to low)
|
||||
strings = ["F#4", "D#4", "G#3", "E3", "B3", "G#3",
|
||||
"F#3", "E3", "D3", "B2"]
|
||||
return cls(tones=[Tone.from_string(s, system="western") for s in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def bouzouki(cls, variant: Union[str, tuple[str, ...]] = "irish") -> Fretboard:
|
||||
def bouzouki(cls, variant: Union[str, tuple[str, ...]] = "irish",
|
||||
high_to_low: bool = False) -> Fretboard:
|
||||
"""Bouzouki tuning.
|
||||
|
||||
Args:
|
||||
variant: ``"irish"`` (default, GDAD) or ``"greek"`` (CFAD).
|
||||
A custom tuple is read low to high unless
|
||||
``high_to_low=True``.
|
||||
high_to_low: When ``True``, the board reads high to low.
|
||||
|
||||
The Irish bouzouki is a staple of Celtic music, usually tuned
|
||||
in unison or octave pairs. The Greek bouzouki traditionally
|
||||
@@ -1678,11 +1693,12 @@ class Fretboard:
|
||||
"greek": ("D4", "A3", "F3", "C3"),
|
||||
}
|
||||
if isinstance(variant, str):
|
||||
variant = tunings[variant]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in variant])
|
||||
return cls._from_canonical(tunings[variant], high_to_low)
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in variant],
|
||||
high_to_low=high_to_low)
|
||||
|
||||
@classmethod
|
||||
def oud(cls) -> Fretboard:
|
||||
def oud(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard Arabic oud tuning (C4 G3 D3 A2 G2 C2).
|
||||
|
||||
The oud is the ancestor of the European lute and the defining
|
||||
@@ -1691,12 +1707,11 @@ class Fretboard:
|
||||
essential to maqam performance. 6 courses (11 strings),
|
||||
typically tuned in fourths.
|
||||
"""
|
||||
from .tones import Tone
|
||||
strings = ["C4", "G3", "D3", "A2", "G2", "C2"]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def sitar(cls) -> Fretboard:
|
||||
def sitar(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Sitar main playing strings (approximation).
|
||||
|
||||
The sitar typically has 6-7 main strings and 11-13 sympathetic
|
||||
@@ -1706,14 +1721,13 @@ class Fretboard:
|
||||
Main strings: Sa Sa Pa Sa Re Sa Ma (approximated in 12-TET).
|
||||
Represented here as the most common Ravi Shankar school tuning.
|
||||
"""
|
||||
from .tones import Tone
|
||||
# Common Ravi Shankar tuning mapped to Western notes
|
||||
# (sitar is tuned relative to Sa, typically C# or D)
|
||||
strings = ["C4", "C3", "G3", "C3", "D3", "C2", "F2"]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def shamisen(cls) -> Fretboard:
|
||||
def shamisen(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard shamisen tuning — honchoshi (C4 G3 C3).
|
||||
|
||||
The shamisen is a 3-stringed Japanese instrument played with
|
||||
@@ -1723,15 +1737,10 @@ class Fretboard:
|
||||
- niagari (二上り): root-5th-2nd (raises 2nd string)
|
||||
- sansagari (三下り): root-5th-b7th (lowers 3rd string)
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("C4", system="western"),
|
||||
Tone.from_string("G3", system="western"),
|
||||
Tone.from_string("C3", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["C4", "G3", "C3"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def erhu(cls) -> Fretboard:
|
||||
def erhu(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard erhu tuning (A4 D4).
|
||||
|
||||
The erhu is a 2-stringed Chinese bowed instrument with a
|
||||
@@ -1739,14 +1748,10 @@ class Fretboard:
|
||||
— the player presses the strings without touching the neck,
|
||||
allowing continuous pitch bending.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("D4", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A4", "D4"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def charango(cls) -> Fretboard:
|
||||
def charango(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard charango tuning (E5 A4 E5 C5 G4).
|
||||
|
||||
A small Andean stringed instrument, traditionally made from
|
||||
@@ -1754,54 +1759,38 @@ class Fretboard:
|
||||
— the 3rd course (E5) is the highest pitched, creating the
|
||||
charango's bright, sparkling sound.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("E5", system="western"),
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("E5", system="western"),
|
||||
Tone.from_string("C5", system="western"),
|
||||
Tone.from_string("G4", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["E5", "A4", "E5", "C5", "G4"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def pipa(cls) -> Fretboard:
|
||||
def pipa(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard pipa tuning (D4 A3 E3 A2).
|
||||
|
||||
The pipa is a 4-stringed Chinese lute with a pear-shaped
|
||||
body, dating back over 2000 years. Known for its percussive
|
||||
attack and rapid tremolo technique.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("D4", system="western"),
|
||||
Tone.from_string("A3", system="western"),
|
||||
Tone.from_string("E3", system="western"),
|
||||
Tone.from_string("A2", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["D4", "A3", "E3", "A2"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def balalaika(cls) -> Fretboard:
|
||||
def balalaika(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard balalaika prima tuning (A4 E4 E4).
|
||||
|
||||
The Russian balalaika has a distinctive triangular body and
|
||||
3 strings. The two lower strings are tuned in unison — a
|
||||
unique feature that gives it a natural chorus effect.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("E4", system="western"),
|
||||
Tone.from_string("E4", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A4", "E4", "E4"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def keyboard(cls, keys: int = 88, start: str = "A0") -> Fretboard:
|
||||
def keyboard(cls, keys: int = 88, start: str = "A0",
|
||||
high_to_low: bool = False) -> Fretboard:
|
||||
"""Piano or keyboard with the given number of keys.
|
||||
|
||||
Args:
|
||||
keys: Number of keys (default 88 for a full piano).
|
||||
Common sizes: 25, 37, 49, 61, 76, 88.
|
||||
start: The lowest note (default ``"A0"`` for standard piano).
|
||||
high_to_low: When ``True``, the board reads high to low.
|
||||
|
||||
A full 88-key piano spans A0 (27.5 Hz) to C8 (4186 Hz) —
|
||||
the widest range of any standard acoustic instrument.
|
||||
@@ -1815,13 +1804,12 @@ class Fretboard:
|
||||
"""
|
||||
from .tones import Tone
|
||||
start_tone = Tone.from_string(start, system="western")
|
||||
tones = []
|
||||
for i in range(keys - 1, -1, -1):
|
||||
tones.append(start_tone.add(i))
|
||||
return cls(tones=tones)
|
||||
# Built high-to-low (canonical): highest key first, down to `start`.
|
||||
tones = [start_tone.add(i) for i in range(keys - 1, -1, -1)]
|
||||
return cls(tones=tones, high_to_low=high_to_low, _canonical=True)
|
||||
|
||||
@classmethod
|
||||
def lute(cls) -> Fretboard:
|
||||
def lute(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Renaissance lute in G tuning (6 courses).
|
||||
|
||||
The European lute was the dominant instrument of the
|
||||
@@ -1829,21 +1817,19 @@ class Fretboard:
|
||||
a major third between the 3rd and 4th courses — the
|
||||
same intervallic pattern as a modern guitar.
|
||||
"""
|
||||
from .tones import Tone
|
||||
strings = ["G4", "D4", "A3", "F3", "C3", "G2"]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def twelve_string(cls) -> Fretboard:
|
||||
def twelve_string(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""12-string guitar in standard tuning.
|
||||
|
||||
The lower 4 courses are doubled at the octave; the upper 2
|
||||
are doubled in unison. This creates the characteristic
|
||||
shimmering, chorus-like sound.
|
||||
|
||||
Represented as 12 strings (high to low, pairs together).
|
||||
Represented as 12 strings (canonical: high to low, pairs together).
|
||||
"""
|
||||
from .tones import Tone
|
||||
strings = [
|
||||
"E4", "E4", # 1st course (unison)
|
||||
"B3", "B3", # 2nd course (unison)
|
||||
@@ -1852,7 +1838,7 @@ class Fretboard:
|
||||
"A3", "A2", # 5th course (octave)
|
||||
"E3", "E2", # 6th course (octave)
|
||||
]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
def scale_diagram(self, scale, frets: int = 12, chord=None) -> str:
|
||||
"""Render an ASCII diagram showing where scale notes fall on the neck.
|
||||
@@ -1881,10 +1867,24 @@ class Fretboard:
|
||||
>>> am = Chord.from_symbol("Am")
|
||||
>>> print(fb.scale_diagram(pentatonic, frets=5, chord=am))
|
||||
"""
|
||||
scale_notes = set(scale.note_names)
|
||||
# Match notes enharmonically: the fretboard spells tones with
|
||||
# sharps (e.g. D#), but a scale may use flats (e.g. Eb). Compare
|
||||
# via the system's canonical name so Eb and D# count as the same
|
||||
# pitch — and display using the scale's own spelling.
|
||||
_system = self._tones[0].system
|
||||
def _resolve(name):
|
||||
resolved = _system.resolve_name(name)
|
||||
return resolved if resolved is not None else name
|
||||
|
||||
# Map canonical pitch -> the scale's preferred spelling for display.
|
||||
scale_display = {}
|
||||
for n in scale.note_names:
|
||||
scale_display.setdefault(_resolve(n), n)
|
||||
scale_notes = set(scale_display)
|
||||
|
||||
chord_notes = set()
|
||||
if chord is not None:
|
||||
chord_notes = {t.name for t in chord.tones}
|
||||
chord_notes = {_resolve(t.name) for t in chord.tones}
|
||||
|
||||
max_name = max(len(t.name) for t in self.tones)
|
||||
lines = []
|
||||
@@ -1899,13 +1899,15 @@ class Fretboard:
|
||||
fret_marks = []
|
||||
for f in range(frets + 1):
|
||||
note = tone.add(f)
|
||||
if note.name in scale_notes:
|
||||
if chord_notes and note.name in chord_notes:
|
||||
fret_marks.append(f" {note.name.upper():<2s}")
|
||||
key = _resolve(note.name)
|
||||
if key in scale_notes:
|
||||
label = scale_display[key]
|
||||
if chord_notes and key in chord_notes:
|
||||
fret_marks.append(f" {label.upper():<2s}")
|
||||
elif chord_notes:
|
||||
fret_marks.append(f" {note.name.lower():<2s}")
|
||||
fret_marks.append(f" {label.lower():<2s}")
|
||||
else:
|
||||
fret_marks.append(f" {note.name:<2s}")
|
||||
fret_marks.append(f" {label:<2s}")
|
||||
else:
|
||||
fret_marks.append(" - ")
|
||||
line = f"{tone.name:>{max_name}}|{'|'.join(fret_marks)}|"
|
||||
@@ -1927,7 +1929,7 @@ class Fretboard:
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("G")
|
||||
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
|
||||
Fingering(E=3, A=2, D=0, G=0, B=0, e=3)
|
||||
"""
|
||||
from .charts import CHARTS
|
||||
return CHARTS[system][name].fingering(fretboard=self)
|
||||
@@ -1945,7 +1947,7 @@ class Fretboard:
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb["G"]
|
||||
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
|
||||
Fingering(E=3, A=2, D=0, G=0, B=0, e=3)
|
||||
"""
|
||||
return self.chord(name)
|
||||
|
||||
@@ -1964,12 +1966,12 @@ class Fretboard:
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> print(fb.tab("Am"))
|
||||
A minor
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--2--
|
||||
D|--2--
|
||||
E|--x--
|
||||
A|--0--
|
||||
E|--0--
|
||||
D|--2--
|
||||
G|--2--
|
||||
B|--1--
|
||||
e|--0--
|
||||
"""
|
||||
return self.chord(name, system=system).tab()
|
||||
|
||||
@@ -1984,7 +1986,7 @@ class Fretboard:
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> chart = fb.chart()
|
||||
>>> chart["Am7"]
|
||||
Fingering(e=0, B=1, G=0, D=2, A=0, E=0)
|
||||
Fingering(E=0, A=0, D=2, G=0, B=1, e=0)
|
||||
"""
|
||||
from .charts import charts_for_fretboard, CHARTS
|
||||
return charts_for_fretboard(chart=CHARTS[system], fretboard=self)
|
||||
@@ -2009,13 +2011,16 @@ class Fretboard:
|
||||
"""
|
||||
from .charts import Fingering
|
||||
|
||||
if not len(positions) == len(self.tones):
|
||||
if not len(positions) == len(self._tones):
|
||||
raise ValueError(
|
||||
"The number of positions must match the number of tones (strings)."
|
||||
)
|
||||
|
||||
string_names = tuple(t.name for t in self.tones)
|
||||
return Fingering(positions, string_names, fretboard=self)
|
||||
# Positions arrive in this board's orientation; canonicalise them
|
||||
# (high-to-low) to match the internal tone order Fingering expects.
|
||||
string_names = tuple(t.name for t in self._tones)
|
||||
return Fingering(self._orient(positions), string_names, fretboard=self,
|
||||
high_to_low=self.high_to_low)
|
||||
|
||||
|
||||
def analyze_progression(chords: list[Chord], key: str = "C", mode: str = "major") -> list[str | None]:
|
||||
|
||||
+696
-40
@@ -3706,17 +3706,19 @@ class Part:
|
||||
fingering = self._fretboard.chord(chord_name)
|
||||
|
||||
# Get the sounding tones (skips muted strings)
|
||||
tones = fingering.tones # list of Tone objects, high to low
|
||||
tones = fingering.tones
|
||||
|
||||
if not tones:
|
||||
self.rest(duration)
|
||||
return self
|
||||
|
||||
# Order: down strum = low to high (reverse since tones are high-to-low)
|
||||
# Sort by pitch so strum direction is correct regardless of the
|
||||
# fingering's display orientation: down = low to high, up = high to low.
|
||||
low_to_high = sorted(tones, key=lambda t: t.midi)
|
||||
if direction == "down":
|
||||
strum_tones = list(reversed(tones))
|
||||
strum_tones = low_to_high
|
||||
else:
|
||||
strum_tones = list(tones)
|
||||
strum_tones = list(reversed(low_to_high))
|
||||
|
||||
if hasattr(duration, 'value'):
|
||||
total_beats = duration.value
|
||||
@@ -3796,6 +3798,128 @@ class Part:
|
||||
return max(note_beats, drum_beats)
|
||||
return note_beats
|
||||
|
||||
# ── ASCII tablature export ──────────────────────────────────────────
|
||||
|
||||
_TAB_TUNINGS = {
|
||||
"guitar": [40, 45, 50, 55, 59, 64],
|
||||
"bass": [28, 33, 38, 43],
|
||||
"drop_d": [38, 45, 50, 55, 59, 64],
|
||||
}
|
||||
_TAB_LABELS = {
|
||||
"guitar": ["E", "A", "D", "G", "B", "e"],
|
||||
"bass": ["E", "A", "D", "G"],
|
||||
"drop_d": ["D", "A", "D", "G", "B", "e"],
|
||||
}
|
||||
|
||||
def to_tab(self, *, tuning="guitar", frets=24, time_signature=None):
|
||||
"""Generate ASCII guitar/bass tablature from this part's notes.
|
||||
|
||||
Args:
|
||||
tuning: ``"guitar"`` (6-string standard), ``"bass"`` (4-string),
|
||||
``"drop_d"`` (guitar drop D), a ``Fretboard`` object, or a
|
||||
list of MIDI note numbers for custom tuning (low string first).
|
||||
frets: Maximum fret number (default 24).
|
||||
time_signature: A ``TimeSignature`` or ``None`` for 4/4.
|
||||
|
||||
Returns:
|
||||
A multi-line ASCII tablature string.
|
||||
"""
|
||||
if isinstance(tuning, str):
|
||||
open_midis = list(self._TAB_TUNINGS[tuning])
|
||||
labels = list(self._TAB_LABELS[tuning])
|
||||
elif hasattr(tuning, "tones"):
|
||||
# Fretboard object — sort by pitch so we get low-to-high
|
||||
# regardless of the board's display orientation.
|
||||
fb_tones = sorted(tuning.tones, key=lambda t: t.midi)
|
||||
open_midis = [t.midi for t in fb_tones]
|
||||
labels = [t.name if len(t.name) <= 2 else t.name[0] for t in fb_tones]
|
||||
else:
|
||||
open_midis = list(tuning)
|
||||
_note_names = ["C", "C#", "D", "D#", "E", "F",
|
||||
"F#", "G", "G#", "A", "A#", "B"]
|
||||
labels = [_note_names[m % 12] for m in open_midis]
|
||||
|
||||
n_strings = len(open_midis)
|
||||
beats_per_measure = 4.0
|
||||
if time_signature is not None:
|
||||
beats_per_measure = time_signature.beats_per_measure
|
||||
|
||||
# Build columns: each column is a list[str] of length n_strings
|
||||
columns: list[list[str]] = []
|
||||
beat_acc = 0.0
|
||||
|
||||
for note in self.notes:
|
||||
dur_beats = note.duration.value
|
||||
# Insert barline if we've crossed a measure boundary
|
||||
while beat_acc >= beats_per_measure - 0.001:
|
||||
columns.append(["|"] * n_strings)
|
||||
beat_acc -= beats_per_measure
|
||||
|
||||
col = ["---"] * n_strings
|
||||
|
||||
tone = note.tone
|
||||
if tone is None or isinstance(tone, _DrumTone):
|
||||
pass
|
||||
elif hasattr(tone, "tones"):
|
||||
# Chord — assign each chord tone to a different string
|
||||
used: set[int] = set()
|
||||
for ct in tone.tones:
|
||||
midi_val = getattr(ct, "midi", None)
|
||||
if midi_val is None:
|
||||
continue
|
||||
best_s, best_f = self._find_best_string(
|
||||
midi_val, open_midis, frets, used)
|
||||
if best_s is not None:
|
||||
fret_str = str(best_f)
|
||||
col[best_s] = fret_str.center(3, "-")
|
||||
used.add(best_s)
|
||||
else:
|
||||
midi_val = getattr(tone, "midi", None)
|
||||
if midi_val is not None:
|
||||
best_s, best_f = self._find_best_string(
|
||||
midi_val, open_midis, frets, set())
|
||||
if best_s is not None:
|
||||
fret_str = str(best_f)
|
||||
col[best_s] = fret_str.center(3, "-")
|
||||
|
||||
columns.append(col)
|
||||
if not note._hold:
|
||||
beat_acc += dur_beats
|
||||
|
||||
# Trailing barline
|
||||
if columns and columns[-1] != ["|"] * n_strings:
|
||||
while beat_acc >= beats_per_measure - 0.001:
|
||||
columns.append(["|"] * n_strings)
|
||||
beat_acc -= beats_per_measure
|
||||
columns.append(["|"] * n_strings)
|
||||
|
||||
# Build output lines (highest-pitched string first in display)
|
||||
lines: list[str] = []
|
||||
for s_idx in range(n_strings - 1, -1, -1):
|
||||
label = labels[s_idx]
|
||||
parts_str = "".join(c[s_idx] for c in columns)
|
||||
lines.append(f"{label}|{parts_str}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _find_best_string(midi_val, open_midis, max_fret, used):
|
||||
"""Find the best string/fret for a MIDI note.
|
||||
|
||||
Returns (string_index, fret) or (None, None) if unplayable.
|
||||
"""
|
||||
best_s = None
|
||||
best_f = None
|
||||
for s_idx, open_m in enumerate(open_midis):
|
||||
if s_idx in used:
|
||||
continue
|
||||
f = midi_val - open_m
|
||||
if 0 <= f <= max_fret:
|
||||
if best_f is None or f < best_f:
|
||||
best_s = s_idx
|
||||
best_f = f
|
||||
return best_s, best_f
|
||||
|
||||
def __len__(self):
|
||||
return len(self.notes) + len(self._drum_hits)
|
||||
|
||||
@@ -4396,21 +4520,33 @@ class Score:
|
||||
f"L:1/{default_unit}",
|
||||
]
|
||||
|
||||
# Collect voices: default notes first, then named parts (skip drums)
|
||||
# Collect voices: default notes first, then named parts
|
||||
# Skip drum parts and parts with no pitched notes
|
||||
voices: list[tuple[str, list]] = []
|
||||
if self.notes:
|
||||
voices.append(("default", self.notes))
|
||||
for name, part in self.parts.items():
|
||||
if part.is_drums:
|
||||
continue
|
||||
if part.notes:
|
||||
voices.append((name, part.notes))
|
||||
if not part.notes:
|
||||
continue
|
||||
# Skip parts that have no pitched tones (only drum tones / rests)
|
||||
has_pitched = any(
|
||||
n.tone is not None
|
||||
and (hasattr(n.tone, "name") or hasattr(n.tone, "tones"))
|
||||
for n in part.notes
|
||||
)
|
||||
if not has_pitched:
|
||||
continue
|
||||
voices.append((name, part.notes))
|
||||
|
||||
multi = len(voices) > 1
|
||||
|
||||
if multi:
|
||||
for i, (vname, _) in enumerate(voices, 1):
|
||||
lines.append(f"V:{i} name=\"{vname}\"")
|
||||
for i, (vname, notes) in enumerate(voices, 1):
|
||||
clef = self._guess_clef(notes)
|
||||
clef_str = f" clef={clef}" if clef != "treble" else ""
|
||||
lines.append(f"V:{i} name=\"{vname}\"{clef_str}")
|
||||
lines.append(f"K:{key}")
|
||||
for i, (_, notes) in enumerate(voices, 1):
|
||||
lines.append(f"V:{i}")
|
||||
@@ -4436,6 +4572,26 @@ class Score:
|
||||
+ ");\n</script>\n</body></html>\n"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _guess_clef(notes):
|
||||
"""Return 'bass' if most pitched notes are below C4, else 'treble'."""
|
||||
octaves = []
|
||||
for note in notes:
|
||||
tone = note.tone
|
||||
if tone is None or not hasattr(tone, "octave"):
|
||||
continue
|
||||
if hasattr(tone, "tones"):
|
||||
# Chord — use average of chord tones
|
||||
for t in tone.tones:
|
||||
if hasattr(t, "octave") and t.octave is not None:
|
||||
octaves.append(t.octave)
|
||||
elif tone.octave is not None:
|
||||
octaves.append(tone.octave)
|
||||
if not octaves:
|
||||
return "treble"
|
||||
avg = sum(octaves) / len(octaves)
|
||||
return "bass" if avg < 4 else "treble"
|
||||
|
||||
@staticmethod
|
||||
def _tone_to_abc(tone, default_unit):
|
||||
"""Convert a single Tone to an ABC note string."""
|
||||
@@ -4467,6 +4623,24 @@ class Score:
|
||||
|
||||
return f"{abc_acc}{note_char}{oct_str}"
|
||||
|
||||
@staticmethod
|
||||
def _format_dur(multiplier):
|
||||
"""Format an ABC duration multiplier string."""
|
||||
if abs(multiplier - 1) < 0.001:
|
||||
return ""
|
||||
elif abs(multiplier - int(multiplier)) < 0.001:
|
||||
return str(int(multiplier))
|
||||
elif abs(multiplier - 0.5) < 0.001:
|
||||
return "/2"
|
||||
elif abs(multiplier - 0.25) < 0.001:
|
||||
return "/4"
|
||||
elif abs(multiplier - 1.5) < 0.001:
|
||||
return "3/2"
|
||||
else:
|
||||
from fractions import Fraction
|
||||
frac = Fraction(multiplier).limit_denominator(16)
|
||||
return f"{frac.numerator}/{frac.denominator}"
|
||||
|
||||
def _notes_to_abc(self, notes, default_unit, ts,
|
||||
bars_per_line=4):
|
||||
"""Convert a list of Note objects to an ABC body string."""
|
||||
@@ -4476,17 +4650,12 @@ class Score:
|
||||
measure_count = 0
|
||||
|
||||
for note in notes:
|
||||
beats = note.duration.value
|
||||
|
||||
# ABC length multiplier relative to L:1/default_unit
|
||||
# L:1/8 means 1 unit = 0.5 beats (an eighth note)
|
||||
total_beats = note.duration.value
|
||||
unit_beats = 4.0 / default_unit # beats per L unit
|
||||
multiplier = beats / unit_beats
|
||||
|
||||
if note.tone is None:
|
||||
abc_note = "z"
|
||||
elif hasattr(note.tone, "tones"):
|
||||
# Chord: [CEG]
|
||||
chord_notes = [
|
||||
self._tone_to_abc(t, default_unit)
|
||||
for t in note.tone.tones
|
||||
@@ -4495,33 +4664,32 @@ class Score:
|
||||
else:
|
||||
abc_note = self._tone_to_abc(note.tone, default_unit)
|
||||
|
||||
# Format duration multiplier
|
||||
if multiplier == 1:
|
||||
dur_str = ""
|
||||
elif multiplier == int(multiplier):
|
||||
dur_str = str(int(multiplier))
|
||||
elif multiplier == 0.5:
|
||||
dur_str = "/2"
|
||||
elif multiplier == 0.25:
|
||||
dur_str = "/4"
|
||||
elif multiplier == 1.5:
|
||||
dur_str = "3/2"
|
||||
else:
|
||||
# General fraction
|
||||
from fractions import Fraction
|
||||
frac = Fraction(multiplier).limit_denominator(16)
|
||||
dur_str = f"{frac.numerator}/{frac.denominator}"
|
||||
# Split notes longer than one measure into tied pieces
|
||||
remaining = total_beats
|
||||
first_chunk = True
|
||||
while remaining > 0.001:
|
||||
# How much room left in this measure?
|
||||
room = beats_per_measure - beat_in_measure
|
||||
chunk = min(remaining, room) if remaining > room + 0.001 else remaining
|
||||
needs_tie = remaining - chunk > 0.001
|
||||
|
||||
tokens.append(f"{abc_note}{dur_str}")
|
||||
multiplier = chunk / unit_beats
|
||||
dur_str = self._format_dur(multiplier)
|
||||
|
||||
beat_in_measure += beats
|
||||
if beat_in_measure >= beats_per_measure - 0.001:
|
||||
measure_count += 1
|
||||
if measure_count % bars_per_line == 0:
|
||||
tokens.append("|\n")
|
||||
else:
|
||||
tokens.append("|")
|
||||
beat_in_measure -= beats_per_measure
|
||||
tie_str = "-" if needs_tie and abc_note != "z" else ""
|
||||
tokens.append(f"{abc_note}{dur_str}{tie_str}")
|
||||
|
||||
remaining -= chunk
|
||||
beat_in_measure += chunk
|
||||
first_chunk = False
|
||||
|
||||
if beat_in_measure >= beats_per_measure - 0.001:
|
||||
measure_count += 1
|
||||
if measure_count % bars_per_line == 0:
|
||||
tokens.append("|\n")
|
||||
else:
|
||||
tokens.append("|")
|
||||
beat_in_measure -= beats_per_measure
|
||||
|
||||
body = " ".join(tokens)
|
||||
# Clean up trailing/double barlines
|
||||
@@ -4530,6 +4698,494 @@ class Score:
|
||||
body += " |"
|
||||
return body
|
||||
|
||||
# ── LilyPond notation export ─────────────────────────────────────
|
||||
|
||||
def to_lilypond(self, *, title="Untitled", key="C", mode="major"):
|
||||
"""Export the score as a LilyPond source string.
|
||||
|
||||
Args:
|
||||
title: Title for the ``\\header`` block.
|
||||
key: Key signature root (e.g. ``"C"``, ``"D"``, ``"Bb"``).
|
||||
mode: LilyPond mode string (``"major"``, ``"minor"``, etc.).
|
||||
|
||||
Returns:
|
||||
A complete LilyPond source string.
|
||||
"""
|
||||
ts = self.time_signature
|
||||
|
||||
# Collect voices (same filter as to_abc)
|
||||
voices: list[tuple[str, list]] = []
|
||||
if self.notes:
|
||||
voices.append(("default", self.notes))
|
||||
for name, part in self.parts.items():
|
||||
if part.is_drums:
|
||||
continue
|
||||
if not part.notes:
|
||||
continue
|
||||
has_pitched = any(
|
||||
n.tone is not None
|
||||
and (hasattr(n.tone, "name") or hasattr(n.tone, "tones"))
|
||||
for n in part.notes
|
||||
)
|
||||
if not has_pitched:
|
||||
continue
|
||||
voices.append((name, part.notes))
|
||||
|
||||
ly_key = self._tone_name_to_lilypond(key)
|
||||
|
||||
staves = []
|
||||
for vname, notes in voices:
|
||||
clef = self._guess_clef(notes)
|
||||
body = self._notes_to_lilypond(notes, ts)
|
||||
staff = (
|
||||
f' \\new Staff \\with {{ instrumentName = "{vname}" }} {{\n'
|
||||
f" \\clef {clef}\n"
|
||||
f" \\key {ly_key} \\{mode}\n"
|
||||
f" \\time {ts.beats}/{ts.unit}\n"
|
||||
f" \\tempo 4 = {self.bpm}\n"
|
||||
f" {body}\n"
|
||||
f" }}"
|
||||
)
|
||||
staves.append(staff)
|
||||
|
||||
staves_block = "\n".join(staves)
|
||||
|
||||
return (
|
||||
f'\\version "2.24.0"\n'
|
||||
f"\\header {{\n"
|
||||
f' title = "{title}"\n'
|
||||
f"}}\n\n"
|
||||
f"\\score {{\n"
|
||||
f" \\new StaffGroup <<\n"
|
||||
f"{staves_block}\n"
|
||||
f" >>\n"
|
||||
f" \\layout {{ }}\n"
|
||||
f"}}\n"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _tone_name_to_lilypond(name):
|
||||
"""Convert a note name like 'C#', 'Bb', 'F' to LilyPond pitch."""
|
||||
if not name:
|
||||
return "c"
|
||||
letter = name[0].lower()
|
||||
acc = name[1:] if len(name) > 1 else ""
|
||||
ly_acc = (
|
||||
acc.replace("##", "isis")
|
||||
.replace("#", "is")
|
||||
.replace("bb", "eses")
|
||||
.replace("b", "es")
|
||||
)
|
||||
return f"{letter}{ly_acc}"
|
||||
|
||||
@staticmethod
|
||||
def _tone_to_lilypond(tone):
|
||||
"""Convert a single Tone to a LilyPond pitch string (no duration)."""
|
||||
if tone is None:
|
||||
return None
|
||||
if not hasattr(tone, "name") or not hasattr(tone, "octave"):
|
||||
return None
|
||||
|
||||
name = tone.name
|
||||
octave = tone.octave if tone.octave is not None else 4
|
||||
|
||||
letter = name[0].lower()
|
||||
acc = name[1:] if len(name) > 1 else ""
|
||||
ly_acc = (
|
||||
acc.replace("##", "isis")
|
||||
.replace("#", "is")
|
||||
.replace("bb", "eses")
|
||||
.replace("b", "es")
|
||||
)
|
||||
|
||||
# LilyPond: c = C3, c' = C4, c'' = C5, c, = C2, c,, = C1
|
||||
if octave >= 4:
|
||||
oct_str = "'" * (octave - 3)
|
||||
else:
|
||||
oct_str = "," * (3 - octave)
|
||||
|
||||
return f"{letter}{ly_acc}{oct_str}"
|
||||
|
||||
@staticmethod
|
||||
def _beats_to_lilypond_dur(beats):
|
||||
"""Convert a beat count to a LilyPond duration string."""
|
||||
_MAP = {
|
||||
4.0: "1",
|
||||
2.0: "2",
|
||||
1.0: "4",
|
||||
0.5: "8",
|
||||
0.25: "16",
|
||||
3.0: "2.",
|
||||
1.5: "4.",
|
||||
}
|
||||
for ref, ly in _MAP.items():
|
||||
if abs(beats - ref) < 0.001:
|
||||
return ly
|
||||
if abs(beats - 2 / 3) < 0.05:
|
||||
return "4"
|
||||
closest = min(_MAP, key=lambda k: abs(k - beats))
|
||||
return _MAP[closest]
|
||||
|
||||
def _notes_to_lilypond(self, notes, ts, bars_per_line=4):
|
||||
"""Convert a list of Note objects to a LilyPond music body string."""
|
||||
beats_per_measure = ts.beats_per_measure
|
||||
tokens: list[str] = []
|
||||
beat_in_measure = 0.0
|
||||
measure_count = 0
|
||||
|
||||
for note in notes:
|
||||
total_beats = note.duration.value
|
||||
|
||||
if note.tone is None:
|
||||
pitch = None
|
||||
is_rest = True
|
||||
elif hasattr(note.tone, "tones"):
|
||||
chord_pitches = []
|
||||
for t in note.tone.tones:
|
||||
p = self._tone_to_lilypond(t)
|
||||
if p is not None:
|
||||
chord_pitches.append(p)
|
||||
if chord_pitches:
|
||||
pitch = "<" + " ".join(chord_pitches) + ">"
|
||||
is_rest = False
|
||||
else:
|
||||
pitch = None
|
||||
is_rest = True
|
||||
else:
|
||||
p = self._tone_to_lilypond(note.tone)
|
||||
if p is not None:
|
||||
pitch = p
|
||||
is_rest = False
|
||||
else:
|
||||
pitch = None
|
||||
is_rest = True
|
||||
|
||||
remaining = total_beats
|
||||
while remaining > 0.001:
|
||||
room = beats_per_measure - beat_in_measure
|
||||
chunk = min(remaining, room) if remaining > room + 0.001 else remaining
|
||||
needs_tie = remaining - chunk > 0.001
|
||||
|
||||
dur_str = self._beats_to_lilypond_dur(chunk)
|
||||
|
||||
if is_rest or pitch is None:
|
||||
tokens.append(f"r{dur_str}")
|
||||
else:
|
||||
tie_str = "~" if needs_tie else ""
|
||||
tokens.append(f"{pitch}{dur_str}{tie_str}")
|
||||
|
||||
remaining -= chunk
|
||||
beat_in_measure += chunk
|
||||
|
||||
if beat_in_measure >= beats_per_measure - 0.001:
|
||||
measure_count += 1
|
||||
if measure_count % bars_per_line == 0:
|
||||
tokens.append("|\n ")
|
||||
else:
|
||||
tokens.append("|")
|
||||
beat_in_measure -= beats_per_measure
|
||||
|
||||
body = " ".join(tokens)
|
||||
body = body.replace("| |", "|").rstrip("| \n").rstrip()
|
||||
if not body.endswith("|"):
|
||||
body += " |"
|
||||
return body
|
||||
|
||||
# ── MusicXML export ───────────────────────────────────────────────
|
||||
|
||||
def to_musicxml(self, *, title="Untitled"):
|
||||
"""Export the score as a MusicXML string.
|
||||
|
||||
Args:
|
||||
title: Work title embedded in the ``<work-title>`` element.
|
||||
|
||||
Returns:
|
||||
A MusicXML 4.0 partwise document as a pretty-printed XML string.
|
||||
"""
|
||||
import xml.etree.ElementTree as ET
|
||||
import xml.dom.minidom
|
||||
|
||||
DIVISIONS = 4 # divisions per quarter note
|
||||
|
||||
_DUR_MAP = {
|
||||
4.0: ("whole", False),
|
||||
3.0: ("half", True),
|
||||
2.0: ("half", False),
|
||||
1.5: ("quarter", True),
|
||||
1.0: ("quarter", False),
|
||||
0.5: ("eighth", False),
|
||||
0.25: ("16th", False),
|
||||
}
|
||||
|
||||
def _beats_to_divisions(beats):
|
||||
return int(round(beats * DIVISIONS))
|
||||
|
||||
def _best_dur_type(beats):
|
||||
for val, info in _DUR_MAP.items():
|
||||
if abs(beats - val) < 0.001:
|
||||
return info
|
||||
return None
|
||||
|
||||
def _split_into_measures(notes, beats_per_measure):
|
||||
beat_in_measure = 0.0
|
||||
for note in notes:
|
||||
tone = note.tone
|
||||
if tone is not None and not hasattr(tone, "name") and not hasattr(tone, "tones"):
|
||||
tone = None
|
||||
remaining = note.duration.value
|
||||
is_first = True
|
||||
while remaining > 0.001:
|
||||
room = beats_per_measure - beat_in_measure
|
||||
if room < 0.001:
|
||||
room = beats_per_measure
|
||||
beat_in_measure = 0.0
|
||||
chunk = min(remaining, room)
|
||||
needs_tie_start = (remaining - chunk) > 0.001
|
||||
needs_tie_stop = not is_first
|
||||
|
||||
yield (tone, chunk, needs_tie_start, needs_tie_stop,
|
||||
note.velocity, note.articulation)
|
||||
|
||||
remaining -= chunk
|
||||
beat_in_measure += chunk
|
||||
is_first = False
|
||||
|
||||
if beat_in_measure >= beats_per_measure - 0.001:
|
||||
beat_in_measure = 0.0
|
||||
|
||||
def _tone_to_pitch_el(tone):
|
||||
pitch = ET.Element("pitch")
|
||||
name = tone.name
|
||||
letter = name[0].upper()
|
||||
acc_str = name[1:] if len(name) > 1 else ""
|
||||
|
||||
step = ET.SubElement(pitch, "step")
|
||||
step.text = letter
|
||||
|
||||
alter_val = 0
|
||||
if acc_str == "#":
|
||||
alter_val = 1
|
||||
elif acc_str == "##":
|
||||
alter_val = 2
|
||||
elif acc_str == "b":
|
||||
alter_val = -1
|
||||
elif acc_str == "bb":
|
||||
alter_val = -2
|
||||
|
||||
if alter_val != 0:
|
||||
alter = ET.SubElement(pitch, "alter")
|
||||
alter.text = str(alter_val)
|
||||
|
||||
octave_el = ET.SubElement(pitch, "octave")
|
||||
octave_el.text = str(tone.octave if tone.octave is not None else 4)
|
||||
|
||||
return pitch
|
||||
|
||||
def _add_note_el(measure, tone, dur_beats, is_chord_continuation,
|
||||
tie_start, tie_stop, velocity):
|
||||
note_el = ET.SubElement(measure, "note")
|
||||
|
||||
if is_chord_continuation:
|
||||
ET.SubElement(note_el, "chord")
|
||||
|
||||
if tone is None:
|
||||
ET.SubElement(note_el, "rest")
|
||||
elif hasattr(tone, "tones"):
|
||||
ET.SubElement(note_el, "rest")
|
||||
else:
|
||||
note_el.append(_tone_to_pitch_el(tone))
|
||||
|
||||
dur_el = ET.SubElement(note_el, "duration")
|
||||
dur_el.text = str(_beats_to_divisions(dur_beats))
|
||||
|
||||
if tie_stop:
|
||||
tie_s = ET.SubElement(note_el, "tie")
|
||||
tie_s.set("type", "stop")
|
||||
if tie_start:
|
||||
tie_s = ET.SubElement(note_el, "tie")
|
||||
tie_s.set("type", "start")
|
||||
|
||||
dur_info = _best_dur_type(dur_beats)
|
||||
if dur_info:
|
||||
type_el = ET.SubElement(note_el, "type")
|
||||
type_el.text = dur_info[0]
|
||||
if dur_info[1]:
|
||||
ET.SubElement(note_el, "dot")
|
||||
|
||||
if tie_start or tie_stop:
|
||||
notations = ET.SubElement(note_el, "notations")
|
||||
if tie_stop:
|
||||
tied = ET.SubElement(notations, "tied")
|
||||
tied.set("type", "stop")
|
||||
if tie_start:
|
||||
tied = ET.SubElement(notations, "tied")
|
||||
tied.set("type", "start")
|
||||
|
||||
# ── Collect voices ──────────────────────────────────────────
|
||||
voices = []
|
||||
if self.notes:
|
||||
voices.append(("default", self.notes))
|
||||
for name, part in self.parts.items():
|
||||
if part.is_drums:
|
||||
continue
|
||||
if not part.notes:
|
||||
continue
|
||||
has_pitched = any(
|
||||
n.tone is not None
|
||||
and (hasattr(n.tone, "name") or hasattr(n.tone, "tones"))
|
||||
for n in part.notes
|
||||
)
|
||||
if not has_pitched:
|
||||
continue
|
||||
voices.append((name, part.notes))
|
||||
|
||||
if not voices:
|
||||
voices.append(("default", []))
|
||||
|
||||
# ── Build XML tree ──────────────────────────────────────────
|
||||
root = ET.Element("score-partwise")
|
||||
root.set("version", "4.0")
|
||||
|
||||
work = ET.SubElement(root, "work")
|
||||
work_title = ET.SubElement(work, "work-title")
|
||||
work_title.text = title
|
||||
|
||||
part_list = ET.SubElement(root, "part-list")
|
||||
ts = self.time_signature
|
||||
beats_per_measure = ts.beats_per_measure
|
||||
|
||||
for idx, (vname, notes) in enumerate(voices, 1):
|
||||
pid = f"P{idx}"
|
||||
sp = ET.SubElement(part_list, "score-part")
|
||||
sp.set("id", pid)
|
||||
pn = ET.SubElement(sp, "part-name")
|
||||
pn.text = vname
|
||||
|
||||
for idx, (vname, notes) in enumerate(voices, 1):
|
||||
pid = f"P{idx}"
|
||||
part_el = ET.SubElement(root, "part")
|
||||
part_el.set("id", pid)
|
||||
|
||||
clef_type = self._guess_clef(notes)
|
||||
|
||||
chunks = list(_split_into_measures(notes, beats_per_measure))
|
||||
|
||||
beat_in_measure = 0.0
|
||||
measure_num = 1
|
||||
measure_el = ET.SubElement(part_el, "measure")
|
||||
measure_el.set("number", str(measure_num))
|
||||
|
||||
attrs = ET.SubElement(measure_el, "attributes")
|
||||
div_el = ET.SubElement(attrs, "divisions")
|
||||
div_el.text = str(DIVISIONS)
|
||||
time_el = ET.SubElement(attrs, "time")
|
||||
beats_el = ET.SubElement(time_el, "beats")
|
||||
beats_el.text = str(ts.beats)
|
||||
bt_el = ET.SubElement(time_el, "beat-type")
|
||||
bt_el.text = str(ts.unit)
|
||||
clef_el = ET.SubElement(attrs, "clef")
|
||||
sign_el = ET.SubElement(clef_el, "sign")
|
||||
line_el = ET.SubElement(clef_el, "line")
|
||||
if clef_type == "bass":
|
||||
sign_el.text = "F"
|
||||
line_el.text = "4"
|
||||
else:
|
||||
sign_el.text = "G"
|
||||
line_el.text = "2"
|
||||
|
||||
direction = ET.SubElement(measure_el, "direction")
|
||||
dir_type = ET.SubElement(direction, "direction-type")
|
||||
metronome = ET.SubElement(dir_type, "metronome")
|
||||
bu = ET.SubElement(metronome, "beat-unit")
|
||||
bu.text = "quarter"
|
||||
pm = ET.SubElement(metronome, "per-minute")
|
||||
pm.text = str(self.bpm)
|
||||
|
||||
for (tone, dur_beats, tie_start, tie_stop,
|
||||
vel, artic) in chunks:
|
||||
|
||||
if beat_in_measure >= beats_per_measure - 0.001:
|
||||
measure_num += 1
|
||||
measure_el = ET.SubElement(part_el, "measure")
|
||||
measure_el.set("number", str(measure_num))
|
||||
beat_in_measure = 0.0
|
||||
|
||||
if tone is not None and hasattr(tone, "tones"):
|
||||
chord_tones = [
|
||||
t for t in tone.tones
|
||||
if hasattr(t, "name") and hasattr(t, "octave")
|
||||
]
|
||||
if not chord_tones:
|
||||
_add_note_el(measure_el, None, dur_beats, False,
|
||||
tie_start, tie_stop, vel)
|
||||
else:
|
||||
for ci, ct in enumerate(chord_tones):
|
||||
_add_note_el(measure_el, ct, dur_beats,
|
||||
ci > 0, tie_start, tie_stop, vel)
|
||||
else:
|
||||
_add_note_el(measure_el, tone, dur_beats, False,
|
||||
tie_start, tie_stop, vel)
|
||||
|
||||
beat_in_measure += dur_beats
|
||||
|
||||
# ── Serialize ───────────────────────────────────────────────
|
||||
raw = ET.tostring(root, encoding="unicode")
|
||||
doctype = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<!DOCTYPE score-partwise PUBLIC '
|
||||
'"-//Recordare//DTD MusicXML 4.0 Partwise//EN" '
|
||||
'"http://www.musicxml.org/dtds/partwise.dtd">\n'
|
||||
)
|
||||
pretty = xml.dom.minidom.parseString(raw).toprettyxml(indent=" ")
|
||||
lines = pretty.split("\n")
|
||||
if lines and lines[0].startswith("<?xml"):
|
||||
lines = lines[1:]
|
||||
return doctype + "\n".join(lines)
|
||||
|
||||
# ── ASCII tablature export ──────────────────────────────────────────
|
||||
|
||||
def to_tab(self, part_name=None, **kwargs):
|
||||
"""Generate ASCII tablature for a part in this score.
|
||||
|
||||
Args:
|
||||
part_name: Name of the part to tab. If *None*, tabs the first
|
||||
non-drum part that has notes.
|
||||
**kwargs: Passed through to :meth:`Part.to_tab` (e.g.
|
||||
``tuning``, ``frets``, ``time_signature``).
|
||||
|
||||
Returns:
|
||||
An ASCII tablature string.
|
||||
|
||||
Raises:
|
||||
ValueError: If no suitable part is found.
|
||||
"""
|
||||
if "time_signature" not in kwargs:
|
||||
kwargs["time_signature"] = self.time_signature
|
||||
|
||||
if part_name is not None:
|
||||
if part_name not in self.parts:
|
||||
raise ValueError(f"No part named {part_name!r}")
|
||||
return self.parts[part_name].to_tab(**kwargs)
|
||||
|
||||
for name, part in self.parts.items():
|
||||
if part.is_drums:
|
||||
continue
|
||||
if not part.notes:
|
||||
continue
|
||||
has_pitched = any(
|
||||
n.tone is not None and not isinstance(n.tone, _DrumTone)
|
||||
for n in part.notes
|
||||
)
|
||||
if has_pitched:
|
||||
return part.to_tab(**kwargs)
|
||||
|
||||
if self.notes:
|
||||
tmp = Part("_default")
|
||||
tmp.notes = list(self.notes)
|
||||
return tmp.to_tab(**kwargs)
|
||||
|
||||
raise ValueError("No pitched parts with notes found in score")
|
||||
|
||||
def save_midi(self, path, velocity=100):
|
||||
"""Export to Standard MIDI File, measure-aware."""
|
||||
ticks_per_beat = 480
|
||||
|
||||
+113
-33
@@ -482,7 +482,8 @@ def test_fretboard_creation():
|
||||
Tone(name="A", octave=2),
|
||||
Tone(name="E", octave=2),
|
||||
]
|
||||
fretboard = Fretboard(tones=standard_tuning)
|
||||
# Literal is written high-to-low, so declare that orientation.
|
||||
fretboard = Fretboard(tones=standard_tuning, high_to_low=True)
|
||||
assert len(fretboard.tones) == 6
|
||||
assert fretboard.tones[0].full_name == "E4"
|
||||
assert fretboard.tones[-1].full_name == "E2"
|
||||
@@ -505,7 +506,8 @@ def guitar_fretboard():
|
||||
Tone.from_string("A2"),
|
||||
Tone.from_string("E2"),
|
||||
]
|
||||
return Fretboard(tones=tuning)
|
||||
# Literal is written high-to-low, so declare that orientation.
|
||||
return Fretboard(tones=tuning, high_to_low=True)
|
||||
|
||||
|
||||
def test_chord_fingering_c(guitar_fretboard):
|
||||
@@ -1767,28 +1769,32 @@ def test_chord_contains_tone():
|
||||
def test_fretboard_guitar():
|
||||
fb = Fretboard.guitar()
|
||||
assert len(fb) == 6
|
||||
# Low-to-high by default (v0.43.0).
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["E", "B", "G", "D", "A", "E"]
|
||||
assert names == ["E", "A", "D", "G", "B", "E"]
|
||||
# high_to_low=True restores the pre-0.43 order.
|
||||
assert [t.name for t in Fretboard.guitar(high_to_low=True)] == \
|
||||
["E", "B", "G", "D", "A", "E"]
|
||||
|
||||
|
||||
def test_fretboard_guitar_octaves():
|
||||
fb = Fretboard.guitar()
|
||||
octaves = [t.octave for t in fb]
|
||||
assert octaves == [4, 3, 3, 3, 2, 2]
|
||||
assert octaves == [2, 2, 3, 3, 3, 4]
|
||||
|
||||
|
||||
def test_fretboard_bass():
|
||||
fb = Fretboard.bass()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["G", "D", "A", "E"]
|
||||
assert names == ["E", "A", "D", "G"]
|
||||
|
||||
|
||||
def test_fretboard_ukulele():
|
||||
fb = Fretboard.ukulele()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["A", "E", "C", "G"]
|
||||
assert names == ["G", "C", "E", "A"]
|
||||
|
||||
|
||||
def test_fretboard_iter():
|
||||
@@ -1821,8 +1827,9 @@ def test_fretboard_ukulele_fingerings():
|
||||
def test_fretboard_guitar_drop_d():
|
||||
fb = Fretboard.guitar("drop d")
|
||||
assert len(fb) == 6
|
||||
assert fb.tones[-1].name == "D"
|
||||
assert fb.tones[-1].octave == 2
|
||||
# Low-to-high: the dropped low D is now the first string.
|
||||
assert fb.tones[0].name == "D"
|
||||
assert fb.tones[0].octave == 2
|
||||
|
||||
|
||||
def test_fretboard_guitar_open_g():
|
||||
@@ -1840,7 +1847,8 @@ def test_fretboard_guitar_custom_tuple():
|
||||
def test_fretboard_bass_five_string():
|
||||
fb = Fretboard.bass(five_string=True)
|
||||
assert len(fb) == 5
|
||||
assert fb.tones[-1].name == "B"
|
||||
# Low-to-high: the added low B is the first string.
|
||||
assert fb.tones[0].name == "B"
|
||||
|
||||
|
||||
def test_fretboard_tunings_dict():
|
||||
@@ -1852,36 +1860,37 @@ def test_fretboard_tunings_dict():
|
||||
def test_fretboard_mandolin():
|
||||
fb = Fretboard.mandolin()
|
||||
assert len(fb) == 4
|
||||
assert fb.tones[0].name == "E"
|
||||
assert fb.tones[-1].name == "G"
|
||||
# Low-to-high.
|
||||
assert fb.tones[0].name == "G"
|
||||
assert fb.tones[-1].name == "E"
|
||||
|
||||
|
||||
def test_fretboard_violin():
|
||||
fb = Fretboard.violin()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["E", "A", "D", "G"]
|
||||
assert names == ["G", "D", "A", "E"]
|
||||
|
||||
|
||||
def test_fretboard_viola():
|
||||
fb = Fretboard.viola()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["A", "D", "G", "C"]
|
||||
assert names == ["C", "G", "D", "A"]
|
||||
|
||||
|
||||
def test_fretboard_cello():
|
||||
fb = Fretboard.cello()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["A", "D", "G", "C"]
|
||||
assert fb.tones[0].octave == 3
|
||||
assert names == ["C", "G", "D", "A"]
|
||||
assert fb.tones[0].octave == 2
|
||||
|
||||
|
||||
def test_fretboard_banjo():
|
||||
fb = Fretboard.banjo()
|
||||
assert len(fb) == 5
|
||||
assert fb.tones[-1].name == "G" # high drone string
|
||||
assert fb.tones[0].name == "G" # high drone string (now first, low-to-high)
|
||||
|
||||
|
||||
def test_fretboard_banjo_open_d():
|
||||
@@ -1898,46 +1907,49 @@ def test_fretboard_violin_tuned_in_fifths():
|
||||
"""Violin strings should be a perfect 5th apart."""
|
||||
fb = Fretboard.violin()
|
||||
for i in range(len(fb.tones) - 1):
|
||||
interval = fb.tones[i] - fb.tones[i + 1]
|
||||
# Low-to-high: each next string is a 5th higher.
|
||||
interval = fb.tones[i + 1] - fb.tones[i]
|
||||
assert interval == 7, f"Strings {i} and {i+1} not a 5th apart"
|
||||
|
||||
|
||||
def test_fretboard_octave_mandolin():
|
||||
fb = Fretboard.octave_mandolin()
|
||||
assert len(fb) == 4
|
||||
assert fb.tones[0].name == "E"
|
||||
assert fb.tones[0].octave == 4
|
||||
assert fb.tones[-1].name == "E"
|
||||
assert fb.tones[-1].octave == 4
|
||||
|
||||
|
||||
def test_fretboard_mandocello():
|
||||
fb = Fretboard.mandocello()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["A", "D", "G", "C"]
|
||||
assert fb.tones[0].octave == 3
|
||||
assert names == ["C", "G", "D", "A"]
|
||||
assert fb.tones[0].octave == 2
|
||||
|
||||
|
||||
def test_fretboard_double_bass():
|
||||
fb = Fretboard.double_bass()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["G", "D", "A", "E"]
|
||||
assert names == ["E", "A", "D", "G"]
|
||||
|
||||
|
||||
def test_fretboard_double_bass_tuned_in_fourths():
|
||||
fb = Fretboard.double_bass()
|
||||
for i in range(len(fb.tones) - 1):
|
||||
interval = fb.tones[i] - fb.tones[i + 1]
|
||||
# Low-to-high: each next string is a 4th higher.
|
||||
interval = fb.tones[i + 1] - fb.tones[i]
|
||||
assert interval == 5, f"Strings {i} and {i+1} not a 4th apart"
|
||||
|
||||
|
||||
def test_fretboard_harp():
|
||||
fb = Fretboard.harp()
|
||||
assert len(fb) == 47
|
||||
assert fb.tones[0].name == "G"
|
||||
assert fb.tones[0].octave == 7
|
||||
assert fb.tones[-1].name == "C"
|
||||
assert fb.tones[-1].octave == 1
|
||||
# Low-to-high.
|
||||
assert fb.tones[0].name == "C"
|
||||
assert fb.tones[0].octave == 1
|
||||
assert fb.tones[-1].name == "G"
|
||||
assert fb.tones[-1].octave == 7
|
||||
|
||||
|
||||
def test_fretboard_pedal_steel():
|
||||
@@ -1950,7 +1962,7 @@ def test_mandolin_family_fifths():
|
||||
for name in ["mandolin", "mandola", "octave_mandolin", "mandocello"]:
|
||||
fb = getattr(Fretboard, name)()
|
||||
for i in range(len(fb.tones) - 1):
|
||||
interval = fb.tones[i] - fb.tones[i + 1]
|
||||
interval = fb.tones[i + 1] - fb.tones[i]
|
||||
assert interval == 7, f"{name} strings {i},{i+1} not a 5th apart"
|
||||
|
||||
|
||||
@@ -1982,7 +1994,7 @@ def test_fretboard_shamisen():
|
||||
def test_fretboard_erhu():
|
||||
fb = Fretboard.erhu()
|
||||
assert len(fb) == 2
|
||||
assert fb.tones[0] - fb.tones[1] == 7 # tuned in 5ths
|
||||
assert fb.tones[1] - fb.tones[0] == 7 # tuned in 5ths (low-to-high)
|
||||
|
||||
|
||||
def test_fretboard_bouzouki_irish():
|
||||
@@ -2003,8 +2015,8 @@ def test_fretboard_charango():
|
||||
def test_fretboard_balalaika():
|
||||
fb = Fretboard.balalaika()
|
||||
assert len(fb) == 3
|
||||
# Two unison strings
|
||||
assert fb.tones[1].name == fb.tones[2].name
|
||||
# Two unison strings (now the lowest two, low-to-high)
|
||||
assert fb.tones[0].name == fb.tones[1].name
|
||||
|
||||
|
||||
def test_fretboard_lute():
|
||||
@@ -2030,8 +2042,9 @@ def test_keyboard_88():
|
||||
def test_keyboard_25():
|
||||
kb = Fretboard.keyboard(25, "C3")
|
||||
assert len(kb) == 25
|
||||
assert kb.tones[-1].name == "C"
|
||||
assert kb.tones[-1].octave == 3
|
||||
# Low-to-high: the start note is now the first key.
|
||||
assert kb.tones[0].name == "C"
|
||||
assert kb.tones[0].octave == 3
|
||||
|
||||
|
||||
def test_keyboard_custom():
|
||||
@@ -2039,6 +2052,60 @@ def test_keyboard_custom():
|
||||
assert len(kb) == 61
|
||||
|
||||
|
||||
# ── Fingering orientation (low-to-high default, v0.43.0) ─────────────────────
|
||||
|
||||
def test_chord_low_to_high_default():
|
||||
"""By default, chord fingerings read low-to-high (low E first)."""
|
||||
fb = Fretboard.guitar()
|
||||
g = fb.chord("G")
|
||||
assert g.string_names == ("E", "A", "D", "G", "B", "e")
|
||||
assert g.positions == (3, 2, 0, 0, 0, 3)
|
||||
|
||||
|
||||
def test_chord_high_to_low_opt_out():
|
||||
"""high_to_low=True restores the pre-0.43 high-to-low order."""
|
||||
fb = Fretboard.guitar(high_to_low=True)
|
||||
g = fb.chord("G")
|
||||
assert g.string_names == ("e", "B", "G", "D", "A", "E")
|
||||
assert g.positions == (3, 0, 0, 0, 2, 3)
|
||||
|
||||
|
||||
def test_orientation_is_a_reversal():
|
||||
"""The two orientations are exact reverses of each other."""
|
||||
lo = Fretboard.guitar().chord("Am7")
|
||||
hi = Fretboard.guitar(high_to_low=True).chord("Am7")
|
||||
assert lo.positions == tuple(reversed(hi.positions))
|
||||
# ...and identify to the same chord.
|
||||
assert lo.identify() == hi.identify()
|
||||
|
||||
|
||||
def test_manual_fingering_input_orientation():
|
||||
"""Manual fret positions are read in the board's orientation."""
|
||||
lo = Fretboard.guitar()
|
||||
hi = Fretboard.guitar(high_to_low=True)
|
||||
# Same physical G voicing, expressed in each orientation.
|
||||
assert lo.fingering(3, 2, 0, 0, 0, 3) == lo.chord("G")
|
||||
assert hi.fingering(3, 0, 0, 0, 2, 3) == hi.chord("G")
|
||||
|
||||
|
||||
def test_orientation_cache_no_collision():
|
||||
"""The two orientations must not collide in the fingering cache."""
|
||||
lo = Fretboard.guitar().chord("C")
|
||||
hi = Fretboard.guitar(high_to_low=True).chord("C")
|
||||
assert lo.positions != hi.positions
|
||||
assert lo.positions == tuple(reversed(hi.positions))
|
||||
|
||||
|
||||
def test_to_tab_orientation_agnostic():
|
||||
"""to_tab output is identical regardless of board orientation."""
|
||||
from pytheory import Part
|
||||
notes = ["C4", "E4", "G4"]
|
||||
p_lo = Part("test"); [p_lo.add(n) for n in notes]
|
||||
p_hi = Part("test"); [p_hi.add(n) for n in notes]
|
||||
assert p_lo.to_tab(tuning=Fretboard.guitar()) == \
|
||||
p_hi.to_tab(tuning=Fretboard.guitar(high_to_low=True))
|
||||
|
||||
|
||||
# ── Ergonomic integration tests ─────────────────────────────────────────────
|
||||
|
||||
def test_ergonomic_workflow():
|
||||
@@ -3455,6 +3522,19 @@ def test_scale_diagram():
|
||||
assert len(lines) == 7
|
||||
|
||||
|
||||
def test_scale_diagram_enharmonic_flat_note():
|
||||
"""A flat-spelled scale note (e.g. the blues Eb) must render even
|
||||
though the fretboard spells that pitch as D#."""
|
||||
fb = Fretboard.guitar()
|
||||
blues = TonedScale(tonic="A4", system="blues")["blues"]
|
||||
assert "Eb" in blues.note_names
|
||||
diagram = fb.scale_diagram(blues, frets=12)
|
||||
# The blue note shows up using the scale's own (flat) spelling,
|
||||
# never the fretboard's sharp spelling.
|
||||
assert "Eb" in diagram
|
||||
assert "D#" not in diagram
|
||||
|
||||
|
||||
# ── Coverage gap tests ─────────────────────────────────────────────────────
|
||||
|
||||
def test_tone_init_octave_parsed_from_name():
|
||||
|
||||
Reference in New Issue
Block a user