Skip to content

Instantly share code, notes, and snippets.

@GanaramInukshuk
Last active November 17, 2022 06:26
Show Gist options
  • Save GanaramInukshuk/3b09f806573ecd90745d1d7fad11abdc to your computer and use it in GitHub Desktop.
Save GanaramInukshuk/3b09f806573ecd90745d1d7fad11abdc to your computer and use it in GitHub Desktop.
Moscalc and Modecalc (Jupyter Notebook)
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"# Mos algorithm rules, for a mos xL ys (x L's and y s's)\n",
"# This algorithm, given x and y, will produce a string corresponding to the\n",
"# moment-of-symmetry scale xL ys, where the string represents that scale's\n",
"# brightest mode. There are five main cases that xL ys can fall under:\n",
"# - If x and y are both 1, then the scale is \"Ls\"\n",
"# - If only x is 1, then the scale is one L and however many s's.\n",
"# - If only y is 1, then the scale is however many L's and one s.\n",
"# - If x and y share a common factor k, then the mos consists of the mos\n",
"# string for (x/k)L (y/k)s repeated k times; k > 1 and is the greatest common\n",
"# factor between x and y. Recursively call this algorithm to find the mos\n",
"# string corresponding to (x/k)L (y/k)s.\n",
"# - If x and y are coprime (they have no common factors), the scale is built\n",
"# recursively.\n",
"# These cases can be grouped into two broader cases:\n",
"# - Either x or y is 1 (first three cases)\n",
"# - Neither x nor y is 1 (remaining cases)\n",
"# Case 5 is described as follows, given x and y being coprime. Regardless of\n",
"# whether x or y is larger, the mos is built up recursively from a precursor\n",
"# scale where its L's and s's are replaced with subsequences of L's and s's\n",
"# that build up the entire scale.\n",
"# - Let m1 = min(x, y) and m2 = max(x, y). Let z = m2 mod m1 and w = m1 - z.\n",
"# z and w are the number of large and small steps in the precursor scale\n",
"# respectively. Recursively call this algorithm for the mos representing\n",
"# zL ws.\n",
"# - Let u = ceil(m2/m1) and v = floor(m2/m1). If x < y, then the replacement\n",
"# rules are L -> (L and u s's) and s -> (L and v s's). Otherwise, the repl.\n",
"# rules are L -> (s and u L's) and s -> (s and v L's).\n",
"# - If x < y, or there are fewer L's than s's, then it's necessary to reverse\n",
"# the precursor scale before applying the replacement rules. This is to\n",
"# ensure the final mos string represents the mos in its brightest mode. More\n",
"# notes down below.\n",
"# How brightness is ensured:\n",
"# - The algorithm is recursive so that the mos's brightest mode is always\n",
"# returned. Consider the prescales LLs and Lss and the two sets of rules:\n",
"# - L->LLs and s->Ls\n",
"# - L->Lss and s->Ls\n",
"# - One way to gauge brightness is to determine whether as many L's as\n",
"# possible are to the left. Applying the rules to the two prescales produces\n",
"# these scales, with dashes as delimiters:\n",
"# - LLs-LLs-Ls and Lss-Lss-Ls\n",
"# - LLs-Ls-Ls and Lss-Ls-Ls\n",
"# - Applying the second set of rules results in a scale that's not in its\n",
"# brightest mode; the correct scales should be Ls-Lss-Lss and Ls-Ls-Lss.\n",
"# It turns out correcting this is as easy as reversing the prescale first.\n",
"# - This works no matter what the precursor scale is, because the algoritm is\n",
"# recursive, and no matter what the replacement rules are, because the rules\n",
"# will always be L->LL...LLs s->LL...Ls or L->Lss...ss s->Lss...s.\n",
"def CalculateMos(x, y):\n",
"\n",
" mos_string = \"\"\n",
"\n",
" if (x == 1 or y == 1):\n",
" if (x == 1 and y == 1):\n",
" mos_string = \"Ls\"\n",
" elif (x == 1 and y > 1):\n",
" mos_string = \"L\" + \"s\" * y\n",
" elif (x > 1 and y == 1):\n",
" mos_string = \"L\" * x + \"s\"\n",
" else:\n",
" mos_string = \"unable to find mos\"\n",
" \n",
" else:\n",
" \n",
" k = min(x, y)\n",
" while (k > 0):\n",
" if (x % k == 0 and y % k == 0):\n",
" break\n",
" k -= 1\n",
"\n",
" if (k != 1):\n",
" mos_string = CalculateMos(x // k, y // k) * k\n",
" elif (k == 1):\n",
" m1 = min(x, y)\n",
" m2 = max(x, y)\n",
" z = m2 % m1\n",
" w = m1 - z\n",
"\n",
" prescale = CalculateMos(z, w)\n",
"\n",
" if (x < y):\n",
" prescale = prescale[::-1]\n",
"\n",
" l_rule = \"\"\n",
" s_rule = \"\"\n",
" if (x > y):\n",
" l_rule = \"L\" * math.ceil(m2 / m1) + \"s\"\n",
" s_rule = \"L\" * math.floor(m2 / m1) + \"s\"\n",
" else:\n",
" l_rule = \"L\" + \"s\" * math.ceil(m2 / m1)\n",
" s_rule = \"L\" + \"s\" * math.floor(m2 / m1)\n",
"\n",
" for step in prescale:\n",
" if step == \"L\":\n",
" mos_string += l_rule\n",
" elif step == \"s\":\n",
" mos_string += s_rule\n",
"\n",
" else:\n",
" mos_string = \"unable to find mos\"\n",
"\n",
" return mos_string\n",
"\n",
"# This follows the same exact procedure as CalculateMos(), with a few modifications:\n",
"# The base cases return a scale that's split into two parts; in fact, the entire function\n",
"# returns a scale split into two parts. Each half represents one of the scale's generators\n",
"# as a substring of the entire scale, where the left half is the chroma-positive generator\n",
"# and the right half is the chroma-negative generator (or CPG and CNG). The base cases are:\n",
"# - If x and y are both 1, the CPG is L and the CNG s\n",
"# - If only x is 1, the CPG is L followed by y-1 s's and the CNG s\n",
"# - If only y is 1, the CPG is L and the CNG is x-1 L's followed by s\n",
"# For the recursive cases:\n",
"# - If x and y have a GCF k that's greater than 1, only return the generators corresponding to\n",
"# the mos (x/k)L (y/k)s; the generators apply to one period, which is duplicated k times.\n",
"# - For the recursive case, the production rules will now apply to two separated halves of the\n",
"# scale, so extra code is needed for each half. Also, in the case where the prescale needs to\n",
"# be reversed, reversal is done by first reversing the individual substrings, followed by\n",
"# swapping the string for the CPG and CNG.\n",
"def CalculateMosGenerators(x, y):\n",
"\n",
" mos_string_left = \"\"\n",
" mos_string_right = \"\"\n",
"\n",
" if (x == 1 or y == 1):\n",
" if (x == 1 and y == 1):\n",
" mos_string_left = \"L\"\n",
" mos_string_right = \"s\"\n",
" elif (x == 1 and y > 1):\n",
" mos_string_left = \"L\" + \"s\" * (y - 1)\n",
" mos_string_right = \"s\"\n",
" elif (x > 1 and y == 1):\n",
" mos_string_left = \"L\"\n",
" mos_string_right = \"L\" * (x - 1) + \"s\"\n",
" else:\n",
" mos_string_left = \"unable to find generator\"\n",
" mos_string_right = \"unable to find generator\"\n",
" \n",
" else:\n",
" \n",
" k = min(x, y)\n",
" while (k > 0):\n",
" if (x % k == 0 and y % k == 0):\n",
" break\n",
" k -= 1\n",
"\n",
" if (k != 1):\n",
" mos_string_left, mos_string_right = CalculateMosGenerators(x // k, y // k)\n",
" elif (k == 1):\n",
" m1 = min(x, y)\n",
" m2 = max(x, y)\n",
" z = m2 % m1\n",
" w = m1 - z\n",
"\n",
" prescale_left, prescale_right = CalculateMosGenerators(z, w)\n",
"\n",
" if (x < y):\n",
" prescale_left = prescale_left[::-1]\n",
" prescale_right = prescale_right[::-1]\n",
" \n",
" temp = prescale_left\n",
" prescale_left = prescale_right\n",
" prescale_right = temp\n",
"\n",
" l_rule = \"\"\n",
" s_rule = \"\"\n",
" if (x > y):\n",
" l_rule = \"L\" * math.ceil(m2 / m1) + \"s\"\n",
" s_rule = \"L\" * math.floor(m2 / m1) + \"s\"\n",
" else:\n",
" l_rule = \"L\" + \"s\" * math.ceil(m2 / m1)\n",
" s_rule = \"L\" + \"s\" * math.floor(m2 / m1)\n",
"\n",
" for step in prescale_left:\n",
" if step == \"L\":\n",
" mos_string_left += l_rule\n",
" elif step == \"s\":\n",
" mos_string_left += s_rule\n",
" \n",
" for step in prescale_right:\n",
" if step == \"L\":\n",
" mos_string_right += l_rule\n",
" elif step == \"s\":\n",
" mos_string_right += s_rule\n",
"\n",
" else:\n",
" mos_string_left = \"unable to find generator\"\n",
" mos_string_right = \"unable to find generator\"\n",
"\n",
" return mos_string_left, mos_string_right"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [],
"source": [
"# Modecalc function\n",
"from enum import unique\n",
"from matplotlib.pyplot import sca\n",
"from tabulate import *\n",
"\n",
"def Modecalc(scalestring):\n",
"\n",
" modes_cpo = []\n",
" modes_udp = []\n",
"\n",
" # Produce the rotations of the scalestring; each rotation is a different\n",
" # mode, and the order they're inserted in the array is cyclic permutational\n",
" # order, or the order in which the modes appear when using each successive\n",
" # note of one mode as the root. Only unique modes are added.\n",
" notecount = len(scalestring)\n",
" for i in range(notecount):\n",
" scale_to_add = scalestring[i:] + scalestring[:i]\n",
"\n",
" if (modes_udp.count(scale_to_add) == 0):\n",
" modes_udp.append(scale_to_add)\n",
"\n",
" modecount = len(modes_udp)\n",
"\n",
" # A second set of modes is sorted by modal brightness. UDP notation denotes\n",
" # the number of generating intervals going up or down that produces that mode.\n",
" # Diatonic ionian, for example, is produced by going up 5 generators (C to G,\n",
" # G to D, D to A, A to E, and E to B) and going down 1 generator (C to F), and\n",
" # its UDP is therefore 5|1. Going up 6 generators instead produces a scale\n",
" # that has F# instead of F natural; this is the lydian mode and its UDP\n",
" # is 6|0.\n",
" # Multi-period scales are denoted as x|y(k), and UDP becomes tricker to notate\n",
" # with arbitrary alterations, since these alterations necessarily require\n",
" # a distributionally even scale to refer off of. This function currently only\n",
" # knows how to deal with scales that are already distributionally even (so every\n",
" # moment-of-symmetry scale in existence and their SVn analogues) and that is\n",
" # contingent on sorting scalestrings in lexicographic order.\n",
" # Lexicographic ordering of scalestrings makes more sense on scalestrings whose\n",
" # steps are distributionally even, such as mosses. Deviations from that, such\n",
" # as with a modmos, require its distributionally even form for reference, so \n",
" # sorting by UDP may not make much sense in this case.\n",
" modes_udp.sort()\n",
"\n",
" # Modal brightness necessarily requires a scale's rotations be sorted in\n",
" # lexicographic ordering. The order in which they appear by rotation is a\n",
" # different ordering altogether. For the sake of standardization, rotational\n",
" # ordering starts with the first mode in the modal brightness ordering.\n",
" for i in range(notecount):\n",
" scale_to_add = modes_udp[0][i:] + modes_udp[0][:i]\n",
"\n",
" if (modes_cpo.count(scale_to_add) == 0):\n",
" modes_cpo.append(scale_to_add)\n",
"\n",
" # Make a pair of tables that shows the interval matrix for the scale,\n",
" # where one table depicts modes sorted by CPO and the other by modal brightness.\n",
" interval_table_cpo = GenerateIntervalMatrix(modes_cpo)\n",
" interval_table_udp = GenerateIntervalMatrix(modes_udp)\n",
"\n",
" # Generate row headers; this should be the modetsring and their enumeration based\n",
" # on the type of ordering.\n",
" notecount = len(scalestring)\n",
" modecount = len(interval_table_cpo)\n",
" for i in range(modecount):\n",
" interval_table_cpo[i].insert(0, str(i))\n",
" interval_table_cpo[i].insert(0, modes_cpo[i])\n",
"\n",
" interval_table_udp[i].insert(0, str(i))\n",
" interval_table_udp[i].insert(0, modes_udp[i])\n",
"\n",
" table_header = []\n",
" table_header.append(\"Modestring\")\n",
" table_header.append(\"Order\")\n",
" for i in range(notecount + 1):\n",
" table_header.append(str(i) + \"-step\")\n",
"\n",
" interval_table_cpo.insert(0, table_header)\n",
"\n",
" interval_table_udp.insert(0, table_header)\n",
"\n",
" # Generate tables for scale degrees\n",
" degree_table_cpo = GenerateDegreeMatrix(modes_cpo)\n",
" degree_table_udp = GenerateDegreeMatrix(modes_udp)\n",
"\n",
" for i in range(modecount):\n",
" degree_table_cpo[i].insert(0, str(i))\n",
" degree_table_cpo[i].insert(0, modes_cpo[i])\n",
"\n",
" degree_table_udp[i].insert(0, str(i))\n",
" degree_table_udp[i].insert(0, modes_udp[i])\n",
"\n",
" table_header = []\n",
" table_header.append(\"Modestring\")\n",
" table_header.append(\"Order\")\n",
" for i in range(notecount + 1):\n",
" table_header.append(str(i) + \"-deg\")\n",
"\n",
" degree_table_cpo.insert(0, table_header)\n",
" degree_table_udp.insert(0, table_header)\n",
"\n",
" print(\"\\nScale modes of \" + scalestring + \" sorted by rotational order:\")\n",
" print(tabulate(interval_table_cpo))\n",
" print(\"Scale modes of \" + scalestring + \" sorted by brigthness:\")\n",
" print(tabulate(interval_table_udp))\n",
"\n",
" print(\"\\nScale degrees of \" + scalestring + \" for each mode (modes sorted by rotation):\")\n",
" print(tabulate(degree_table_cpo))\n",
" print(\"\\nScale degrees of \" + scalestring + \" for each mode (modes sorted by brightness):\")\n",
" print(tabulate(degree_table_udp))\n",
"\n",
" # for row in interval_table_udp:\n",
" # table_row = \"\"\n",
" # for entry in row:\n",
" # table_row += entry + \"\\t\"\n",
" # print(table_row)\n",
"\n",
" return None\n",
"\n",
"\n",
"# Create an interval matrix out of the provided list of modes\n",
"# Table produced this way lacks headers\n",
"def GenerateIntervalMatrix(modes):\n",
"\n",
" interval_matrix = []\n",
"\n",
" for mode in modes:\n",
" row = []\n",
" notecount = len(mode) + 1\n",
" for i in range(notecount):\n",
" row.append(IntervalToStepsum(mode[0:i]))\n",
" interval_matrix.append(row)\n",
"\n",
" return interval_matrix\n",
"\n",
"def GenerateDegreeMatrix(modes):\n",
"\n",
" interval_matrix = []\n",
" for mode in modes:\n",
" row = []\n",
" notecount = len(mode) + 1\n",
" for i in range(notecount):\n",
" row.append(\"\".join(sorted(mode[0:i])))\n",
" interval_matrix.append(row)\n",
"\n",
" degree_matrix = interval_matrix.copy()\n",
" unique_intervals = GenerateUniqueIntervalListings(modes)\n",
"\n",
" modecount = len(modes)\n",
" notecount = len(modes[0])\n",
"\n",
" for i in range(modecount):\n",
" for j in range(notecount + 1):\n",
" interval_count = len(unique_intervals[j])\n",
" interval = interval_matrix[i][j]\n",
" degree = str(interval_count - 1 - unique_intervals[j].index(interval))\n",
" degree_matrix[i][j] = degree\n",
"\n",
" return degree_matrix\n",
"\n",
"# Generate an interval listing matrix, where each column corresponds to a mode\n",
"# and each row corresponds to the intervals of a specific size from every mode\n",
"# This is basically the transpose of an interval matrix\n",
"# def GenerateIntervalListings(modes):\n",
"\n",
"# interval_listing_matrix = []\n",
"\n",
"# notecount = len(modes[0])\n",
"# modecount = len(modes)\n",
"# for i in range(notecount + 1):\n",
"# row = []\n",
"# for j in range(modecount):\n",
"# intervalstring = modes[j][0:i]\n",
"# if intervalstring == \"\":\n",
"# intervalstring = \"empty\"\n",
"# else:\n",
"# intervalstring = \"\".join(sorted(intervalstring))\n",
"# row.append(intervalstring)\n",
"# interval_listing_matrix.append(row)\n",
" \n",
"# return interval_listing_matrix\n",
"\n",
"def GenerateUniqueIntervalListings(modes):\n",
"\n",
" interval_listings = []\n",
"\n",
" notecount = len(modes[0])\n",
" modecount = len(modes)\n",
" for i in range(notecount + 1):\n",
" row = []\n",
" for j in range(modecount):\n",
" intervalstring = modes[j][0:i]\n",
" intervalstring = \"\".join(sorted(intervalstring))\n",
" row.append(intervalstring)\n",
" interval_listings.append(row)\n",
"\n",
" unique_intervals = []\n",
"\n",
" for intervals in interval_listings:\n",
" row = []\n",
" for intervalstring in intervals:\n",
" if row.count(intervalstring) == 0:\n",
" row.append(intervalstring)\n",
" row.sort()\n",
" unique_intervals.append(row)\n",
"\n",
" return unique_intervals\n",
"\n",
"# def IntervalToSortedInterval(intervalstring):\n",
"\n",
"# return \"\".join(sorted(intervalstring))\n",
"\n",
"def IntervalToStepsum(intervalstring):\n",
"\n",
" intervalsum = \"\"\n",
"\n",
" if intervalstring == \"\":\n",
" intervalsum = \"0\"\n",
" else:\n",
"\n",
" stepset = set()\n",
"\n",
" for step in intervalstring:\n",
" stepset.add(step)\n",
"\n",
" sorted_stepset = list(stepset)\n",
" sorted_stepset.sort()\n",
"\n",
" for step in sorted_stepset:\n",
" stepcount = intervalstring.count(step)\n",
" if stepcount == 1:\n",
" intervalsum += step + \"+\"\n",
" else:\n",
" intervalsum += str(stepcount) + step + \"+\"\n",
"\n",
" intervalsum = intervalsum[:-1]\n",
"\n",
" return intervalsum"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [],
"source": [
"def GetTamnamsPrefix(lg_count, sm_count):\n",
"\n",
" step_count = lg_count + sm_count\n",
" x_index = step_count - 2\n",
" y_index = lg_count - 1\n",
"\n",
" names = [\n",
" [\"triv\"],\n",
" [\"atri\", \"tri\"],\n",
" [\"atetra\", \"biwd\", \"tetra\"],\n",
" [\"ped\", \"pent\", \"apent\", \"manu\"],\n",
" [\"amech\", \"mal\", \"triwood\", \"citry\", \"mech\"],\n",
" [\"on\", \"pel\", \"mosh\", \"smi\", \"\", \"arch\"],\n",
" [\"apine\", \"subar\", \"checker\", \"tetwud\", \"oneiro\", \"ek\", \"pine\"],\n",
" [\"ablu\", \"bal\", \"cher\", \"gram\", \"cthon\", \"hyru\", \"arm\", \"blu\"],\n",
" [\"asina\", \"jara\", \"seph\", \"lime\", \"penwud\", \"lem\", \"zal\", \"tara\", \"sina\"]\n",
" ]\n",
"\n",
" if (step_count <= 10) :\n",
" return names[x_index][y_index]\n",
" else:\n",
" return \"\""
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [],
"source": [
"# This is a special version of Modecalc specific for mosses: MosModecalc\n",
"# Function accepts an additional parameter for formatting (table_fmt):\n",
"# - If this is \"mediawiki\", then the output is formatted such that it can be copypasted into a MediaWiki editor to form a table.\n",
"# - If left unspecified or blank, then the output is the default formatting for tabulate.\n",
"# Notes:\n",
"# Because a mos has all the fun properties expected out of the standard diatonic scale (but generalized),\n",
"# it makes sense to produce a table where modes are sorted by UDP primarily, which is what MosModecalc does.\n",
"# For a general scale, this may not always hold up, hence Modecalc on its own will produce four tables\n",
"# where two are sorted by \"brightness\" (not necessarily UDP) and two more by rotational order.\n",
"def MosModecalc(x, y, table_fmt=\"\", mosprefix=\"mos\", modenames=[]):\n",
"\n",
" # Find the scalestring that represents the mos in its brightest mode, and its generators\n",
" brightest_mode = CalculateMos(x, y)\n",
" cpg, cng = CalculateMosGenerators(x, y)\n",
"\n",
" # Find the size of the scale and its generator sizes in number of steps\n",
" notecount = x + y\n",
" cpg_size = len(cpg)\n",
" cng_size = len(cng)\n",
"\n",
" # Use that info to find the number of periods the scale has\n",
" # Note that period size is cpg_size + cng_size. This is equivalent to notecount only for single\n",
" # period mosses, hence this extra calculation.\n",
" period_size = cpg_size + cng_size\n",
" number_of_periods = notecount // period_size\n",
"\n",
" # Find the modes of the mos by producing all of the unique \"shifts\" of the brightest scalestring.\n",
" # Storing these shifts in an array and sorting them sorts all of the modes by modal brightness,\n",
" # given that the characters that represent the large and small step (L and s typically), when sorted\n",
" # in lexicographic order, also represents the order in which those step sizes appear in decreasing\n",
" # order of size. (In short, if L comes before s asciibetically, then L should come before s because\n",
" # L represents a value that's larger than s.)\n",
" # Leaving the modes produced this way unsorted sorts the modes by rotation instead, or rather, the\n",
" # order in which modes appear if each successive scale degree of the brightest mode were used as\n",
" # the root. If sorted by brightness, the UDP of each mode will necessarily be u|d. Finding a mode's\n",
" # rotational order is then calculating d * cpg_size % notecount.\n",
" modes = []\n",
" for i in range(notecount):\n",
" scale_to_add = brightest_mode[i:] + brightest_mode[:i]\n",
"\n",
" if (modes.count(scale_to_add) == 0):\n",
" modes.append(scale_to_add)\n",
" modes.sort()\n",
" modecount = len(modes)\n",
"\n",
" # Create the interval matrix\n",
" interval_matrix = GenerateIntervalMatrix(modes)\n",
"\n",
" # Create the interval table header\n",
" interval_table_header = []\n",
" interval_table_header.append(\"Mode\")\n",
" interval_table_header.append(\"UDP\")\n",
" interval_table_header.append(\"Mode name\")\n",
" interval_table_header.append(\"Rotational order\")\n",
" for i in range(notecount + 1):\n",
" if i == 0:\n",
" interval_table_header.append(mosprefix + \"unison\")\n",
" elif i == notecount:\n",
" interval_table_header.append(mosprefix + \"octave\")\n",
" else:\n",
" interval_table_header.append(str(i) + \"-\" + mosprefix + \"step\")\n",
"\n",
" # Create the rest of the interval table\n",
" interval_table = []\n",
" for i in range(modecount):\n",
" # Create a new row\n",
" interval_table.append([])\n",
" \n",
" # Add the mos step pattern\n",
" interval_table[i].append(modes[i])\n",
"\n",
" # Add the UDP, formatted based on whethr it's mediawiki or not\n",
" if number_of_periods == 1:\n",
" if (table_fmt == \"mediawiki\"):\n",
" interval_table[i].append(str(modecount-1-i) + \"<nowiki>|</nowiki>\" + str(i))\n",
" else:\n",
" interval_table[i].append(str(modecount-1-i) + \"|\" + str(i))\n",
" else:\n",
" if (table_fmt == \"mediawiki\"):\n",
" interval_table[i].append(str((modecount-1-i)*number_of_periods) + \"<nowiki>|</nowiki>\" + str(i * number_of_periods) + \"(\" + str(number_of_periods) + \")\")\n",
" else:\n",
" interval_table[i].append(str((modecount-1-i)*number_of_periods) + \"|\" + str(i * number_of_periods) + \"(\" + str(number_of_periods) + \")\")\n",
"\n",
" # Add the mode names, if provided\n",
" # The order of mode names should be in decreasing modal brightness\n",
" # If the number of mode names doesn't match the number of modes, don't add\n",
" # and default to naming them as \"Mode <number>\"\n",
" if len(modenames) == modecount:\n",
" interval_table[i].append(modenames[i])\n",
" else:\n",
" modename = \"Mode \" + str(i+1)\n",
" interval_table[i].append(modename)\n",
" \n",
" # Add the \"rotational order\"\n",
" interval_table[i].append(str((i * cpg_size) % period_size))\n",
"\n",
" # Add the mossteps\n",
" for j in range(len(interval_matrix[i])):\n",
" interval_table[i].append(interval_matrix[i][j])\n",
" \n",
" # Append table header to front of the actual table\n",
" interval_table = [interval_table_header] + interval_table\n",
"\n",
"\n",
" # Create the degree matrix\n",
" degree_matrix = GenerateDegreeMatrix(modes)\n",
"\n",
" # Go through the degree matrix and convert the numeric values to major/minor/perfect/augmented/diminished accordingly\n",
" # - The unison and mosoctave are perfect.\n",
" # - The smaller interval size is minor, where the larger is major.\n",
" # - For the bright generator, the larger is perfect and the smaller is diminished. Diminished should appear only once.\n",
" # - For the dark generator, the larger is augmented and the smaller is perfect. Augmented should appear only once.\n",
" # - For multi-period mosses, mosdegrees that appear at each period are perfect, on top of the unison and mosoctave.\n",
" # Additionally, the generators lose their designation of augmented/perfect/diminished and instead are classed as major/minor.\n",
" # A few notes:\n",
" # - The unison is reached by going up zero notes; the mosoctave is reached by going up however many steps there are in the mos.\n",
" # - The generators are reached by going up however many steps there are in each generator. Since the CPG and CNG are\n",
" # distinct values that, when added, produce the mosoctave (or period for multi-period mosses), the sizes of these\n",
" # generators (in mossteps) are enough to determine where they are in the table.\n",
" degree_matrix_to_text = degree_matrix.copy()\n",
" for j in range(notecount + 1):\n",
" for i in range(modecount):\n",
" if j % period_size == 0:\n",
" degree_matrix_to_text[i][j] = \"perfect\"\n",
" elif j == cpg_size and (number_of_periods == 1):\n",
" if degree_matrix[i][j] == \"0\":\n",
" degree_matrix_to_text[i][j] = \"diminished\"\n",
" elif degree_matrix[i][j] == \"1\":\n",
" degree_matrix_to_text[i][j] = \"perfect\"\n",
" elif j == cng_size and (number_of_periods == 1):\n",
" if degree_matrix[i][j] == \"0\":\n",
" degree_matrix_to_text[i][j] = \"perfect\"\n",
" elif degree_matrix[i][j] == \"1\":\n",
" degree_matrix_to_text[i][j] = \"augmented\"\n",
" else:\n",
" if degree_matrix[i][j] == \"0\":\n",
" degree_matrix_to_text[i][j] = \"minor\"\n",
" elif degree_matrix[i][j] == \"1\":\n",
" degree_matrix_to_text[i][j] = \"major\"\n",
"\n",
" # Create the degree table header\n",
" degree_table_header = []\n",
" degree_table_header.append(\"Mode\")\n",
" degree_table_header.append(\"UDP\")\n",
" degree_table_header.append(\"Mode name\")\n",
" degree_table_header.append(\"Rotational order\")\n",
" for i in range(notecount + 1):\n",
" degree_table_header.append(str(i) + \"-\" + mosprefix + \"degree\")\n",
"\n",
" # Create the rest of the degree table\n",
" degree_table = []\n",
" for i in range(modecount):\n",
" # Add a new row to the table\n",
" degree_table.append([])\n",
"\n",
" # Add the mos step pattern\n",
" degree_table[i].append(modes[i])\n",
"\n",
" # Add the UDP\n",
" # Format it depending on whether it's a mediawiki table or not\n",
" if number_of_periods == 1:\n",
" if (table_fmt == \"mediawiki\"):\n",
" degree_table[i].append(str(modecount-1-i) + \"<nowiki>|</nowiki>\" + str(i))\n",
" else:\n",
" degree_table[i].append(str(modecount-1-i) + \"|\" + str(i))\n",
" else:\n",
" if (table_fmt == \"mediawiki\"):\n",
" degree_table[i].append(str((modecount-1-i)*number_of_periods) + \"<nowiki>|</nowiki>\" + str(i * number_of_periods) + \"(\" + str(number_of_periods) + \")\")\n",
" else:\n",
" degree_table[i].append(str((modecount-1-i)*number_of_periods) + \"|\" + str(i * number_of_periods) + \"(\" + str(number_of_periods) + \")\")\n",
"\n",
" # Add the mode names, if provided\n",
" # The order of mode names should be in decreasing modal brightness\n",
" # If the number of mode names doesn't match the number of modes, don't add\n",
" # and default to naming them as \"Mode <number>\"\n",
" if len(modenames) == modecount:\n",
" degree_table[i].append(modenames[i])\n",
" else:\n",
" modename = \"Mode \" + str(i+1)\n",
" degree_table[i].append(modename)\n",
"\n",
" # Add the \"rotational order\" for the mode\n",
" degree_table[i].append(str((i * cpg_size) % period_size))\n",
"\n",
" # Add the mosdegrees\n",
" for j in range(len(degree_matrix_to_text[i])):\n",
" degree_table[i].append(degree_matrix_to_text[i][j])\n",
"\n",
" degree_table = [degree_table_header] + degree_table\n",
"\n",
" # Print the table\n",
" print(tabulate(interval_table, headers=\"firstrow\", tablefmt=table_fmt))\n",
" print(tabulate(degree_table, headers=\"firstrow\", tablefmt=table_fmt))\n",
"\n",
"\n",
" return None"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [],
"source": [
"# This is a special version of Modecalc specific for mosses: MosModecalc\n",
"# Function accepts an additional parameter for formatting (table_fmt):\n",
"# - If this is \"mediawiki\", then the output is formatted such that it can be copypasted into a MediaWiki editor to form a table.\n",
"# - If left unspecified or blank, then the output is the default formatting for tabulate.\n",
"# Notes:\n",
"# Because a mos has all the fun properties expected out of the standard diatonic scale (but generalized),\n",
"# it makes sense to produce a table where modes are sorted by UDP primarily, which is what MosModecalc does.\n",
"# For a general scale, this may not always hold up, hence Modecalc on its own will produce four tables\n",
"# where two are sorted by \"brightness\" (not necessarily UDP) and two more by rotational order.\n",
"def MosModecalcOnetable(x, y, table_fmt=\"\", mosprefix=\"mos\", modenames=[]):\n",
"\n",
" # Find the scalestring that represents the mos in its brightest mode, and its generators\n",
" brightest_mode = CalculateMos(x, y)\n",
" cpg, cng = CalculateMosGenerators(x, y)\n",
"\n",
" # Find the size of the scale and its generator sizes in number of steps\n",
" notecount = x + y\n",
" cpg_size = len(cpg)\n",
" cng_size = len(cng)\n",
"\n",
" # Use that info to find the number of periods the scale has\n",
" # Note that period size is cpg_size + cng_size. This is equivalent to notecount only for single\n",
" # period mosses, hence this extra calculation.\n",
" period_size = cpg_size + cng_size\n",
" number_of_periods = notecount // period_size\n",
"\n",
" # Find the modes of the mos by producing all of the unique \"shifts\" of the brightest scalestring.\n",
" # Storing these shifts in an array and sorting them sorts all of the modes by modal brightness,\n",
" # given that the characters that represent the large and small step (L and s typically), when sorted\n",
" # in lexicographic order, also represents the order in which those step sizes appear in decreasing\n",
" # order of size. (In short, if L comes before s asciibetically, then L should come before s because\n",
" # L represents a value that's larger than s.)\n",
" # Leaving the modes produced this way unsorted sorts the modes by rotation instead, or rather, the\n",
" # order in which modes appear if each successive scale degree of the brightest mode were used as\n",
" # the root. If sorted by brightness, the UDP of each mode will necessarily be u|d. Finding a mode's\n",
" # rotational order is then calculating d * cpg_size % notecount.\n",
" modes = []\n",
" for i in range(notecount):\n",
" scale_to_add = brightest_mode[i:] + brightest_mode[:i]\n",
"\n",
" if (modes.count(scale_to_add) == 0):\n",
" modes.append(scale_to_add)\n",
" modes.sort()\n",
" modecount = len(modes)\n",
"\n",
" # Create the interval matrix\n",
" interval_matrix = GenerateIntervalMatrix(modes)\n",
"\n",
" # Create the degree matrix\n",
" degree_matrix = GenerateDegreeMatrix(modes)\n",
"\n",
" # Go through the degree matrix and convert the numeric values to major/minor/perfect/augmented/diminished accordingly\n",
" # - The unison and mosoctave are perfect.\n",
" # - The smaller interval size is minor, where the larger is major.\n",
" # - For the bright generator, the larger is perfect and the smaller is diminished. Diminished should appear only once.\n",
" # - For the dark generator, the larger is augmented and the smaller is perfect. Augmented should appear only once.\n",
" # - For multi-period mosses, mosdegrees that appear at each period are perfect, on top of the unison and mosoctave.\n",
" # Additionally, the generators lose their designation of augmented/perfect/diminished and instead are classed as major/minor.\n",
" # A few notes:\n",
" # - The unison is reached by going up zero notes; the mosoctave is reached by going up however many steps there are in the mos.\n",
" # - The generators are reached by going up however many steps there are in each generator. Since the CPG and CNG are\n",
" # distinct values that, when added, produce the mosoctave (or period for multi-period mosses), the sizes of these\n",
" # generators (in mossteps) are enough to determine where they are in the table.\n",
" degree_matrix_to_text = degree_matrix.copy()\n",
" for j in range(notecount + 1):\n",
" for i in range(modecount):\n",
" if j % period_size == 0:\n",
" degree_matrix_to_text[i][j] = \"perfect\"\n",
" elif j == cpg_size and (number_of_periods == 1):\n",
" if degree_matrix[i][j] == \"0\":\n",
" degree_matrix_to_text[i][j] = \"diminished\"\n",
" elif degree_matrix[i][j] == \"1\":\n",
" degree_matrix_to_text[i][j] = \"perfect\"\n",
" elif j == cng_size and (number_of_periods == 1):\n",
" if degree_matrix[i][j] == \"0\":\n",
" degree_matrix_to_text[i][j] = \"perfect\"\n",
" elif degree_matrix[i][j] == \"1\":\n",
" degree_matrix_to_text[i][j] = \"augmented\"\n",
" else:\n",
" if degree_matrix[i][j] == \"0\":\n",
" degree_matrix_to_text[i][j] = \"minor\"\n",
" elif degree_matrix[i][j] == \"1\":\n",
" degree_matrix_to_text[i][j] = \"major\"\n",
"\n",
" # Create the interval table header\n",
" interval_table_header = []\n",
" interval_table_header.append(\"Mode\")\n",
" interval_table_header.append(\"UDP\")\n",
" interval_table_header.append(\"Mode name\")\n",
" interval_table_header.append(\"Rotational order\")\n",
" for i in range(notecount + 1):\n",
" if i == 0:\n",
" interval_table_header.append(mosprefix + \"unison (\" + str(i) + \"-\" + mosprefix + \"degree)\")\n",
" elif i == notecount:\n",
" interval_table_header.append(mosprefix + \"octave (\" + str(i) + \"-\" +mosprefix + \"degree)\")\n",
" else:\n",
" interval_table_header.append(str(i) + \"-\" + mosprefix + \"step (\" +str(i) + \"-\" +mosprefix + \"degree)\")\n",
"\n",
" # Create the rest of the interval table\n",
" interval_table = []\n",
" for i in range(modecount):\n",
" # Create a new row\n",
" interval_table.append([])\n",
" \n",
" # Add the mos step pattern\n",
" interval_table[i].append(modes[i])\n",
"\n",
" # Add the UDP, formatted based on whethr it's mediawiki or not\n",
" if number_of_periods == 1:\n",
" if (table_fmt == \"mediawiki\"):\n",
" interval_table[i].append(str(modecount-1-i) + \"<nowiki>|</nowiki>\" + str(i))\n",
" else:\n",
" interval_table[i].append(str(modecount-1-i) + \"|\" + str(i))\n",
" else:\n",
" if (table_fmt == \"mediawiki\"):\n",
" interval_table[i].append(str((modecount-1-i)*number_of_periods) + \"<nowiki>|</nowiki>\" + str(i * number_of_periods) + \"(\" + str(number_of_periods) + \")\")\n",
" else:\n",
" interval_table[i].append(str((modecount-1-i)*number_of_periods) + \"|\" + str(i * number_of_periods) + \"(\" + str(number_of_periods) + \")\")\n",
"\n",
" # Add the mode names, if provided\n",
" # The order of mode names should be in decreasing modal brightness\n",
" # If the number of mode names doesn't match the number of modes, don't add\n",
" # and default to naming them as \"Mode <number>\"\n",
" if len(modenames) == modecount:\n",
" interval_table[i].append(modenames[i])\n",
" else:\n",
" modename = \"Mode \" + str(i+1)\n",
" interval_table[i].append(modename)\n",
" \n",
" # Add the \"rotational order\"\n",
" interval_table[i].append(str((i * cpg_size) % period_size))\n",
"\n",
" # Add the mossteps\n",
" for j in range(len(interval_matrix[i])):\n",
" interval_table[i].append(interval_matrix[i][j] + \" (\" + degree_matrix[i][j] + \")\")\n",
" \n",
" # Append table header to front of the actual table\n",
" interval_table = [interval_table_header] + interval_table\n",
"\n",
"\n",
"\n",
" # Print the table\n",
" print(tabulate(interval_table, headers=\"firstrow\", tablefmt=table_fmt))\n"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Mode UDP Mode name Rotational order smiunison (0-smidegree) 1-smistep (1-smidegree) 2-smistep (2-smidegree) 3-smistep (3-smidegree) 4-smistep (4-smidegree) 5-smistep (5-smidegree) 6-smistep (6-smidegree) smioctave (7-smidegree)\n",
"------- ----- ----------- ------------------ ------------------------- ------------------------- ------------------------- ------------------------- ------------------------- ------------------------- ------------------------- -------------------------\n",
"LLsLsLs 6|0 Mode 1 0 0(0) L(1) 2L(1) 2L+s(1) 3L+s(1) 3L+2s(1) 4L+2s(1) 4L+3s(0)\n",
"LsLLsLs 5|1 Mode 2 5 0(0) L(1) L+s(0) 2L+s(1) 3L+s(1) 3L+2s(1) 4L+2s(1) 4L+3s(0)\n",
"LsLsLLs 4|2 Mode 3 3 0(0) L(1) L+s(0) 2L+s(1) 2L+2s(0) 3L+2s(1) 4L+2s(1) 4L+3s(0)\n",
"LsLsLsL 3|3 Mode 4 1 0(0) L(1) L+s(0) 2L+s(1) 2L+2s(0) 3L+2s(1) 3L+3s(0) 4L+3s(0)\n",
"sLLsLsL 2|4 Mode 5 6 0(0) s(0) L+s(0) 2L+s(1) 2L+2s(0) 3L+2s(1) 3L+3s(0) 4L+3s(0)\n",
"sLsLLsL 1|5 Mode 6 4 0(0) s(0) L+s(0) L+2s(0) 2L+2s(0) 3L+2s(1) 3L+3s(0) 4L+3s(0)\n",
"sLsLsLL 0|6 Mode 7 2 0(0) s(0) L+s(0) L+2s(0) 2L+2s(0) 2L+3s(0) 3L+3s(0) 4L+3s(0)\n"
]
}
],
"source": [
"lg_count = 4\n",
"sm_count = 3\n",
"MosModecalcOnetable(lg_count, sm_count, mosprefix=GetTamnamsPrefix(lg_count, sm_count))\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.9.12 ('base')",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.12"
},
"orig_nbformat": 4,
"vscode": {
"interpreter": {
"hash": "eca05a44a48bc7782e9f30921d98d7664616a388f5fc77845300f50c37270011"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment