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 = {}
<<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.
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.
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
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.
<<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
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.
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