Module:Scale tree
Jump to navigation
Jump to search
Documentation transcluded from /doc
Note: Do not invoke this module directly; use the corresponding template instead: Template:Scale tree.
Documentation transcluded from /doc
Note: Do not invoke this module directly; use the corresponding template instead: Template:Scale tree.
local p = {}
local MOS = require("Module:MOS")
local ET = require("Module:ET")
local rat = require("Module:Rational")
local sb = require("Module:SB tree")
local utils = require("Module:Utils")
-- Helper function that parses entries from a semicolon-delimited string and returns them in an array
-- TODO: Separate this and parse_pairs into its own module of helper functions, as they're included
-- in various modules at this point
function p.parse_entries(unparsed)
local parsed = {}
for entry in string.gmatch(unparsed, "([^;]+)") do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
return parsed
end
-- Helper function that parses pairs of elements separated by a colon
-- A pair must be two elements or it will be returned as an empty array
function p.parse_pair(unparsed)
local parsed = {}
for entry in string.gmatch(unparsed, "([^:]+)") do
local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
table.insert(parsed, trimmed) -- Add to array
end
if #parsed == 2 then
return parsed
else
return {}
end
end
-- Function that takes a list of semicolon-delimited pairs and returns a map
-- (or dictionary or associative array) of key-value pairs
-- Each entry is colon-delimited as key : pair
function p.parse_kv_pairs(unparsed)
-- Tokenize the string of unparsed pairs
local parsed = p.parse_entries(unparsed)
-- Then tokenize the tokens into key-value pairs
local pairs_ = {}
for i = 1, #parsed do
local pair = p.parse_pair(parsed[i])
if #pair == 2 then
pairs_[pair[1]] = pair[2]
end
end
return pairs_
end
-- Rewrite of scale tree function (has bugfixes and new formatting)
function p._scale_tree(input_mos, depth, comments)
local input_mos = input_mos or MOS.new(5, 2)
local depth = depth or 5
local comments = comments or {}
local equave = input_mos.equave
local L = input_mos.nL -- Large steps in mos
local s = input_mos.ns -- Small steps in mos
local n = utils._gcd(L, s) -- Number of periods
local abstract_bright_gen = MOS.bright_gen(input_mos)
local step_ratios = sb.sb_tree_ratios(depth)
local depths = sb.sb_tree_depths(depth)
-- What is the equave suffix (edo, edt, edf, ed-p/q)
local equave_suffix = ""
if rat.eq(input_mos.equave, rat.new(2)) then
equave_suffix = "o"
elseif rat.eq(input_mos.equave, rat.new(3)) then
equave_suffix = "t"
elseif rat.eq(input_mos.equave, rat.new(3, 2)) then
equave_suffix = "f"
else
equave_suffix = rat.as_ratio(input_mos.equave)
end
-- Default comments for TAMNAMS-named step ratios
local default_comments = {}
local mos_as_string = MOS.as_string(input_mos)
default_comments["1/1"] = string.format("'''Equalized %s'''", mos_as_string)
default_comments["4/3"] = string.format("'''Supersoft %s'''", mos_as_string)
default_comments["3/2"] = string.format("'''Soft %s'''", mos_as_string)
default_comments["5/3"] = string.format("'''Semisoft %s'''", mos_as_string)
default_comments["2/1"] = string.format("'''Basic %s'''", mos_as_string)
default_comments["5/2"] = string.format("'''Semihard %s'''", mos_as_string)
default_comments["3/1"] = string.format("'''Hard %s'''", mos_as_string)
default_comments["4/1"] = string.format("'''Superhard %s'''", mos_as_string)
default_comments["1/0"] = string.format("'''Collapsed %s'''", mos_as_string)
-- Append boundary of proper scales to basic comment, if applicable
-- Monosmall mosses and knL ns mosses are always proper, but all other mosses
-- are proper if the step ratio is within the soft-of-basic range
if n < s then
default_comments["2/1"] = default_comments["2/1"] .. "<br />Scales with tunings softer than this are proper"
end
-- Produce table header for the comments
local comments_header_text = "Comments"
if s == 1 then
comments_header_text = comments_header_text .. "<sup><abbr title=\"Every tuning produces a proper scale.\">(always proper)</abbr></sup>"
elseif s == n and n > 1 then
comments_header_text = comments_header_text .. "<sup><abbr title=\"Every true-MOS tuning produces a proper scale.\">(always proper)</abbr></sup>"
end
-- Table headers
-- There are 6 columns:
-- - Steps of ED
-- - Bright and dark gens in cents
-- - Step ratio and hardness
-- - Comments
local result = "{| class=\"wikitable center-all\"\n"
.. "|+ style=\"font-size: 105%; white-space: nowrap;\" | " .. string.format("Scale Tree and Tuning Spectrum of %s\n", mos_as_string)
.. string.format("! rowspan=\"2\" colspan=\"%d\" | Generator<sup><abbr title=\"In steps of ed%s.\">(ed%s)</abbr></sup>\n", depth + 1, equave_suffix, equave_suffix)
.. "! colspan=\"2\" | Cents\n"
.. "! colspan=\"2\" | Step ratio\n"
.. "! rowspan=\"2\" | " .. comments_header_text .. "\n"
.. "|-\n"
.. "! Bright\n"
.. "! Dark\n"
.. "! L:s\n"
.. "! Hardness\n"
-- Rounding is done using string.format, to 3 decimal places (%.3f)
-- Create each row of the table
for i = 1, #step_ratios do
local step_ratio = step_ratios[i]
local steps_per_equave = step_ratio[1] * L + step_ratio[2] * s
local steps_per_period = steps_per_equave / n
local et = ET.new(steps_per_equave, equave)
-- Calculate the bright gen and cent value
local bright_generator_steps = step_ratio[1] * abstract_bright_gen["L"] + step_ratio[2] * abstract_bright_gen["s"]
local bright_generator_cents = ET.cents(et, bright_generator_steps)
-- Calculate dark generator step count and cent value
local dark_generator_steps = steps_per_period - bright_generator_steps
local dark_generator_cents = ET.cents(et, dark_generator_steps)
-- New row
result = result .. "|-\n"
-- Cells for bright generator, as steps in et
local current_depth = depths[i]
for i = 1, depth + 1 do
result = result .. "| "
if i == current_depth then
result = result .. string.format("[[%s|%d\\%s]]", ET.as_string(et), bright_generator_steps, et.size)
end
result = result .. "\n"
end
-- Cells for generators in cents
result = result .. string.format("| %.3f\n", bright_generator_cents)
result = result .. string.format("| %.3f\n", dark_generator_cents)
-- Cell for step ratio
result = result .. string.format("| %d:%d\n", step_ratio[1], step_ratio[2])
-- Cell for hardness, with divide-by-zero check
local hardness = ""
if step_ratio[2] == 0 then
hardness = "→ ∞"
else
hardness = string.format("%.3f", step_ratio[1] / step_ratio[2])
end
result = result .. string.format("| %s\n", hardness)
-- Cell for comment
-- Default comments are on their own line before custom comments
local key = step_ratios[i][1] .. "/" .. step_ratios[i][2] -- The step ratio is (literally and figuratively) the key to add comments!
local comment = ""
local default_comment = default_comments[key] or ""
local custom_comment = comments[key] or ""
if default_comment == "" then
comment = custom_comment
else
comment = default_comment .. "<br />" .. custom_comment
end
result = result .. string.format("| style=\"text-align: left;\" | %s\n", comment)
end
result = result .. "|}"
return result
end
function p.scale_tree(frame)
local mos = MOS.parse(frame.args["tuning"])
local depth = frame.args["depth"] or 5
local comments_unparsed = frame.args["Comments"] or ""
local comments = p.parse_kv_pairs(comments_unparsed) or {}
return p._scale_tree(mos, depth, comments)
end
return p