モジュール:MOS intro

提供: Xenharmonic Wiki
2026年4月4日 (土) 15:16時点におけるFurcht968 (トーク | 投稿記録)による版
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)
ナビゲーションに移動 検索に移動

このモジュールについての説明文ページを モジュール:MOS intro/doc に作成できます

local p = {}

local et = require("Module:ET")
local mos = require("Module:MOS")
local rat = require("Module:Rational")
local tamnams = require("Module:TAMNAMS")
local tip = require("Module:Template input parse")
local utils = require("Module:Utils")
local yesno = require("Module:Yesno")

-- TODO:
-- - Possible cleanup/rewording
-- - Possible official deprecation of other names; focus should be on what a mos
--   is, not what it's called.

-- Helper function
-- Lists out names, with each name being bold
function p.mos_intro_list_names(mos_names, conjunction)
	local mos_names = mos_names or { "name1", "name2", "name3" }
	local conjunction = conjunction or "and"
	
	-- List the names
	local names_list = ""
	if #mos_names == 1 then
		-- Only one mos name
		names_list = string.format("'''%s'''", mos_names[1])
	elseif #mos_names == 2 then
		-- Two mos names (name and alternate-name)
		names_list = string.format("'''%s''' %s '''%s'''", mos_names[1], conjunction, mos_names[2])
	elseif #mos_names > 2 then
		-- Three or more mos names
		for i = 1, #mos_names - 1 do
			names_list = names_list .. string.format("'''%s''', ", mos_names[i])
		end
		names_list = names_list .. string.format("%s '''%s'''", conjunction, mos_names[#mos_names])
	else
		-- No names
		names_list = ""
	end
	
	return names_list
end

-- Helper function
-- Introduces the mos by its scale sig and names
-- Names must be entered as an array
function p.mos_intro_names(scale_sig, tamnams_name, other_names)
	local scale_sig = scale_sig or "5L 2s"
	local tamnams_name = tamnams_name or { "diatonic" }
	local other_names = other_names or { "other-name" }
	
	-- Get all the mos's names, starting with tamnams names if applicable
	local tamnams_names_list = p.mos_intro_list_names(tamnams_name, "and")
	local other_names_list = p.mos_intro_list_names(other_names, "or")
	
	-- Construct the sentence
	local sentence = string.format("'''%s'''は、", scale_sig)
	
	-- Add names
	if tamnams_names_list ~= "" and other_names_list ~= "" then
		-- There are both tamnams names and alternate names
		sentence = sentence .. string.format("{{En仮リンク|TAMNAMS}}においては%sと呼ばれ、他に%sといった呼称が存在する。この音階は", tamnams_names_list, other_names_list)
	elseif tamnams_names_list ~= "" and other_names_list == "" then
		-- There are only tamnams names
		sentence = sentence .. string.format("{{En仮リンク|TAMNAMS}}において%sと呼ばれる。この音階は", tamnams_names_list)
	elseif tamnams_names_list == "" and other_names_list ~= "" then
		-- There are no tamnams names but there are alternate names
		sentence = sentence .. string.format("%sと呼ばれる。この音階は", other_names_list)
	end
	
	return sentence
end

-- Helper function
-- Determines what mos the given mos descends from
-- as well as what step ratio that produces this scale
function p.find_mos_ancestor(input_mos)
	local input_mos = input_mos or mos.new(7, 7)
	
	local z = input_mos.nL
	local w = input_mos.ns
	local generations = 0
	
	-- For an ancestral mos zU wv and descendant xL ys, how many steps of size
	-- L and s can fit inside U and v? (basically the chunking operation)
	local lg_chunk = { nL = 1, ns = 0 }
	local sm_chunk = { nL = 0, ns = 1 }
	
	while (z ~= w) and (z + w > 10) do
		local m1 = math.max(z, w)
		local m2 = math.min(z, w)
		
		-- For use with updating ancestor mos chunks
		local z_prev = z
		
		-- Count how many generations
		generations = generations + 1
		
		-- Update step ratios
		z = m2
		w = m1 - m2
		
		-- Update large chunk
		local prev_lg_chunk = { nL = lg_chunk.nL, ns = lg_chunk.ns }
		lg_chunk.nL = lg_chunk.nL + sm_chunk.nL
		lg_chunk.ns = lg_chunk.ns + sm_chunk.ns
		
		-- Update small chunk
		if z ~= z_prev then
			sm_chunk = prev_lg_chunk
		end
	end
	
	return mos.new(z, w, input_mos.equave), lg_chunk, sm_chunk, generations
end

-- Helper function
-- What mos does the input mos descend from?
function p.mos_descends_from(input_mos)
	local input_mos = input_mos or mos.new(7, 7)
	
	local ancestor_mos, lg_chunk, sm_chunk, generations = p.find_mos_ancestor(input_mos)
	
	--[[
	-- Calculate the range of step ratios the ancestor should have
	-- Sort ratios by hardness
	local num1 = lg_chunk.nL + lg_chunk.ns
	local den1 = sm_chunk.nL + sm_chunk.ns
	local num2 = lg_chunk.nL
	local den2 = sm_chunk.nL
	local first_ancestor_step_ratio = ""
	local second_ancestor_step_ratio = ""
	if num1/den1 < num2/den2 then
		first_ancestor_step_ratio = string.format("%d:%d", num1, den1)
		second_ancestor_step_ratio = string.format("%d:%d", num2, den2)
	else
		first_ancestor_step_ratio = string.format("%d:%d", num2, den2)
		second_ancestor_step_ratio = string.format("%d:%d", num1, den1)
	end
	
	-- Step ratio range as text
	local step_ratio_range = string.format("%s to %s", first_ancestor_step_ratio, second_ancestor_step_ratio)
	
	-- Step ratio range as a named range
	local named_range = ""
	if step_ratio_range == "1:1 to 2:1" then
		named_range = "soft-of-basic"
	elseif step_ratio_range == "2:1 to 1:0" then
		named_range = "hard-of-basic"
	elseif step_ratio_range == "1:1 to 3:2" then
		named_range = "soft"
	elseif step_ratio_range == "3:2 to 2:1" then
		named_range = "hyposoft"
	elseif step_ratio_range == "2:1 to 3:1" then
		named_range = "hypohard"
	elseif step_ratio_range == "3:1 to 1:0" then
		named_range = "hard"
	elseif step_ratio_range == "1:1 to 4:3" then
		named_range = "ultrasoft"
	elseif step_ratio_range == "4:3 to 3:2" then
		named_range = "parasoft"
	elseif step_ratio_range == "3:2 to 5:3" then
		named_range = "quasisoft"
	elseif step_ratio_range == "5:3 to 2:1" then
		named_range = "minisoft"
	elseif step_ratio_range == "2:1 to 5:2" then
		named_range = "minihard"
	elseif step_ratio_range == "5:2 to 3:1" then
		named_range = "quasihard"
	elseif step_ratio_range == "3:1 to 4:1" then
		named_range = "parahard"
	elseif step_ratio_range == "4:1 to 1:0" then
		named_range = "ultrahard"
	end
	]]--
	
	local descendant_text = ""
	if generations == 1 then
		descendant_text = string.format("%sは、[[%s]]の親MOSであり", mos.as_string(input_mos), mos.as_string(ancestor_mos))
	elseif generations == 2 then
		descendant_text = string.format("%sは、[[%s]]の祖MOSであり", mos.as_string(input_mos), mos.as_string(ancestor_mos))
	elseif generations == 3 then
		descendant_text = string.format("%sは、[[%s]]の曽祖MOSであり", mos.as_string(input_mos), mos.as_string(ancestor_mos))
	else
		descendant_text = string.format("%sは、[[%s]]と近親であり", mos.as_string(input_mos), mos.as_string(ancestor_mos))
	end
	
	--if named_range == "" then
	--	descendant_text = descendant_text .. string.format(", produced by such scales with a [[step ratio]] within the range of %s.", step_ratio_range)
	--	descendant_text = descendant_text .. "."
	--else
	--	descendant_text = descendant_text .. string.format(", produced by such scales with a [[step ratio]] within the %s range (%s).", named_range, step_ratio_range)
	--	descendant_text = descendant_text .. "."
	--end
	
	descendant_text = descendant_text .. string.format("、%d音分増える。", input_mos.nL + input_mos.ns - ancestor_mos.nL - ancestor_mos.ns)
	
	return descendant_text
end

-- Main function (updated)
function p._mos_intro(input_mos, other_names)
	local input_mos = input_mos or mos.new(5, 5, 3)
	local other_names = ""
	
	-- Scale sig
	local scale_sig = mos.as_string(input_mos)
	
	-- Tamnams names, if any
	local tamnams_name = tamnams.lookup_name(input_mos) or ""
	
	-- Parsed names
	local tamnams_pasred = tip.parse_entries(tamnams_name)
	local other_parsed = tip.parse_entries(other_names)
	
	-- Step counts for large steps, small steps, and number of periods
	local nL = input_mos.nL
	local ns = input_mos.ns
	local n = utils._gcd(nL, ns)
	
	-- Equave as ratio and cents
	local equave_as_ratio = rat.as_ratio(input_mos.equave)
	local equave_in_cents = rat.cents(input_mos.equave)
	local period_in_cents = equave_in_cents / n
	
	-- How many decimal places to round to?
	local round = 1
	
	-- Build up intro text, starting with the scale sig and scale names
	-- This is done through the aid of a helper function
	local intro = p.mos_intro_names(scale_sig, tamnams_pasred, other_parsed)

	-- Add step counts
	if nL >= 10 then
		intro = intro .. string.format("%d個の広い音程と", nL)
	else
		intro = intro .. string.format("%dつの広い音程と", nL)
	end
		
	if ns >= 10 then
		intro = intro .. string.format("%d個の狭い音程を有し、", ns)
	else
		intro = intro .. string.format("%dつの狭い音程を有し、", ns)
	end

	-- Add equave equivalence
	if rat.eq(input_mos.equave, rat.new(2)) then
		intro = intro .. "[[オクターブ]]を周期とする[[MOSスケール]]である。"
	elseif rat.eq(input_mos.equave, rat.new(3)) then
		intro = intro .. "[[トリターブ]]を周期とする[MOSスケール]]である。"
	elseif rat.eq(input_mos.equave, rat.new(3,2)) then
		intro = intro .. "[[3/2]]を周期とする[[MOSスケール]]である。"
	else
		intro = intro .. string.format("%sを周期とする[[MOSスケール]]である。", equave_as_ratio)
	end
	
	-- Add repetition
	if n == 1 then
		intro = intro .. (equave_in_cents == 1200 and "[[オクターブ]]" or string.format("[[%s]] (%.1f{{cent}})の音程", equave_as_ratio, equave_in_cents, round)) .. "の中で"
	else
		intro = intro .. string.format("この音階は、%d個の広い音程と%d個の狭い音程からなる繰り返しが", nL/n, ns/n)
		intro = intro .. string.format("%.1f{{cent}}ごとに、つまり", period_in_cents)
		intro = intro .. (equave_in_cents == 1200 and "[[オクターブ]]" or string.format("[[%s]] (%.1f{{cent}})の音程", equave_as_ratio, equave_in_cents, round)) .. string.format("につき%d回繰り返される。", n)

	end
	
	-- TODO: add descendant info
	if equave_in_cents == 1200 and nL + ns > 10 and nL ~= ns then
		intro = intro .. " " .. p.mos_descends_from(input_mos)
	end
	
	-- Add generator ranges
	-- Get the eds (ets) corresponding to the collapsed and equalized mosses
	local collapsed_et = et.new(nL, input_mos.equave)
	local equalized_et = et.new(nL + ns, input_mos.equave)
	
	-- Get the sizes of the bright generator for the collapsed and equalized et in steps
	-- These are used to calculate cent values for the generators
	-- The values for the dark generator are the period complements
	local generator = mos.bright_gen(input_mos)
	local gen_collapsed_in_steps = generator["L"]
	local gen_equalized_in_steps = generator["L"] + generator["s"]
	local bright_gen_max = et.cents(collapsed_et, gen_collapsed_in_steps)
	local bright_gen_min = et.cents(equalized_et, gen_equalized_in_steps)
	
	local dark_gen_min = equave_in_cents / n - bright_gen_max
	local dark_gen_max = equave_in_cents / n - bright_gen_min
	
	local bright_gen_min_r = tostring(utils._round_dec(bright_gen_min, round))
	local bright_gen_max_r = tostring(utils._round_dec(bright_gen_max, round))
	local dark_gen_min_r = tostring(utils._round_dec(dark_gen_min, round))
	local dark_gen_max_r = tostring(utils._round_dec(dark_gen_max, round))
	
	intro = intro .. string.format("%s{{cent}}から%s{{cent}}、あるいは%s{{cent}}から%s{{cent}}までの[[ジェネレーター]]を用いて作られる。", bright_gen_min_r, bright_gen_max_r, dark_gen_min_r, dark_gen_max_r)
	
	-- Rothenberg propriety (rothenprop) info
	if ns == 1 then
		intro = intro .. "この形式の音階は、狭い音程が1つしかないため、常に{{En仮リンク|厳密に適正|Rothenberg propriety}}となる。"
	elseif ns / n == 1 then
		intro = intro .. "全ての周期が等しく、真にMOSである場合は、1周期につき狭い音程が1つしかないため、{{En仮リンク|厳密に適正|Rothenberg propriety}}である。"
	end
	
	return intro
end

-- Function for use with a template
function p.mos_intro_frame(frame)
	-- Get and parse the the mos's scale signature, in the form xL ys or xL ys <p/q>
	local input_mos = mos.parse(frame.args["Scale Signature"]) or mos.new(5, 2, 2)
	local other_names = frame.args["Other Names"] or ""
	
	local depparams = (other_names ~= "" and " [[Category:Pages with deprecated parameters]]" or "")
	
    local result = p._mos_intro(input_mos, other_names) .. depparams
    local debugg = yesno(frame.args["debug"])
    
    -- Debugger option
    if debugg == true then
		result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
	end
	
	return frame:preprocess(result)
end

return p