I think I’ve finally succeeded in completing a Python script to generate the .tsv
values that the Striso editor expects to be pasted in from the intervals the tetrachord is divided into and the knowledge that it’s the Greater Perfect System
It’s very jank (no passing arguments from the terminal, just change the function calls in main()
to what you want, lol), but I think it’s finally generating correct output
If anyone sees any errors, I’d really appreciate them being pointed out so I can fix them
Along the way, I ended up writing functions to give useful info on various things, most importantly a table (most nicely formatted if you pass format="adoc"
, but then you have to compile with asciidoctor
) which lists all the intervals possible with 4 steps or fewer given those tetrachord divisions and the structure of the Greater Perfect System
The informational output is in Woolhouse Units (1\730edo
), but you can change the function call to cents()
to change that
Oh, and does anyone else find it easier to think about adding inverted ratios rather than subtracting? Maybe it’s just because that’s how one does it by hand, but I don’t believe the script ever uses /
for subtracting intervals, but rather * inv()
"""
This script outputs the contents of a `.tsv` tuning file for use in the Striso of the Ancient Greek Greater Perfect System
Supplementary tetrachords:
syn: going up from the mese starting with the low interval (only this is officially part of the Greater Perfect System)
down-syn: going down from the paramese starting with the characteristic interval
anti-syn: going down from the trite hyperbolaion starting with the low interval followed by 9/8 followed by the characteristic interval
up-anti-syn: going up from the nete diezeugmenon starting with 9/8 followed by the low interval
All functions and objects in this script — even those based on real algorithms — are entirely misguided.
All snippets are copy/pasted — poorly.
The following program contains magic numbers, unhandled errors, useless comments, pointless bloat, and ungeneralized "solutions" and due to its content it should not be viewed by anyone.
Instructions for Generating a Striso Tuning File:
1. Go to the bottom of the file to main()
2. Set the values under CONFIGURE TUNING
3. Go down to CONFIGURE OUTPUT and uncomment the functions under "Output tuning file"
4. Comment out the rest of the functions under CONFIGURE OUTPUT
5. Save and run the script, piping the output to an empty `.tsv` file or to your clipboard
6. The output should be pasteable into the Striso layout editor
"""
from fractions import Fraction
from math import log2
from sympy.ntheory import factorint
from sympy import primerange
from typing import Optional
def inv(n: Fraction) -> Fraction:
"""
Inverts a fraction to allow more intuitive division
"""
return Fraction(n.denominator, n.numerator)
def cents(n: Fraction) -> float:
"""
Converts an interval ratio to cents (1\\1200edo) from the unison
"""
return log2(n) * 1200
def woolhouse(n: Fraction) -> float:
"""
Converts an interval ratio to woolhouse units (1\\730edo) from the unison
"""
return log2(n) * 730
def color_notation(n: Fraction) -> Optional[str]:
"""
Converts an interval ratio to color notation
Returns None if there are primes present too high for it to handle (> 31)
"""
# list of prime numbers
primes: list[int] = list(primerange(32))
# pseudoedomapping:
# [0¢-50¢), [50¢-250¢), [250¢, 450¢), [450¢, 600¢), [600¢, 750¢), [750¢, 950¢), [950¢, 1150¢), [1150¢, 1200¢]
pseudoedomapping: list[int] = [7, 11, 16, 20, 24, 26, 29, 30, 32, 34, 37]
# list of color abbreviations of form tuple(over, under)
colors: list[Optional[tuple[str]]] = [None, None, ("y", "g"), ("z", "r"), ("1o", "1u"), ("3o", "3u"), ("17o", "17u"), ("19o", "19u"), ("23o", "23u"), ("29o", "29u"), ("31o", "31u")]
# find monzo(n) (list of degrees of prime factors in increasing order)
monzo: list[int] = []
num_monzo: dict = factorint(n.numerator)
den_monzo: dict = factorint(n.denominator)
for i in primes:
monzo.append(0)
for i in range(len(primes)):
# pop and add to the monzo the degree the prime factor is raised to for the ith prime of the numerator
j: Optional[int] = num_monzo.pop(primes[i], None)
if j is not None:
monzo[i] = monzo[i] + j
# pop and subtract from the monzo the degree the prime factor is raised to for the ith prime of the denominator
k: Optional[int] = den_monzo.pop(primes[i], None)
if k is not None:
monzo[i] = monzo[i] - k
# primes are too high to handle
if num_monzo != {} or den_monzo != {}:
return None
# get the color
color: str = ""
# iterate over monzo backwards, skipping monzo[0] and monzo[1] (in Color Notation, higher primes are notated first)
for i in range(len(monzo) - 1, 1, -1):
if monzo[i] < 0:
for j in range(monzo[i] * -1):
# append under color to color
color = "".join([color, colors[i][1]])
if monzo[i] > 0:
for j in range(monzo[i]):
# append over color to color
color = "".join([color, colors[i][0]])
# assign color of wa if there are no higher primes
if color == "":
color = "w"
# find the stepspan: for each entry of the monzo, multiply by the corresponding entry in the pseudoedomapping, and return the sum of the results
stepspan: int = 0
for i in range(len(monzo)):
stepspan += monzo[i] * pseudoedomapping[i]
# find the degree by adding 1 to the stepspan if it's non-negative, and subtracting 1 if it's negative
degree: int = stepspan
if degree < 0:
degree -= 1
else:
degree += 1
# find the magnitude: round({sum of monzo entries (ignoring the first)} / 7). If 0: central. If positive: L. If negative: s
magnitude_int: int = round(sum(monzo[1:]) / 7.0)
magnitude_str: str = ""
if magnitude_int > 0:
for i in range(magnitude_int):
magnitude_str = "".join([magnitude_str, "L"])
elif magnitude_int < 0:
for i in range(magnitude_int * -1):
magnitude_str = "".join([magnitude_str, "s"])
return f"{magnitude_str}{color}{degree}"
def describe_interval(n: Fraction, format: str = "txt") -> str:
"""
Describes an interval in terms of color notation, ratio, and woolhouse units
Format
------
{color-notation} ({fraction}, {woolhouse-units}w)
"""
if format == "md" or format == "markdown":
return f"**{color_notation(n)}** ({n}, {woolhouse(n):.2f}w)"
elif format == "adoc" or format == "asciidoc":
return f"^.^! {color_notation(n)} ^.^! {n} ^.^! {woolhouse(n):.2f}w"
else:
return f"{color_notation(n)}\t({n},\t{woolhouse(n):.2f}w)"
def describe_genus(characteristic_interval: Fraction, mid_interval: Fraction, low_interval: Fraction, format: str = "txt") -> str:
"""
Describes the intervals present in a genus in `.txt`, `.md`, or `.adoc` format
Ignores intervals of more than 4 steps, or which are present across all genera
Table Format
------------
| | Adiazenksis | Diazenksis |
| ----------- | ----------- | ---------- |
| One Step | | ---------- |
| Two Steps | | |
| Three Steps | ----------- | |
| Four Steps | | ---------- |
Information Included
--------------------
* Adiazenksis Intervals (1 step)
* {characteristic_interval}
* {mid_interval}
* {low_interval}
* Adiazenksis Intervals (2 steps)
* {characteristic_interval + mid_interval}
* {mid_interval + low_interval}
* {characteristic_interval + low_interval}
* Diazenksis Intervals (2 steps)
* {characteristic_interval + 9/8}
* {low_interval + 9/8}
* Diazenksis Intervals (3 steps)
* {characteristic_interval + mid_interval + 9/8}
* {mid_interval + low_interval + 9/8}
* {characteristic_interval + low_interval + 9/8}
* Adiazenksis Intervals (4 steps)
* {characteristic_interval + 4/3}
* {mid_interval + 4/3}
* {low_interval + 4/3}
where `{X}` where `X` is an interval is replaced with:
{color-notation}\t({fraction},\t{woolhouse-units}w)
"""
output: str = ""
if format == "md" or format == "markdown":
# output for markdown
output = "".join([output, f"| | Adiazenksis | Diazenksis |\n"])
output = "".join([output, f"| :---------- | :---------- | :--------- |\n"])
output = "".join([output, f"| **One Step** | "])
output = "".join([output, f"{describe_interval(characteristic_interval, format)} (characteristic)<br>"])
output = "".join([output, f"{describe_interval(mid_interval, format)} (mid)<br>"])
output = "".join([output, f"{describe_interval(low_interval, format)} (low) | "])
output = "".join([output, f"{describe_interval(Fraction(9, 8), format)} (w2) |\n"])
output = "".join([output, f"| **Two Steps** | "])
output = "".join([output, f"{describe_interval(characteristic_interval * mid_interval, format)} (characteristic + mid)<br>"])
output = "".join([output, f"{describe_interval(mid_interval * low_interval, format)} (mid + low)<br>"])
output = "".join([output, f"{describe_interval(characteristic_interval * low_interval, format)} (characteristic + low) | "])
output = "".join([output, f"{describe_interval(characteristic_interval * Fraction(9, 8), format)} (characteristic + w2)<br>"])
output = "".join([output, f"{describe_interval(low_interval * Fraction(9, 8), format)} (low + w2) |\n"])
output = "".join([output, f"| **Three Steps** | "])
output = "".join([output, f"{describe_interval(Fraction(4, 3), format)} (w4) | "])
output = "".join([output, f"{describe_interval(characteristic_interval * mid_interval * Fraction(9, 8), format)} (characteristic + mid + w2)<br>"])
output = "".join([output, f"{describe_interval(mid_interval * low_interval * Fraction(9, 8), format)} (mid + low + w2)<br>"])
output = "".join([output, f"{describe_interval(characteristic_interval * low_interval * Fraction(9, 8), format)} (characteristic + low + w2) |\n"])
output = "".join([output, f"| **Four Steps** | "])
output = "".join([output, f"{describe_interval(characteristic_interval * Fraction(4, 3), format)} (characteristic + w4)<br>"])
output = "".join([output, f"{describe_interval(mid_interval * Fraction(4, 3), format)} (mid + w4)<br>"])
output = "".join([output, f"{describe_interval(low_interval * Fraction(4, 3), format)} (low + w4) | "])
output = "".join([output, f"{describe_interval(Fraction(3, 2), format)} (w5) |"])
elif format == "adoc" or format == "asciidoc":
# output for asciidoc
output = "".join([output, f""])
output = "".join([output, f"[%autowidth]\n|===\n"]) # new table with autosized columns
output = "".join([output, f"^.^| ^.^| Adiazenksis ^.^| Diazenksis\n\n"]) # centered column headers
output = "".join([output, f"| *One Step*\na|\n!===\n"])
output = "".join([output, f"{describe_interval(characteristic_interval, format)} ^.^! characteristic\n"])
output = "".join([output, f"{describe_interval(mid_interval, format)} ^.^! mid\n"])
output = "".join([output, f"{describe_interval(low_interval, format)} ^.^! low\n!===\na|\n!===\n"])
output = "".join([output, f"{describe_interval(Fraction(9, 8), format)} ^.^! w2\n!===\n\n"])
output = "".join([output, f"| *Two Steps*\na|\n!===\n"])
output = "".join([output, f"{describe_interval(characteristic_interval * mid_interval, format)} ^.^! characteristic {{plus}} mid\n"])
output = "".join([output, f"{describe_interval(mid_interval * low_interval, format)} ^.^! mid {{plus}} low\n"])
output = "".join([output, f"{describe_interval(characteristic_interval * low_interval, format)} ^.^! characteristic {{plus}} low\n!===\na|\n!===\n"])
output = "".join([output, f"{describe_interval(characteristic_interval * Fraction(9, 8), format)} ^.^! characteristic {{plus}} w2\n"])
output = "".join([output, f"{describe_interval(low_interval * Fraction(9, 8), format)} ^.^! low {{plus}} w2\n!===\n\n"])
output = "".join([output, f"| *Three Steps*\na|\n!===\n"])
output = "".join([output, f"{describe_interval(Fraction(4, 3), format)} ^.^! w4\n!===\na|\n!===\n"])
output = "".join([output, f"{describe_interval(characteristic_interval * mid_interval * Fraction(9, 8), format)} ^.^! characteristic {{plus}} mid {{plus}} w2\n"])
output = "".join([output, f"{describe_interval(mid_interval * low_interval * Fraction(9, 8), format)} ^.^! mid {{plus}} low {{plus}} w2\n"])
output = "".join([output, f"{describe_interval(characteristic_interval * low_interval * Fraction(9, 8), format)} ^.^! characteristic {{plus}} low {{plus}} w2\n!===\n\n"])
output = "".join([output, f"| *Four Steps*\na|\n!===\n"])
output = "".join([output, f"{describe_interval(characteristic_interval * Fraction(4, 3), format)} ^.^! characteristic {{plus}} w4\n"])
output = "".join([output, f"{describe_interval(mid_interval * Fraction(4, 3), format)} ^.^! mid {{plus}} w4\n"])
output = "".join([output, f"{describe_interval(low_interval * Fraction(4, 3), format)} ^.^! low {{plus}} w4\n!===\na|\n!===\n"])
output = "".join([output, f"{describe_interval(Fraction(3, 2), format)} ^.^! w5\n!===\n|==="])
else:
# output for text or terminal
output = "".join([output, f"Adiazenksis: One Step\n"])
output = "".join([output, f" -- {describe_interval(characteristic_interval)} (characteristic)\n"])
output = "".join([output, f" -- {describe_interval(mid_interval)} (mid)\n"])
output = "".join([output, f" -- {describe_interval(low_interval)} (low)\n"])
output = "".join([output, f"Diazenksis: One Step\n"])
output = "".join([output, f" -- {describe_interval(Fraction(9, 8))} (w2)\n"])
output = "".join([output, f"Adiazenksis: Two Steps\n"])
output = "".join([output, f" -- {describe_interval(characteristic_interval * mid_interval)} (characteristic + mid)\n"])
output = "".join([output, f" -- {describe_interval(mid_interval * low_interval)} (mid + low)\n"])
output = "".join([output, f" -- {describe_interval(characteristic_interval * low_interval)} (characteristic + low)\n"])
output = "".join([output, f"Diazenksis: Two Steps\n"])
output = "".join([output, f" -- {describe_interval(characteristic_interval * Fraction(9, 8))} (characteristic + w2)\n"])
output = "".join([output, f" -- {describe_interval(low_interval * Fraction(9, 8))} (low + w2)\n"])
output = "".join([output, f"Adiazenksis: Three Steps\n"])
output = "".join([output, f" -- {describe_interval(Fraction(4, 3))} (w4)\n"])
output = "".join([output, f"Diazenksis: Three Steps\n"])
output = "".join([output, f" -- {describe_interval(characteristic_interval * mid_interval * Fraction(9, 8))} (characteristic + mid + w2)\n"])
output = "".join([output, f" -- {describe_interval(mid_interval * low_interval * Fraction(9, 8))} (mid + low + w2)\n"])
output = "".join([output, f" -- {describe_interval(characteristic_interval * low_interval * Fraction(9, 8))} (characteristic + low + w2)\n"])
output = "".join([output, f"Adiazenksis: Four Steps\n"])
output = "".join([output, f" -- {describe_interval(characteristic_interval * Fraction(4, 3))} (characteristic + w4)\n"])
output = "".join([output, f" -- {describe_interval(mid_interval * Fraction(4, 3))} (mid + w4)\n"])
output = "".join([output, f" -- {describe_interval(low_interval * Fraction(4, 3))} (low + w4)\n"])
output = "".join([output, f"Diazenksis: Four Steps\n"])
output = "".join([output, f" -- {describe_interval(Fraction(3, 2))} (w5)"])
return output
def octave_reduce(n: Fraction) -> Fraction:
"""
Octave reduces an interval
"""
output: Fraction = n
while output.numerator > output.denominator:
output = output * Fraction(1, 2)
while output.numerator < output.denominator:
output = output * Fraction(2, 1)
return output
def print_header(
name: str = "unnamed",
octave: float = 1200.0,
fifth: float = 701.955,
offset: float = 0.0,
color: str = "#ffffff",
tuning_id: int = 0) -> str:
"""
Generates the first 6 lines of the striso file
Parameters
----------
name: str
The name of the tuning (8 char. limit) (default is unnamed)
octave: float
The value in cents of the octave (default is 1200.0)
fifth: float
The value in cents of the perfect fifth (default is 701.955)
offset: float
The value in cents of the offset (default is 0.0)
color: str
The rgb value for the led to represent the tuning (default is #ffffff)
tuning_id: int
The index of the tuning on the striso board in [0, 9) (default is 0)
"""
print(f"Key\tValue")
print(f"sT{tuning_id:1d}name\t{name:.8s}")
print(f"hT{tuning_id:1d}color\t{color:.7s}")
print(f"fT{tuning_id:1d}off\t{offset:.4f}")
print(f"fT{tuning_id:1d}oct\t{octave:.4f}")
print(f"fT{tuning_id:1d}fifth\t{fifth:.4f}")
def octave_race(root_value: Fraction, root_index: int, has_first: bool = True) -> list[Optional[Fraction]]:
"""
Returns a list of octave-equivalent notes for the Striso
Race is used in the sense of of [sic] a ball bearing
Parameters
----------
root_value: Fraction
The ratio of one of the octave-equivalent notes
root_index: int
The Striso octave index of the root_value note
has_first: bool
Whether there is a first octave index of the root_value note (default is True)
"""
output: List[Optional[Fraction]] = []
for i in range(4):
# The "+1" corrects for root_index being 1-indexed
output.append(root_value * (pow(Fraction(2, 1), i - root_index + 1)))
if not has_first:
output[0] = None
return output
def generate_pythagorean() -> dict:
output: dict = {}
output["Gb"] = octave_race(octave_reduce(pow(Fraction(3, 2), -6)), 3)
output["Db"] = octave_race(octave_reduce(pow(Fraction(3, 2), -5)), 3, False)
output["Ab"] = octave_race(octave_reduce(pow(Fraction(3, 2), -4)), 3)
output["Eb"] = octave_race(octave_reduce(pow(Fraction(3, 2), -3)), 3, False)
output["Bb"] = octave_race(octave_reduce(pow(Fraction(3, 2), -2)), 3)
output["F"] = octave_race(octave_reduce(pow(Fraction(3, 2), -1)), 3)
output["C"] = octave_race(octave_reduce(pow(Fraction(3, 2), 0)), 3, False)
output["G"] = octave_race(octave_reduce(pow(Fraction(3, 2), 1)), 3)
output["D"] = octave_race(octave_reduce(pow(Fraction(3, 2), 2)), 3, False)
output["A"] = octave_race(octave_reduce(pow(Fraction(3, 2), 3)), 3)
output["E"] = octave_race(octave_reduce(pow(Fraction(3, 2), 4)), 3, False)
output["B"] = octave_race(octave_reduce(pow(Fraction(3, 2), 5)), 3)
output["F#"] = octave_race(octave_reduce(pow(Fraction(3, 2), 6)), 3)
output["C#"] = octave_race(octave_reduce(pow(Fraction(3, 2), 7)), 3, False)
output["G#"] = octave_race(octave_reduce(pow(Fraction(3, 2), 8)), 3)
output["D#"] = octave_race(octave_reduce(pow(Fraction(3, 2), 9)), 3, False)
output["A#"] = octave_race(octave_reduce(pow(Fraction(3, 2), 10)), 3)
return output
def generate_greater_perfect_system(characteristic_interval: Fraction, mid_interval: Fraction, low_interval: Fraction) -> dict:
"""
Generates a Greater Perfect System set of ratios for the Striso
Parameters
----------
characteristic_interval: Fraction
The characteristic interval
mid_interval: Fraction
The interval between the trite diezeugmenon and paranete diezeugmenon
low_interval: Fraction
The interval between the paramese and the trite diezeugmenon
"""
output: dict = {}
# syn: going up from the mese starting with the low interval (only this is officially part of the Greater Perfect System)
# down-syn: going down from the paramese starting with the characteristic interval
# former: anti-syn: going down from the trite hyperbolaion starting with 9/8 followed by the characteristic interval
# anti-syn: going down from the trite hyperbolaion starting with the low interval followed by 9/8 followed by the characteristic interval
# up-anti-syn: going up from the nete diezeugmenon starting with 9/8 followed by the low interval
output["Gb"] = octave_race(Fraction(3, 2) * Fraction(8, 9) * inv(characteristic_interval), 3) # anti-syn, dynamic
output["Db"] = octave_race(Fraction(9, 8) * inv(characteristic_interval) * inv(mid_interval), 3, False) # down-syn, dynamic
output["Ab"] = octave_race(Fraction(3, 2) * Fraction(8, 9), 3) # anti-syn, static
output["Eb"] = octave_race(Fraction(9, 8) * inv(characteristic_interval), 3, False) # down-syn, dynamic
output["Bb"] = octave_race(Fraction(3, 2), 3) # anti-syn, static
output["F"] = octave_race(Fraction(9, 8), 3) # static
output["C"] = octave_race(Fraction(3, 2) * low_interval, 4, False) # dynamic
output["G"] = octave_race(Fraction(9, 8) * low_interval, 3) # dynamic
output["D"] = octave_race(Fraction(3, 2) * low_interval * mid_interval, 4, False) # dynamic
output["A"] = octave_race(Fraction(9, 8) * low_interval * mid_interval, 3)
output["E"] = octave_race(octave_reduce(pow(Fraction(3, 2), 0)), 3, False) # static, mese
output["B"] = octave_race(octave_reduce(pow(Fraction(3, 2), 1)), 3) # static
output["F#"] = octave_race(low_interval, 3) # syn, dynamic
output["C#"] = octave_race(Fraction(3, 2) * Fraction(9, 8), 4, False) # up-anti-syn, static
output["G#"] = octave_race(low_interval * mid_interval, 3) # syn, dynamic
output["D#"] = octave_race(Fraction(3, 2) * Fraction(9, 8) * low_interval, 4, False) # up-anti-syn, dynamic
output["A#"] = octave_race(Fraction(4, 3), 3) # syn, static
return output
def striso_pitch_bends(base: dict, target: dict, tuning_id: int = 0) -> str:
"""
Converts a dictionary of octave races of ratios into pitch bends for the striso
Gps cents value - 3-limit cents value = adjustment to output
"""
output: str = ""
# List constants to hold the output order of notes within each octave
notes_internal_name: list[str] = ["C", "Db", "C#", "D", "Eb", "D#", "E", "F", "Gb", "F#", "G", "Ab", "G#", "A", "Bb", "A#", "B"]
notes_striso_name: list[str] = ["C_", "Db", "C#", "D_", "Eb", "D#", "E_", "F_", "Gb", "F#", "G_", "Ab", "G#", "A_", "Bb", "A#", "B_"]
print(target[notes_internal_name[7]][2])
print(base[notes_internal_name[7]][2])
print(target[notes_internal_name[7]][2] * inv(base[notes_internal_name[7]][2]))
# Note that internally 0-indexing is used (as here), but the output will be 1-indexed
for i in range(4): # range over octaves
for j in range(17): # range over notes
if base[notes_internal_name[j]][i] is not None:
output = "".join([output, f"fT{tuning_id}{notes_striso_name[j]}{i + 1}\t{cents(target[notes_internal_name[j]][i] * inv(base[notes_internal_name[j]][i])):.4f}\n"])
return output.strip() # remove trailing "\n" before returning
def main():
# CONFIGURE TUNING
name: str = "archy-en" # Archytas's Enharmonic
offset: float = 0.0
color: str = "#268bd2" # LED color when the tuning is active
tuning_id: int = 8 # which tuning number it is on the Striso (0-indexed)
# These 3 values define a Greater Perfect System (2/3 define the system, but all 3 are required for code clarity)
# Make sure to list the numerator first: I have used the modern convention of notating intervals as going up, not the ancient convention (which is the opposite)
# The 3 intervals together must add to form a perfect 4th
characteristic_interval: Fraction = Fraction(5, 4)
mid_interval: Fraction = Fraction(36, 35)
low_interval: Fraction = Fraction(28, 27)
# INTERNAL FUNCTIONS
# Ensure that the three intervals chosen for the system add up to a w4
assert low_interval * mid_interval * characteristic_interval == Fraction(4, 3)
# Generate Greater Perfect System and Pythagorean tunings based on the 2 intervals (not counting 8ve and 5th)
notes_3limit: dict = generate_pythagorean()
notes_gps: dict = generate_greater_perfect_system(characteristic_interval, mid_interval, low_interval)
# CONFIGURE OUTPUT
# Output tuning file in Striso format
print_header(name, 1200.0, 701.955, offset, color, tuning_id)
print(striso_pitch_bends(notes_3limit, notes_gps, tuning_id))
# Output information on an interval or tuning
# print(describe_interval(Fraction(6, 5), "txt"))
# print(color_notation(Fraction(6, 5)))
# print(describe_genus(characteristic_interval, mid_interval, low_interval, "adoc"))
# Convert a ratio to logarithmic units
# print(f"{cents(Fraction(6, 5)):.2f}¢")
# print(f"{woolhouse(Fraction(6, 5)):.2f}w")
# FUTURE IDEAS
# make function to nicely output from notes_gps or notes_3limit in ratios or cents (or color notation?) (or woolhouse units?) (in either a list or Striso-like chart?)
# generate carlos alpha, beta, and gamma scales
# DEBUG
# print(notes_3limit)
# print(striso_pitch_bends(notes_3limit, 3))
# print(notes_gps)
# print(describe_genus(characteristic_interval, mid_interval, low_interval, "adoc"))
# print(describe_interval(Fraction(81, 80)))
# print(f"{cents(Fraction(81, 80)):.2f}¢")
# print(f"Pitch Discrimination: {woolhouse(1.002):.2f}w—{woolhouse(1.003):.2f}w")
# print(describe_interval(Fraction(8, 7), "txt"))
if __name__ == "__main__":
main()