Greater Perfect System Generator

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()
1 Like

Minor QoL fix to make it output pitch bends relative to C3. The ratios between keys should remain the same

"""
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

	# converts the ratios from terms of the mese to terms of C3, which the Striso expects (so ignore "ctoe" below to think in terms of the mese)
	ctoe: Fraction = mid_interval * characteristic_interval

	output["Gb"] = octave_race(ctoe * Fraction(3, 2) * Fraction(8, 9) * inv(characteristic_interval), 3) # anti-syn, dynamic
	output["Db"] = octave_race(ctoe * Fraction(9, 8) * inv(characteristic_interval) * inv(mid_interval), 3, False) # down-syn, dynamic
	output["Ab"] = octave_race(ctoe * Fraction(3, 2) * Fraction(8, 9), 3) # anti-syn, static
	output["Eb"] = octave_race(ctoe * Fraction(9, 8) * inv(characteristic_interval), 3, False) # down-syn, dynamic
	output["Bb"] = octave_race(ctoe * Fraction(3, 2), 3) # anti-syn, static
	output["F"] = octave_race(ctoe * Fraction(9, 8), 3) # static
	output["C"] = octave_race(ctoe * Fraction(3, 2) * low_interval, 4, False) # dynamic
	output["G"] = octave_race(ctoe * Fraction(9, 8) * low_interval, 3) # dynamic
	output["D"] = octave_race(ctoe * Fraction(3, 2) * low_interval * mid_interval, 4, False) # dynamic
	output["A"] = octave_race(ctoe * Fraction(9, 8) * low_interval * mid_interval, 3)
	output["E"] = octave_race(ctoe * Fraction(1, 1), 3, False) # static, mese
	output["B"] = octave_race(ctoe * Fraction(3, 2), 3) # static
	output["F#"] = octave_race(ctoe * low_interval, 3) # syn, dynamic
	output["C#"] = octave_race(ctoe * Fraction(3, 2) * Fraction(9, 8), 4, False) # up-anti-syn, static
	output["G#"] = octave_race(ctoe * low_interval * mid_interval, 3) # syn, dynamic
	output["D#"] = octave_race(ctoe * Fraction(3, 2) * Fraction(9, 8) * low_interval, 4, False) # up-anti-syn, dynamic
	output["A#"] = octave_race(ctoe * 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

if __name__ == "__main__":
	main()
1 Like