|
|
| (125 intermediate revisions by 3 users not shown) |
| Line 1: |
Line 1: |
| local mos = require('Module:MOS')
| |
| local rat = require('Module:Rational')
| |
| local ord = require('Module:Ordinal')
| |
| local utils = require('Module:Utils')
| |
| local et = require('Module:ET')
| |
| local p = {} | | local p = {} |
|
| |
|
| -- Helper function that turns a mosstep into a step count as a string
| | local getArgs = require("Module:Arguments").getArgs |
| -- EG, the 3-mosstep of "LLsLsLs" becomes "2L + s"
| | local mos = require("Module:MOS") |
| function p.mos_interval_to_step_count_string(step_pattern, mossteps)
| | local rat = require("Module:Rational") |
|
| | local tamnams = require("Module:TAMNAMS") |
| local L_count = 0
| | local yesno = require("Module:Yesno") |
| local s_count = 0
| |
| for i = 1, mossteps do
| |
| local step = string.sub(step_pattern, i, i)
| |
| if step == "L" then
| |
| L_count = L_count + 1
| |
| elseif step == "s" then
| |
| s_count = s_count + 1
| |
| end
| |
| end
| |
|
| |
| -- There are 9 combinations to write xL + ys, based on whether x and y are
| |
| -- 0, 1, or greater than 1
| |
| local return_string = ""
| |
| if L_count == 0 and s_count == 0 then
| |
| return_string = "0"
| |
| elseif L_count == 0 and s_count == 1 then
| |
| return_string = "s"
| |
| elseif L_count == 0 and s_count > 1 then
| |
| return_string = s_count .. "s"
| |
| elseif L_count == 1 and s_count == 0 then
| |
| return_string = "L"
| |
| elseif L_count == 1 and s_count == 1 then
| |
| return_string = "L + s"
| |
| elseif L_count == 1 and s_count > 1 then
| |
| return_string = "L + " .. s_count .. "s"
| |
| elseif L_count > 1 and s_count == 0 then
| |
| return_string = L_count .. "L"
| |
| elseif L_count > 1 and s_count == 1 then
| |
| return_string = L_count .. "L + s"
| |
| else
| |
| return_string = L_count .. "L + " .. s_count .. "s"
| |
| end
| |
| return return_string
| |
| end
| |
|
| |
|
| -- Helper function | | -- -- TODO: |
| -- Produces the cent range of a mosstep | | -- - (High priority): Refactor code so instead of string concatenation, lines |
| function p.mos_interval_to_cent_range(step_pattern, mossteps, input_mos)
| | -- are appended to a table, where table.concat() is called at the end. |
| local step_pattern = step_pattern or "LLsLLLs"
| |
| local mossteps = mossteps or 1
| |
| local input_mos = input_mos or mos.new(5, 5, 2)
| |
|
| |
| local L_count = 0
| |
| local s_count = 0
| |
| for i = 1, mossteps do
| |
| local step = string.sub(step_pattern, i, i)
| |
| if step == "L" then
| |
| L_count = L_count + 1
| |
| elseif step == "s" then
| |
| s_count = s_count + 1
| |
| end
| |
| end
| |
|
| |
| -- The range of a small step is from 0 to 1 step of the equalized mos
| |
| -- The range of a large step is from 1 step of equalized to 1 step of collapsed
| |
| -- These ranges do not apply if the mosstep interval is for a period; instead
| |
| -- it's a fixed value.
| |
| local equave_in_cents = rat.cents(input_mos.equave)
| |
| local s_size_min = 0
| |
| local s_size_max = equave_in_cents / (input_mos.nL + input_mos.ns)
| |
| local L_size_min = s_size_max
| |
| local L_size_max = equave_in_cents / input_mos.nL
| |
| local mossteps_per_period = (input_mos.nL + input_mos.ns) / utils._gcd(input_mos.nL, input_mos.ns)
| |
| local is_period = mossteps % mossteps_per_period == 0
| |
|
| |
| local result = ""
| |
| if not is_period then
| |
| local min_cents = L_count * L_size_min + s_count * s_size_max
| |
| local max_cents = L_count * L_size_max + s_count * s_size_min
| |
| result = string.format("%.1f¢ to %.1f¢", min_cents, max_cents)
| |
| else
| |
| local period_in_cents = equave_in_cents / utils._gcd(input_mos.nL, input_mos.ns)
| |
| local number_of_periods = mossteps / mossteps_per_period
| |
| result = string.format("%.1f¢", period_in_cents * number_of_periods)
| |
| end
| |
| return result
| |
| end
| |
|
| |
|
| -- Helper function | | -- EXPERIMENTAL FEATURE: lookup table for intervals |
| -- Produces the cent range for a mosstep | | -- Mostly based off Margo Schulter's categories (without large/medium/small), |
| --[[ | | -- but other interpretations are possible. Plus, this only goes up to 1200c. |
| function p.mos_interval_to_cent_range(step_pattern, mossteps, input_mos)
| | p.interval_ranges = { |
|
| | { name = "Pure unison (1:1)" , range = { 0, 0} }, |
| local nL = input_mos.nL
| | { name = "Comma/diesis" , range = { 0, 60} }, |
| local ns = input_mos.ns
| | { name = "Minor second" , range = { 60, 125} }, |
| local equave_in_cents = rat.cents(input_mos.equave)
| | { name = "Neutral second" , range = { 125, 170} }, |
| | { name = "Major second" , range = { 180, 240} }, |
| | { name = "Interseptimal (Maj2-min3)", range = { 240, 260} }, |
| | { name = "Minor third" , range = { 260, 330} }, |
| | { name = "Neutral third" , range = { 330, 372} }, |
| | { name = "Major third" , range = { 372, 440} }, |
| | { name = "Interseptimal (Maj3-4)" , range = { 440, 468} }, |
| | { name = "Perfect fourth" , range = { 468, 528} }, |
| | { name = "Superfourth" , range = { 528, 560} }, |
| | { name = "Tritonic region" , range = { 560, 640} }, |
| | { name = "Subfifth" , range = { 640, 672} }, |
| | { name = "Perfect fifth" , range = { 672, 732} }, |
| | { name = "Interseptimal (5-min6)" , range = { 732, 760} }, |
| | { name = "Minor sixth" , range = { 760, 828} }, |
| | { name = "Neutral sixth" , range = { 828, 870} }, |
| | { name = "Major sixth" , range = { 870, 940} }, |
| | { name = "Interseptimal (Maj6-min7)", range = { 940, 960} }, |
| | { name = "Minor seventh" , range = { 960, 1020} }, |
| | { name = "Neutral seventh" , range = {1030, 1075} }, |
| | { name = "Major seventh" , range = {1075, 1140} }, |
| | { name = "Octave less comma/diesis" , range = {1140, 1200} }, |
| | { name = "Pure octave (2:1)" , range = {1200, 1200} } |
| | } |
|
| |
|
| local L_count = 0
| | -- EXPERIMENTAL FEATURE: interval lookup function |
| local s_count = 0
| | function p.lookup_interval_range(cents) |
| for i = 1, mossteps do
| | for _, interval in ipairs(p.interval_ranges) do |
| local step = string.sub(step_pattern, i, i)
| | if cents >= interval.range[1] and cents <= interval.range[2] then |
| if step == "L" then
| | return interval.name |
| L_count = L_count + 1
| | end |
| elseif step == "s" then
| | end |
| s_count = s_count + 1
| | return "Out of range" |
| end
| |
| end
| |
|
| |
| local result = ""
| |
| if utils._gcd(nL, ns) > 1 then
| |
| result = string.format("%.1f¢", (L_count + s_count) * equave_in_cents / (nL + ns))
| |
| else
| |
| local lim1 = (L_count + s_count) * equave_in_cents / (nL + ns)
| |
| local lim2 = (L_count) * equave_in_cents / (nL)
| |
| result = string.format("%.1f¢ to %.1f¢", math.min(lim1, lim2), math.max(lim1, lim2))
| |
| end
| |
| return result
| |
| end
| |
| ]]--
| |
| | |
| -- Helper function
| |
| -- Extracts the prefix from the mos module, without the dash and without any text after that
| |
| -- May need to revisit to clean up code since this splits text at the "-"
| |
| function p.get_mos_prefix(scale_sig)
| |
| local unparsed = mos.tamnams_prefix[scale_sig]
| |
|
| |
| local parsed = {}
| |
|
| |
| if unparsed == nil then
| |
| return "mos"
| |
| else
| |
| for entry in string.gmatch(unparsed, '([^-]+)') do
| |
| local trimmed = entry:gsub("^%s*(.-)%s*$", "%1")
| |
| table.insert(parsed, trimmed) -- Add to array
| |
| end
| |
| return parsed[1]
| |
| end
| |
| end | | end |
|
| |
|
| function p.mos_intervals(input_mos, mos_prefix) | | -- Main function; to be called by wrapper |
| | function p._mos_intervals(args) |
| -- Default param for input mos is 5L 2s | | -- Default param for input mos is 5L 2s |
| local input_mos = input_mos or mos.new(5, 2, 2) | | local input_mos = args["Input MOS" ] or mos.new(5, 2, 2) |
| local mos_prefix = mos_prefix or "dia" | | local mos_prefix = args["MOS Prefix" ] or "mos" |
| | local mos_abbrev = args["MOS Abbrev" ] or "m" |
| | local is_collapsed = args["Is Collapsed"] == true |
| | local show_inregs = false |
| | | |
| -- Get the scale sig | | -- Get the scale sig |
| local scale_sig = mos.as_string(input_mos) | | local scale_sig = mos.as_string(input_mos) |
| | | |
| -- Get the brightest and darkest modes for the mos | | -- Get the brightest and darkest modes as step matrices |
| local brightest_mode = mos.brightest_mode(input_mos) | | local bright_step_matrix = mos.mode_to_step_matrix(mos.brightest_mode(input_mos)) |
| local darkest_mode = string.reverse(brightest_mode) | | local dark_step_matrix = mos.mode_to_step_matrix(mos.darkest_mode(input_mos)) |
| | | |
| -- Get the number of steps per period and equave | | -- Get the number of steps per period and equave |
| local steps_per_equave = (input_mos.nL + input_mos.ns) | | local equave_step_count = mos.equave_step_count(input_mos) |
| local steps_per_period = steps_per_equave / utils._gcd(input_mos.nL, input_mos.ns) | | local period_step_count = mos.period_step_count(input_mos) |
| | | |
| -- Get the step counts for the bright and dark generators | | -- Get the step counts for the bright and dark generators |
| local bright_gen = mos.bright_gen(input_mos) | | local bright_gen_step_count = mos.bright_gen_step_count(input_mos) |
| local steps_per_bright_gen = bright_gen['L'] + bright_gen['s'] | | local dark_gen_step_count = mos.dark_gen_step_count(input_mos) |
| local steps_per_dark_gen = steps_per_period - steps_per_bright_gen
| |
| | | |
| -- Create the table, starting with the headers | | -- Create the table |
| local result = '{| class="wikitable"\n' | | local result = '{| class="wikitable mw-collapsible' .. (is_collapsed and ' mw-collapsed"\n' or '"\n') |
| result = result .. '|+Intervals of ' .. scale_sig .. '\n'
| |
| result = result .. '! colspan="2" |Intervals (with relation to root)\n'
| |
| result = result .. '! colspan="2" |Size\n'
| |
| result = result .. '! rowspan="2" |Abbrev.\n'
| |
| result = result .. '|-\n'
| |
| result = result .. '!Generic\n'
| |
| result = result .. '!Specific\n'
| |
| result = result .. '!L\'s and s\'s\n'
| |
| result = result .. '!Range in cents\n'
| |
| | | |
| -- First row is the unison | | -- Create table title |
| result = result .. "|-\n" | | result = result |
| result = result .. "|'''0-" .. mos_prefix .. "step (root)'''\n"
| | .. '|+ style="font-size: 105%; white-space: nowrap;" | ' .. string.format('Intervals of %s', scale_sig) .. '\n' |
| result = result .. "|Perfect 0-" .. mos_prefix .. "step\n"
| | .. '|-\n' |
| result = result .. "|0\n" | | |
| result = result .. string.format("| %.1f¢\n", 0)
| | -- Create table headers |
| result = result .. string.format("| P0ms\n")
| | result = result |
|
| | .. '! colspan="3" | Intervals\n' |
| -- Successive rows are the mossteps, starting at the 1-mosstep
| | .. '! rowspan="2" | Steps<br />subtended\n' |
| -- Name the interval according to whether it's major/minor or
| | .. '! rowspan="2" | Range in cents\n' |
| -- perf/aug/dim; for nL ns mosses, it's only major/minor for any
| | .. '|-\n' -- Start of second row of header cells |
| -- non-period intervals.
| | .. '! Generic\n' |
| local is_nL_ns = input_mos.nL == input_mos.ns
| | .. '! Specific\n' |
| for i = 1, steps_per_equave - 1 do | | .. '! Abbrev.\n' |
| if i % steps_per_period == steps_per_bright_gen and not is_nL_ns then | | .. (show_inregs and '! Interval Regions\n' or '') |
| -- If i corresponds to the bright generator, then the large size is
| | |
| -- perfect and the small size is diminished. Add two rows.
| | -- Write each row |
| result = result .. "|-\n"
| | for i = 1, #bright_step_matrix do |
| result = result .. '|rowspan="2"' .. "|'''" .. i .. "-" .. mos_prefix .. "step'''\n"
| | -- Compare the bright and dark intervals. If they're the same, then the |
| result = result .. "|Diminished " .. i .. "-" .. mos_prefix .. "step\n"
| | -- current interval class is a period interval. |
| result = result .. "|" .. p.mos_interval_to_step_count_string(darkest_mode, i) .. "\n"
| | local current_bright_interval = bright_step_matrix[i] |
| result = result .. string.format("| %s\n", p.mos_interval_to_cent_range(darkest_mode, i, input_mos))
| | local current_dark_interval = dark_step_matrix[i] |
| result = result .. string.format("| d%ims\n", i)
| | local is_period = mos.interval_eq(current_bright_interval, current_dark_interval) |
| result = result .. "|-\n"
| | |
| result = result .. "|Perfect " .. i .. "-" .. mos_prefix .. "step\n"
| | -- If it's a period interval, then there is only one row to write. |
| result = result .. "|" .. p.mos_interval_to_step_count_string(brightest_mode, i) .. "\n"
| | -- Otherwise, there are two rows to write, one for each size. |
| result = result .. string.format("| %s\n", p.mos_interval_to_cent_range(brightest_mode, i, input_mos)) | | if is_period then |
| result = result .. string.format("| P%ims\n", i) | | local cents = mos.interval_to_cents(current_bright_interval, input_mos, {1, 1}) |
| | local cents_formatted = string.format("%.1f{{c}}", cents) |
| | | |
| elseif i % steps_per_period == steps_per_dark_gen and not is_nL_ns then
| | result = result |
| -- If i corresponds to the dark generator, then the large size is
| | .. "|-\n" |
| -- augmented and the small size is perfect. Add two rows.
| | .. "| '''" .. i-1 .. "-" .. mos_prefix .. "step'''\n" |
| result = result .. "|-\n" | | .. "| " .. tamnams.interval_quality(current_bright_interval, input_mos, "sentence-case", mos_prefix) .. "\n" |
| result = result .. '|rowspan="2"' .. "|'''" .. i .. "-" .. mos_prefix .. "step'''\n"
| | .. "| " .. tamnams.interval_quality(current_bright_interval, input_mos, "abbrev" , mos_abbrev) .. "\n" |
| result = result .. "|Perfect " .. i .. "-" .. mos_prefix .. "step\n"
| | .. "| <span style=\"white-space: nowrap;\">" .. mos.interval_as_string(current_bright_interval) .. "</span>\n" |
| result = result .. "|" .. p.mos_interval_to_step_count_string(darkest_mode, i) .. "\n"
| | .. "| " .. cents_formatted .. "\n" |
| result = result .. string.format("| %s\n", p.mos_interval_to_cent_range(darkest_mode, i, input_mos))
| | .. (show_inregs and string.format("| %s\n", p.lookup_interval_range(cents)) or "") |
| result = result .. string.format("| P%ims\n", i)
| | else |
| result = result .. "|-\n"
| | -- Calculate the cent values min and max for the current intervals |
| result = result .. "|Augmented " .. i .. "-" .. mos_prefix .. "step\n"
| | local sm_min_cents = mos.interval_to_cents(current_dark_interval, input_mos, {1,1}) |
| result = result .. "|" .. p.mos_interval_to_step_count_string(brightest_mode, i) .. "\n"
| | local sm_max_cents = mos.interval_to_cents(current_dark_interval, input_mos, {1,0}) |
| result = result .. string.format("| %s\n", p.mos_interval_to_cent_range(brightest_mode, i, input_mos))
| | local lg_min_cents = mos.interval_to_cents(current_bright_interval, input_mos, {1,1}) |
| result = result .. string.format("| A%ims\n", i) | | local lg_max_cents = mos.interval_to_cents(current_bright_interval, input_mos, {1,0}) |
| | | |
| elseif i % steps_per_period == 0 and i ~= mossteps_per_equave then
| | -- Then sort, as the min and max may be swapped |
| -- If i corresponds to the period, then the large and small sizes are | | -- This happens if the dark interval has more small steps than large steps |
| -- the same and they're perfect. This also applies to the equave. Add one row. | | local sm_min_sorted = math.min(sm_min_cents, sm_max_cents) |
| result = result .. "|-\n"
| | local sm_max_sorted = math.max(sm_min_cents, sm_max_cents) |
| result = result .. "|'''" .. i .. "-" .. mos_prefix .. "step (period)'''\n"
| | local lg_min_sorted = math.min(lg_min_cents, lg_max_cents) |
| result = result .. "|Perfect " .. i .. "-" .. mos_prefix .. "step\n"
| | local lg_max_sorted = math.max(lg_min_cents, lg_max_cents) |
| result = result .. "|" .. p.mos_interval_to_step_count_string(darkest_mode, i) .. "\n"
| |
| result = result .. string.format("| %s\n", p.mos_interval_to_cent_range(darkest_mode, i, input_mos))
| |
| result = result .. string.format("| P%ims\n", i)
| |
|
| |
| else
| |
| -- For any other interval, and for generators for nL ns mosses, the
| |
| -- large size is major and the small size is minor. Add two rows.
| |
| result = result .. "|-\n"
| |
| result = result .. '|rowspan="2"' .. "|" .. i .. "-" .. mos_prefix .. "step\n"
| |
| result = result .. "|Minor " .. i .. "-" .. mos_prefix .. "step\n"
| |
| result = result .. "|" .. p.mos_interval_to_step_count_string(darkest_mode, i) .. "\n" | |
| result = result .. string.format("| %s\n", p.mos_interval_to_cent_range(darkest_mode, i, input_mos)) | |
| result = result .. string.format("| m%ims\n", i) | |
| result = result .. "|-\n" | |
| result = result .. "|Major " .. i .. "-" .. mos_prefix .. "step\n"
| |
| result = result .. "|" .. p.mos_interval_to_step_count_string(brightest_mode, i) .. "\n"
| |
| result = result .. string.format("| %s\n", p.mos_interval_to_cent_range(brightest_mode, i, input_mos))
| |
| result = result .. string.format("| M%ims\n", i)
| |
| | | |
| | -- Produce text ranges for intervals |
| | local dark_interval_range = string.format("%.1f{{c}} to %.1f{{c}}", sm_min_sorted, sm_max_sorted) |
| | local bright_interval_range = string.format("%.1f{{c}} to %.1f{{c}}", lg_min_sorted, lg_max_sorted) |
| | |
| | result = result |
| | .. "|-\n" |
| | .. '| rowspan="2" | ' .. i-1 .. '-' .. mos_prefix .. 'step\n' |
| | .. "| " .. tamnams.interval_quality(current_dark_interval, input_mos, "sentence-case", mos_prefix) .. "\n" |
| | .. "| " .. tamnams.interval_quality(current_dark_interval, input_mos, "abbrev" , mos_abbrev) .. "\n" |
| | .. "| <span style=\"white-space: nowrap;\">" .. mos.interval_as_string(current_dark_interval) .. "</span>\n" |
| | .. "| " .. dark_interval_range .. "\n" |
| | .. (show_inregs and string.format("| %s to %s\n", p.lookup_interval_range(sm_min_sorted), p.lookup_interval_range(sm_max_sorted)) or "") |
| | .. "|-\n" |
| | .. "| " .. tamnams.interval_quality(current_bright_interval, input_mos, "sentence-case", mos_prefix) .. "\n" |
| | .. "| " .. tamnams.interval_quality(current_bright_interval, input_mos, "abbrev" , mos_abbrev) .. "\n" |
| | .. "| <span style=\"white-space: nowrap;\">" .. mos.interval_as_string(current_bright_interval) .. "</span>\n" |
| | .. "| " .. bright_interval_range .. "\n" |
| | .. (show_inregs and string.format("| %s to %s\n", p.lookup_interval_range(lg_min_sorted), p.lookup_interval_range(lg_max_sorted)) or "") |
| end | | end |
| | |
| end | | end |
| | result = result .. "|}" |
| | |
| | return result |
| | end |
| | |
| | -- Wrapper function; to be called by template |
| | function p.mos_intervals(frame) |
| | local args = getArgs(frame) |
| | | |
| -- Add the last row of the table, which is either the equave or octave | | -- Preprocess scalesig into input mos |
| local equave_name = "" | | local input_mos = mos.parse(args["Scale Signature"]) |
| if rat.eq(input_mos.equave, 2) then
| | args["Input MOS"] = input_mos |
| result = result .. "|-\n"
| | args["Scale Signature"] = nil |
| result = result .. "|'''" .. steps_per_equave .. "-" .. mos_prefix .. "step (octave)'''\n"
| | |
| result = result .. "|Perfect " .. steps_per_equave .. "-" .. mos_prefix .. "step'''\n"
| | -- Preprocess collapse option |
| result = result .. "|" .. p.mos_interval_to_step_count_string(darkest_mode, steps_per_equave) .. "\n"
| | args["Collapsed"] = yesno(args["Collapsed"], false) |
| result = result .. string.format("| %.1f¢\n", 1200)
| |
| result = result .. string.format("| P%ims\n", steps_per_equave)
| |
| else | |
| result = result .. "|-\n"
| |
| result = result .. "|'''" .. steps_per_equave .. "-" .. mos_prefix .. "step (equave)'''\n"
| |
| result = result .. "|Perfect " .. steps_per_equave .. "-" .. mos_prefix .. "step'''\n"
| |
| result = result .. "|" .. p.mos_interval_to_step_count_string(darkest_mode, steps_per_equave) .. "\n"
| |
| result = result .. string.format("| %.1f¢\n", rat.cents(input_mos.equave))
| |
| result = result .. string.format("| P%ims\n", steps_per_equave)
| |
| end
| |
|
| |
| result = result .. "|}"
| |
| | | |
| return result | | -- EXPERIMENTAL: option to show interval regions |
| | args["Show Interval Regions"] = yesno(args["Show Interval Regions"], false) |
| | | |
| end
| | -- Preprocess (verify) prefix/abbrev |
| | args["MOS Prefix"] = tamnams.verify_prefix(input_mos, args["MOS Prefix"]) |
| | args["MOS Abbrev"] = tamnams.verify_abbrev(input_mos, args["MOS Abbrev"]) |
|
| |
|
| function p.mos_intervals_frame(frame)
| | local result = p._mos_intervals(args) |
| -- Get input mos
| | local debugg = yesno(args["debug"]) |
| local input_mos = mos.parse(frame.args['Scale Signature']) | |
| | | |
| -- Default param for mos prefix | | -- Debugger option |
| -- If "NONE" is given, no prefix will be used
| | if debugg == true then |
| -- If left blank, try to find the appropriate mos prefix, or else defualt to "mos"
| | result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>" |
| -- If not left blank, use the prefix passed in instead
| |
| local scale_sig = mos.as_string(input_mos)
| |
| local mos_prefix = p.get_mos_prefix(scale_sig)
| |
| if frame.args['MOS Prefix'] == "NONE" then | |
| mos_prefix = "" | |
| elseif string.len(frame.args['MOS Prefix']) > 0 then
| |
| mos_prefix = frame.args['MOS Prefix']
| |
| end | | end |
| | | |
| local result = p.mos_intervals(input_mos, mos_prefix) | | return frame:preprocess(result) |
| | |
| return result | |
| end | | end |
|
| |
|
| return p | | return p |