Skip to main content

a Python-based set of utilities for just intonation (JI) pitch and pitch collection research and analysis

Project description

jitools — Python Utilities for JI

Introduction

jitools is a Python-based set of utilities for just intonation (JI) pitch and pitch collection research and analysis. It may also be incorporated into workflows for computer-assisted algorithmic composition.

jitools shares some functionalities with Thomas Nicholson's JavaScript-based online resource, the Plainsound Harmonic Space Calculator.

jitools works on Python 3.10+.

Just Intonation

JI is a musical model wherein the intervals between pitches are, as best as possible, tuned as small natural number frequency ratios. Aside from this basic tenet, there are no restrictions on the aesthetic or style of JI music. That said, music in JI often has certain tendencies that highlight or enable its very precise tuning.

JI has its own particular set of analytical concerns for composers, music theorists, and musicians. Many of these issues are well-described in the 2018 paper "Fundamental Principles of Just Intonation and Microtonal Composition" by Thomas Nicholson and Marc Sabat. This paper is essential reading for anyone interested in JI and for anyone who wants to use or understand jitools.

Installation

pip3 install jitools

jitools.Pitch()

In JI pitches are conceptualized as frequency ratios, which are often expressed as fractions with respect to some known reference pitch. The reference pitch is, by convention, labeled as 1/1. Any pitch can function as 1/1, its frequency just needs to be known.

All other pitches are then labeled according to their frequency ratio relationship to 1/1. For example, if 1/1 = A4 = 440Hz, then the frequency of 3/2 would be 440 * 3/2 = 660Hz. But, if 1/1 = G4 = 392Hz, then the frequency of 3/2 would be 392 * 3/2 = 588Hz.

The most essential class in jitools is jitools.Pitch(), which holds information about any single pitch. The principal argument of the class is p, a tuple consisting of two positive integers that represent the numerator and denominator of the pitch's ratio. Since the ratio only has meaning with respect to a known reference pitch, when creating an instance of jitools.Pitch() the user may optionally define the letter-name pitch rp and frequency rf of 1/1. The default values are "A4" and 440 (Hz).

After defining an instance of jitools.Pitch() in terms of its ratio, the class automatically calculates many attributes relevant for JI analysis. These values can later be referenced by the user. Here are a few examples of such attributes:

>>> import jitools
>>> test_pitch = jitools.Pitch(p=(3, 2))
>>> test_pitch.freq
660.0
>>> test_pitch.keynum
76.01955000865388
>>> test_pitch.distance_in_cents_from_reference
701.955000865388

Here is the same information about 3/2, but with 1/1 defined as G4 = 392Hz:

>>> test_pitch = jitools.Pitch(p=(3, 2), rp="G4", rf=392)
>>> test_pitch.freq
588.0
>>> test_pitch.keynum
74.01955000865388
>>> test_pitch.distance_in_cents_from_reference
701.955000865388

JI pitches usually deviate from a nearby 12-tone equal temperament (12-ED2) pitch by some number of cents (1 cent = 1/100 of a 12-ED2 semitone or 1/1200 of an octave), a measure developed by Alexander J. Ellis. Knowing a JI pitch's "cent deviation" is useful for comparing it to its closest 12-ED2 counterpart:

>>> test_pitch = jitools.Pitch(p=(4, 7))
>>> test_pitch.letter_name_and_octave_and_cents
'B3 +31.17409'

Notation

There are various methods for JI pitch notation, including Ben Johnston's well-known system and Sagittal notation, among others. Perhaps the foremost JI pitch notation system in wide use today is the Extended Helmholtz-Ellis JI Pitch Notation (HEJI), originally developed by Marc Sabat and Wolfgang von Schweinitz in the early 2000s, and revised in 2020 by Sabat and Thomas Nicholson in collaboration with Schweinitz, Catherine Lamb, and myself. The revised version is known as HEJI2.

In HEJI/HEJI2, each prime factor of a frequency ratio is denoted with a distinctive accidental glyph. These accidentals appear, alone or in various combinations, in front of letter-name notes on a conventional 5-line musical staff. Musicians familiar with the notation can then interpret the ratios and produce the desired sounds.

The HEJI2 font is available as a cross-platform free download here. Once installed the HEJI2 fonts may be used with any modern music notation program. The glyphs are mapped to ordinary keyboard characters and may be typed. Accidentals and combinations of accidentals may be stored as text strings.

jitools.Pitch() handles the creation of these HEJI2 text strings based on the provided ratio and reference pitch information, and also assigns the correct letter-name pitch. This hastens the translation from ratio-based thinking to HEJI2 notation. One may copy-and-paste the strings while using the HEJI2 font in a notation program, for example. The .notation attribute stores a duple consisting of the HEJI2 text string and letter-name pitch:

>>> test_pitch = jitools.Pitch(p=(25, 13))
>>> test_pitch.notation
('9t', 'G')

Print and Write to File

Since attributes of jitools.Pitch() can be opaque and difficult to get at, detailed reports about a pitch's attributes can be printed to the console in an easy-to-read format:

>>> test_pitch = jitools.Pitch(p=(17, 11))
>>> test_pitch.print_info()

BASIC INFO
ratio: 17/11
monzo: [0, 0, 0, 0, -1, 0, 1]
constituent primes: [11, 17]
frequency (Hz): 680.0
MIDI key number: 76.53637
distance from 1/1 (cents): 753.63747
harmonic distance: 7.54689
Helmholtz-Ellis notation (text string, letter name): (':5v', 'E')
12-ED2 pitch and cent deviation: F5 -46.36253

Such reports can also be written to txt files. By default files are written to the user's current working directory, although a custom output_path can also be provided:

>>> test_pitch = jitools.Pitch(p=(17, 11))
>>> test_pitch.write_info_to_txt()
>>> test_pitch.write_info_to_txt(output_path="/path/to/file/myfile.txt")
>>> test_pitch.write_info_to_txt(output_path="/path/to/file/myfile.txt", verbose=True)
file written to /path/to/file/myfile.txt

jitools.PitchCollection()

The second essential class in jitools is jitools.PitchCollection(). This class allows for collections of jitools.Pitch() instances — which can be regarded as chords, scales, aggregates, or gamuts — to be collectively analyzed as a group.

jitools.PitchCollection() takes pc as its principal argument, a list of two-element tuples. Each tuple represents the ratio of a single pitch in the collection. As with jitools.Pitch(), a letter-name reference pitch rp and reference frequency rf may be optionally defined, otherwise the default values of "A4" and 440 (Hz) are used.

As with jitools.Pitch(), a jitools.PitchCollection() instance stores several important attributes about the pitch collection that may be directly referred to by the user:

>>> test_chord = jitools.PitchCollection([(1, 1), (5, 4), (3, 2)])
>>> test_chord.ratios
[Fraction(1, 1), Fraction(5, 4), Fraction(3, 2)]
>>> test_chord.freqs
[440.0, 550.0, 660.0]
>>> test_chord.keynums
[69.0, 72.86313713864834, 76.01955000865388]
>>> test_chord.intervals
[Fraction(6, 5), Fraction(5, 4), Fraction(3, 2)]

One may also print information about a pitch collection to the console in an easy-to-read format:

>>> test_chord = jitools.PitchCollection([(7, 8), (9, 7), (13, 8), (11, 6)])
>>> test_chord.print_info()

BASIC INFO
ratios: ['7/8', '9/7', '13/8', '11/6']
frequencies (Hz): ['385.0', '565.71429', '715.0', '806.66667']
MIDI key numbers: ['66.68826', '73.35084', '77.40528', '79.49363']
Helmholtz-Ellis notations (text string, letter name): [('<', 'G'), ('>v', 'C'), ('0v', 'F'), ('4', 'G')]
12-ED2 pitch and cent deviations: ['G4 -31.17409', 'C#/Db5 +35.0841', 'F5 +40.52766', 'G5 +49.36294']
harmonic constellation: 147:216:273:308
sequential intervals: ['72/49', '91/72', '44/39']
normalized ratios: ['9/7', '13/8', '7/4', '11/6']
inversion: ['7/8', '77/78', '539/432', '11/6']

The above is the "basic" information about a pitch collection, which is the default type returned when printing (or writing to txt, see below). Various other information types can also be printed or written to txt:

>>> test_chord = jitools.PitchCollection([(7, 8), (9, 7), (13, 8), (11, 6)])
>>> test_chord.print_info("quantitative")

QUANTITATIVE INFO
average ratio: 59/42
minimum ratio: 7/8
maximum ratio: 11/6
ratio span: 44/21
average frequency (Hz): 618.09524
minimum frequency (Hz): 385.0
maximum frequency (Hz): 806.66667
frequency span (Hz): 421.66667
average MIDI key number: 74.88391
minimum MIDI key number: 66.68826
maximum MIDI key number: 79.49363
MIDI key number span: 12.80537
span in cents: 1280.53704

>>> test_chord.print_info("analytic")

ANALYTIC INFO
all intervals: ['44/39', '91/72', '77/54', '72/49', '13/7', '44/21']
tuneable intervals: ['13/7']
periodicity pitch (Hz): 2.61905
least common partial (of periodicity pitch): 1513512
least common partial frequency (Hz): 3963960.0
constituent primes: [2, 3, 7, 11, 13]
harmonic distance sum: 24.52947
average harmonic distance: 6.13237
harmonic intersection: 4391/252252 (0.01741)
harmonic disjunction: 247861/252252 (0.98259)

>>> test_chord.print_info("normalized")

NORMALIZED INFO
ratios: ['9/7', '13/8', '7/4', '11/6']
frequencies (Hz): ['565.71429', '715.0', '770.0', '806.66667']
MIDI key numbers: ['73.35084', '77.40528', '78.68826', '79.49363']
Helmholtz-Ellis notations (text string, letter name): [('>v', 'C'), ('0v', 'F'), ('<', 'G'), ('4', 'G')]
12-ED2 pitch and cent deviations: ['C#/Db5 +35.0841', 'F5 +40.52766', 'G5 -31.17409', 'G5 +49.36294']
harmonic constellation: 216:273:294:308
sequential intervals: ['91/72', '14/13', '22/21']
inversion: ['9/7', '66/49', '132/91', '11/6']

>>> test_chord.print_info("inversion")

INVERSION INFO
ratios: ['7/8', '77/78', '539/432', '11/6']
frequencies (Hz): ['385.0', '434.35897', '548.98148', '806.66667']
MIDI key numbers: ['66.68826', '68.77661', '72.83105', '79.49363']
Helmholtz-Ellis notations (text string, letter name): [('<', 'G'), ('94<e', 'A'), ('4,e', 'D'), ('4', 'G')]
12-ED2 pitch and cent deviations: ['G4 -31.17409', 'A4 -22.33881', 'C#/Db5 -16.89525', 'G5 +49.36294']
harmonic constellation: 4914:5544:7007:10296
sequential intervals: ['44/39', '91/72', '72/49']
normalized ratios: ['539/432', '7/4', '11/6', '77/39']

>>> test_chord.print_info("resultants")

FIRST-ORDER DIFFERENCE TONES
ratios: ['5/24', '19/56', '23/56', '23/42', '3/4', '23/24']
tuneable ratios (vs. any ratio from original chord): ['5/24', '3/4']
frequencies (Hz): ['91.66667', '149.28571', '180.71429', '240.95238', '330.0', '421.66667']
MIDI key numbers: ['41.84359', '50.28687', '53.59448', '58.57493', '64.01955', '68.26319']
Helmholtz-Ellis notations (text string, letter name): [('u', 'F'), ('/>', 'D'), ('3>v', 'E'), ('3>v', 'A'), ('n', 'E'), ('3v', 'G')]
12-ED2 pitch and cent deviations: ['F#/Gb2 -15.64129', 'D3 +28.68711', 'F#/Gb3 -40.55156', 'B3 -42.50656', 'E4 +1.955', 'G#/Ab4 +26.31935']

FIRST-ORDER SUMMATION TONES
ratios: ['121/56', '5/2', '65/24', '163/56', '131/42', '83/24']
tuneable ratios (vs. any ratio from original chord): ['5/2', '65/24']
frequencies (Hz): ['950.71429', '1100.0', '1191.66667', '1280.71429', '1372.38095', '1521.66667']
MIDI key numbers: ['82.3381', '84.86314', '86.24886', '87.49648', '88.69327', '90.48092']
Helmholtz-Ellis notations (text string, letter name): [('44>', 'A'), ('u', 'C'), ('0u', 'D'), ('undefined', 'undefined'), ('undefined', 'undefined'), ('undefined', 'undefined')]
12-ED2 pitch and cent deviations: ['A#/Bb5 +33.80998', 'C#/Db6 -13.68629', 'D6 +24.88637', 'D#/Eb6 +49.64788', 'F6 -30.67331', 'F#/Gb6 +48.09232']

>>> test_chord.print_info("reference")

REFERENCE INFO
reference pitch (1/1): A4
reference key number: 69
reference frequency: 440.0 Hz

jitools.PitchCollection() information may be written to file as txt or csv:

>>> test_chord = jitools.PitchCollection([(9, 4), (15, 48), (21, 17)])
>>> test_chord.write_info_to_txt()
>>> test_chord.write_info_to_csv()

Enharmonic Search

Another functionality of jitools.Pitch() is enharmonic search. Enharmonics in JI are two rational pitches that are extremely close to each other in terms of pitch height -- generally within about 4 cents or less -- so close that the difference between their pitch heights cannot be perceived by ear, or at worst can barely be perceived in a harmonic context (see Nicholson/Sabat, p. 16-19).

Enharmonic assessment can be useful for a variety of purposes, particularly when one has arrived at an extremely complex ratio that is unfamiliar or cumbersome to notate/interpret. Often, one or more simpler nearby ratios are available as alternatives.

Enharmonic information may be generated and stored as a list via get_enharmonics():

>>> test_pitch = jitools.Pitch((711, 184))
>>> test_pitch.get_enharmonics()
[[Fraction(800, 207), 0.27053, 17.337343147274048, Fraction(6399, 6400)],
 [Fraction(2816, 729), -0.58462, 20.969206622964233, Fraction(518319, 518144)],
 ...]

As a list this information is a little opaque, so enharmonics may also be printed to the console in a readable format, or written to txt or csv files:

>>> test_pitch = jitools.Pitch((711, 184))
>>> test_pitch.print_enharmonics_info()

ORIGINAL PITCH INFO
ratio: 711/184
monzo: [-3, 2, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
constituent primes: [2, 3, 23, 79]
frequency (Hz): 1700.21739
MIDI key number: 92.40173
distance from 1/1 (cents): 2340.17255
harmonic distance: 16.99727
Helmholtz-Ellis notation (text string, letter name): ('undefined', 'undefined')
12-ED2 pitch and cent deviation: G#/Ab6 +40.17255

ENHARMONIC SELECTION CRITERIA
tolerance (cents): 1.95
prime limit: 23
maximum number of HEJI symbols: 2
maximum mumber of candidates: 10
sorted by: tolerance
total number of qualifying candidates: 6

ENHARMONIC NO. 1
ratio: 800/207
monzo: [5, -2, 2, 0, 0, 0, 0, 0, -1]
constituent primes: [2, 3, 5, 23]
frequency (Hz): 1700.48309
MIDI key number: 92.40443
distance from 1/1 (cents): 2340.44308
harmonic distance: 17.33734
Helmholtz-Ellis notation (text string, letter name): ('6l', 'A')
12-ED2 pitch and cent deviation: G#/Ab6 +40.44308
melodic interval from 711/184: 6399:6400
enharmonic interval size (cents): +0.27053

ENHARMONIC NO. 2
ratio: 2816/729
monzo: [8, -6, 0, 0, 1]
constituent primes: [2, 3, 11]
frequency (Hz): 1699.64335
MIDI key number: 92.39588
distance from 1/1 (cents): 2339.58794
harmonic distance: 20.96921
Helmholtz-Ellis notation (text string, letter name): ('4e', 'A')
12-ED2 pitch and cent deviation: G#/Ab6 +39.58794
melodic interval from 711/184: 518319:518144
enharmonic interval size (cents): -0.58462

... (4 more)

>>> test_pitch.write_enharmonics_info_to_txt()
>>> test_pitch.write_enharmonics_info_to_csv()

Various constraints on an enharmonic search may be customized, including:

  • tolerance: how close the enharmonic must be to the original pitch, in cents (default = 1.95)
  • limit: maximum prime factor allowed (default = 23)
  • exclude_primes: prime factors to be excluded, as a list (default = [])
  • max_symbols: maximum number of HEJI2 symbols (default = 2)
  • max_candidates: maximum number of results to return (default = 10)
  • lookup_table_path: path to a custom lookup table CSV (default = None, uses the table shipped with the library)

The sort_by parameter can also be changed. The default is "tolerance", which orders results by how closely they match the pitch height of the original pitch. Results may also be sorted by "harmonic distance", a measure developed by James Tenney which generally correlates to interval/ratio simplicity. (See Nicholson/Sabat, p. 26-28, for more information about harmonic distance and other metrics invented by Tenney.)

In the example below, the same original pitch is used as above, but with more restricted tolerance and prime factors. Increasing the maximum allowed number of HEJI2 symbols yields two 3-symbol enharmonics, sorted by harmonic distance:

>>> test_pitch = jitools.Pitch((711, 184))
>>> test_pitch.print_enharmonics_info(tolerance=0.5, limit=17, exclude_primes=[7, 23], max_symbols=3, sort_by="harmonic distance")

ORIGINAL PITCH INFO
ratio: 711/184
monzo: [-3, 2, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
constituent primes: [2, 3, 23, 79]
frequency (Hz): 1700.21739
MIDI key number: 92.40173
distance from 1/1 (cents): 2340.17255
harmonic distance: 16.99727
Helmholtz-Ellis notation (text string, letter name): ('undefined', 'undefined')
12-ED2 pitch and cent deviation: G#/Ab6 +40.17255

ENHARMONIC SELECTION CRITERIA
tolerance (cents): 0.5
prime limit: 17
excluded primes: [7, 23]
maximum number of HEJI symbols: 3
maximum mumber of candidates: 10
sorted by: harmonic distance
total number of qualifying candidates: 2

ENHARMONIC NO. 1
ratio: 85/22
monzo: [-1, 0, 1, 0, -1, 0, 1]
constituent primes: [2, 5, 11, 17]
frequency (Hz): 1700.0
MIDI key number: 92.39951
distance from 1/1 (cents): 2339.95118
harmonic distance: 10.86882
Helmholtz-Ellis notation (text string, letter name): (':5U', 'G')
12-ED2 pitch and cent deviation: G#/Ab6 +39.95118
melodic interval from 711/184: 7821:7820
enharmonic interval size (cents): -0.22137

ENHARMONIC NO. 2
ratio: 8450/2187
monzo: [1, -7, 2, 0, 0, 2]
constituent primes: [2, 3, 5, 13]
frequency (Hz): 1700.04572
MIDI key number: 92.39998
distance from 1/1 (cents): 2339.99775
harmonic distance: 24.13947
Helmholtz-Ellis notation (text string, letter name): ('00t', 'A')
12-ED2 pitch and cent deviation: G#/Ab6 +39.99775
melodic interval from 711/184: 1554957:1554800
enharmonic interval size (cents): -0.17481

Generating a Custom Lookup Table

The enharmonic search uses a prebuilt CSV table that ships with the library. You can generate a custom table using jitools.generate_enharmonic_lookup_table() — for example, to extend the symbol limit, restrict the prime range, or save the results for repeated use:

>>> results = jitools.generate_enharmonic_lookup_table(
...     max_symbols=2,
...     max_prime_3=70,
...     max_prime_5=4,
...     output_path="/path/to/my_table.csv",
...     verbose=True
... )

The generated CSV can then be passed to any enharmonic method via lookup_table_path:

>>> test_pitch = jitools.Pitch(p=(81, 64))
>>> test_pitch.print_enharmonics_info(tolerance=5, lookup_table_path="/path/to/my_table.csv")

Parameters for generate_enharmonic_lookup_table():

  • max_symbols: maximum number of accidental characters (default = 3)
  • max_prime_3: search bound for the prime-3 exponent, range is ±max_prime_3 (default = 70)
  • max_prime_5: search bound for the prime-5 exponent, range is ±max_prime_5 (default = 4)
  • output_path: if provided, write results to a CSV file at this path
  • workers: number of worker processes (default = cpu_count − 1; pass workers=1 to disable multiprocessing)
  • verbose: print progress to stdout (default = False)

State of the Project

jitools is actively maintained and continuously refined. See CHANGELOG.md for a full history of changes.

Areas for future development:

  • auto-generated API reference documentation
  • additional JI tools and features

A Jupyter notebook tutorial covering the full API is available in examples/jitools_tutorial.ipynb.

Feel free to contact me if you have any feedback, suggestions, or requests.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jitools-1.1.0.tar.gz (1.8 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jitools-1.1.0-py3-none-any.whl (1.9 MB view details)

Uploaded Python 3

File details

Details for the file jitools-1.1.0.tar.gz.

File metadata

  • Download URL: jitools-1.1.0.tar.gz
  • Upload date:
  • Size: 1.8 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for jitools-1.1.0.tar.gz
Algorithm Hash digest
SHA256 67dc901cd47d0b826709089af12396d5dddf9f2c7f394741d166f55371c623a7
MD5 ddbac239f72df1cd9b9f14a49a1edb0d
BLAKE2b-256 6367d134416316c406148fd09c1e228174086b17d91b011af29cb05993f65d35

See more details on using hashes here.

File details

Details for the file jitools-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: jitools-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 1.9 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for jitools-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 77816da8dc83dc3a59681618454c26ee0054727492e8c6e48b196c9123fc74e6
MD5 f7ca658ef7174a628fce70e37fe12444
BLAKE2b-256 305f702463023c23fb95a8e14bfdc15cc1a72f35627302951276283e4cd70271

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page