MSeq

MSeq

1. Overview

A sequencing language for morphemes, inspired by Prop, a rhythmic notation language based on proportions.

A morpheme is a slice of time that contains a set of gesture paths with proportional durations. The duration of a morpheme can be scaled using a fractional value similar to the rate scaler used in a (regular, not proportional) gesture path. A morpheme sequence is a list of morphemes and their rate multiplier.

In lua, a morpheme sequence can be represented as a table like so:

seq = {
    {A, {1, 1}},
    {B, {2, 1}},
    {C, {2, 1}},
}

In mseq, this can more succicintly be represented as:

A2(BC)

Patterns are typically single alphabetic letters.

A positive integer N followed by a parentheses () with a pattern inside of it will shrink that pattern by a factor of N.

A positive integer N followed by a brackets [] with a pattern inside of it will grow that pattern by a factor of N.

2. Tangled File

Called mseq.lua.

<<mseq.lua>>=
Mseq = {}

<<grammar>>
<<tree_traversal>>
<<parsing>>
<<parsing_symbol_lookup>>
return Mseq

3. lpeg grammar

LPeg is used to define the grammar of the parser. It is set up below.

<<grammar>>=
local Space = lpeg.S(" \t\n")^0
local Morpheme = lpeg.R("AZ")*lpeg.R("az")^0*Space
local Exp, Pat, S = lpeg.V"Exp", lpeg.V"Pat", lpeg.V"S"
local Mul = lpeg.V"Mul"
local Div = lpeg.V"Div"
local Seq = lpeg.V"Seq"
local Num = lpeg.R("09")^1
local LParen = lpeg.P("(")
local RParen = lpeg.P(")")
local LBrack = lpeg.P("[")
local RBrack = lpeg.P("]")

local G = lpeg.P {
	Exp,
	-- Exp = lpeg.Ct(Mul) + lpeg.Ct(Pat);
	Exp = lpeg.Ct((Space*Seq*Space)^0);
	--Pat = lpeg.Cg(Morpheme)^0 + lpeg.Cg(Mul)^0;
	Pat = Mul;
	Seq = lpeg.Cg(Morpheme) + lpeg.Ct(Mul) + lpeg.Ct(Div);
	Mul =
		lpeg.Cg(Num, "mul") *
		LParen * lpeg.Cg(lpeg.Ct(Seq^1), "seq") *
		RParen;
	Div =
		lpeg.Cg(Num, "div") *
		LBrack * lpeg.Cg(lpeg.Ct(Seq^1), "seq") *
		RBrack

}

4. Parsing

parse is the thing that parses a MSeq string. It gets passed into the generated lpeg grammar, which then produces a capture table that resembles a tree-like structure (the tree-like structure comes from the nested aspects of mseq).

str is the string to be parsed.

lookup is a lookup table for morpheme values. It is assumed that each morpheme contains the same set of paths. This isn't checked.

r is an optional rate multipler that can be applied to the overall sequence. By default, it is set to be (1, 1), which will cause a morpheme to take up a duration of 1 beat.

<<parsing>>=
function Mseq.parse(str, lookup, r)
    local S = {}
    local t = lpeg.match(G, str)
	r = r or {1, 1}

    if t == nil then
        error("mseq: invalid string")
    end

    iterate(t, lookup, r, S)

    return S
end

5. Tree Traversal

<<tree_traversal>>=
function iterate(x, m, r, out)
	for _, v in pairs(x) do
		if type(v) == "string" then
			table.insert(out, {m[v], {r[1], r[2]}})
		else
			r_new = {r[1], r[2]}
			if v.div ~= nil then
				r_new[2] = r_new[2] * v.div
			elseif v.mul ~= nil then
				r_new[1] = r_new[1] * v.mul
			end
			iterate(v.seq, m, r_new, out)
		end
	end
end

6. Two-Phase Parse With Symbol Lookup (v2)

This parser splits the sequence up into two parts: the first part generates the morpheme sequence with the morphemes as symbols instead of the actual data. The second part uses a lookup table to convert the symbols into morphemes.

<<parsing_symbol_lookup>>=
<<iterate2>>
<<resolve>>
function Mseq.parse2(str, r)
    local S = {}
    local t = lpeg.match(G, str)
	r = r or {1, 1}

    if t == nil then
        error("mseq: invalid string")
    end

    iterate2(t, r, S)

    return S
end
<<iterate2>>=
function iterate2(x, r, out)
	for _, v in pairs(x) do
		if type(v) == "string" then
			table.insert(out, {v, {r[1], r[2]}})
		else
			r_new = {r[1], r[2]}
			if v.div ~= nil then
				r_new[2] = r_new[2] * v.div
			elseif v.mul ~= nil then
				r_new[1] = r_new[1] * v.mul
			end
			iterate2(v.seq, r_new, out)
		end
	end
end

The resolve function that replaces symbols with morphemes is so straight forward, it may be not a bad idea to just copy-paste these lines of code and avoid using mseq as a dependency if you generate morpheme sequences ahead of time.

<<resolve>>=
function Mseq.resolve(seq, lookup)
    local o = {}

    for _, v in pairs(seq) do
        table.insert(o, {lookup[v[1]], v[2]})
    end

    return o
end