local require = require
local Default = require "default"
local MML = require "mml"
local Music = require "music"
local Class = require "util.class"
local ChunkNum = require "music.chunk.num"
local Warning = require "util.warning"
local Check = Default.Errors.RuntimeCheck
local ParamAssert = Default.Errors.ParamCheck
local CmdAssert = Default.Errors.CommandCheck
local builder = MML.CmdBuilder()
function builder:quickcmd (mmlname, outbyte, ...)
builder:setHandler(function (ch, ...)
ch:addChunk(outbyte, ...)
end)
for _, v in ipairs {...} do builder:param(v) end
return builder:make(mmlname)
end
local CHANNELS = 4
local engine = Default.Engine(CHANNELS)
local DURATION = {[1] = 32, [2] = 16, [4] = 8, [8] = 4, [16] = 2, [32] = 1}
local lengthLexer; do
local getvalue = function (sv)
local raw = sv:trim "%%" ~= nil
local l = sv:trim "%d*"
if raw then return 0, tonumber(l) end
sv:ws()
local dot = 2 - .5 ^ #sv:trim "%.*"
if l ~= "" then
return 0, ParamAssert(DURATION[tonumber(l)]) * dot
end
return dot, 0
end
lengthLexer = function (sv)
local mult, add = getvalue(sv)
while sv:trim "%s*&%s*" ~= nil do
local a, b = getvalue(sv)
mult, add = mult + a, add + b
end
return function (x)
local ticks = mult * x + add
local z = math.floor(ticks)
CmdAssert(z == ticks and z >= 1 and z <= 256, "Invalid note duration")
if z == 256 then z = 0 end
return z
end
end; end
local Pointer = Class({
__init = function (self, dest, name)
self.__super.__init(self, dest, name, 2)
end,
compile = function (self)
local s = CmdAssert(self.dest, "Unknown pointer destination")
local label = CmdAssert(s:getLabel(self.name), "Unknown pointer label")
return ChunkNum(s:getBase() + label, "2<"):compile()
end,
}, require "music.chunk.pointer")
-- table with numeric keys sorted ascending for iteration
local NumSort = function ()
local order = {}
return setmetatable({}, {
__metatable = "NumSort",
__newindex = function (t, k, v)
rawset(t, k, v)
if not order[1] then order[1] = k; return end
if k < order[1] then table.insert(order, 1, k); return end
if k > order[#order] then order[#order + 1] = k; return end
local b, e = 1, #order
while e - b > 1 do
local m = math.ceil((b + e) / 2)
if k < order[m] then e = m
elseif k > order[m] then b = m
else break end
end
table.insert(order, e, k)
end,
__len = function (t)
return #order
end,
__pairs = function (t)
return coroutine.wrap(function ()
for _, v in ipairs(order) do
coroutine.yield(v, t[v])
end
end)
end,
})
end
-- Channel state
local Channel = engine.channel.__mt.__index
function Channel:__init (...)
self.__super.__init(self, ...)
self.muted = false
self.key = {c = 0, d = 0, e = 0, f = 0, g = 0, a = 0, b = 0}
self.lastnote = nil
self.duration = Music.State(4)
self.octave = Music.State(2)
self.gate = 0
self.patterns = NumSort()
self.anonpatterns = {}
end
function Channel:after ()
self:addChunk(0x00)
end
-- MML command table
builder:setTable(engine.parser.macros)
builder:setHandler(Class.call "addChunk"):param "Uint8":variadic():make "`"
for name, val in pairs {c = 1, d = 3, e = 5, f = 6, g = 8, a = 10, b = 12} do
builder:setHandler(function (ch, acc, durfunc)
local n = val + (acc.neutral and 0 or ch.key[name])
+ acc.shift + ch.octave:get() * 12 + 0x80
CmdAssert(n >= 0x81 and n <= 0xBC, "Note out of range")
if n < 0x8A then
Warning.warn "Notes below A-1 will not produce any sound in this engine"
end
if not ch.muted then
local l = durfunc(DURATION[ch.duration:get()])
if l == 1 then
ch:addChunk(n, l)
else
local lreal = math.max(1, l - ch.gate)
ch:addChunk(n, lreal)
if l > lreal then
ch:addChunk(0x80, l - lreal)
end
end
end
ch.lastnote = n
end):param "Acc":param(lengthLexer):make(name)
end
builder:setHandler(function (ch, t)
for k, v in pairs(t) do
ch.key[k] = v
end
end):param "KeySig":make "k"
builder:setHandler(function (ch, x)
ch.muted = x
end):param "Bool":make "m"
builder:setHandler(function (ch, x)
CmdAssert(DURATION[x], "Invalid default note duration")
ch.duration:set(x)
end):param "Uint8":make "l"
builder:setHandler(function (ch, x)
ch.gate = x
end):param "Uint8":make "q"
builder:setHandler(function (ch)
local n = ch.octave:get()
CmdAssert(n > 0, "Octave out of range")
ch.octave:set(n - 1)
end):make "<"
builder:setHandler(function (ch)
local n = ch.octave:get()
CmdAssert(n < 4, "Octave out of range")
ch.octave:set(n + 1)
end):make ">"
builder:setHandler(function (ch, x)
CmdAssert(x <= 4, "Octave out of range")
ch.octave:set(x)
end):param "Uint8":make "o"
builder:setHandler(function (ch, x)
CmdAssert(x <= 3, "Invalid duty setting")
ch:addChunk(0x03, x * 0x40 + 0x30, 0xA0, 0x21)
end):param "Uint8":make "W"
builder:setHandler(function (ch, id)
local s = ch:pushStream()
if not id then
table.insert(ch.anonpatterns, s)
else
CmdAssert(not ch.patterns[id], "Duplicate pattern index")
ch.patterns[id] = s
end
end):param "Uint":optional():make "["
builder:setHandler(function (ch, x)
CmdAssert(x >= 1 and x <= 256, "Invalid loop count")
if x == 256 then x = 0 end
local s = ch:popStream()
s:push(0x05)
ch:addChunk(0x04, x, Pointer(s, "START"))
end):param "Uint":optional "1":make "]"
builder:setHandler(function (ch, id, x)
CmdAssert(x >= 1 and x <= 256, "Invalid loop count")
if x == 256 then x = 0 end
local s = CmdAssert(ch.patterns[id], "Unknown pattern index")
ch:addChunk(0x04, x, Pointer(s, "START"))
end):param "Uint":param "Uint":optional "1":make "[]"
builder:quickcmd("V", 0x0C, "Uint8")
builder:quickcmd("_", 0x12, "Int8")
builder:quickcmd("@V", 0x16, "Uint8", "Uint8", "Uint8")
-- Directive table
builder:setTable(engine.parser.directives)
-- Inserter function
engine.inserter = function (rom, song, track)
local header = Music.Stream()
header:addLabel "TARGET"
song:doAll(function (ch)
header:push(Pointer(ch.stream, "START"))
end)
rom:seek("set", 0x23E4 + 8 * (track - 1))
header:setBase(rom:seek() + 0x7F80)
local pos = rom:read(1):byte()
pos = pos + rom:read(1):byte() * 0x100
rom:seek("cur", -2)
local s = pos
song:doAll(function (ch)
ch.stream:setBase(s)
s = s + ch.stream.size
for _, v in pairs(ch.patterns) do
v:setBase(s)
s = s + v.size
end
for _, v in ipairs(ch.anonpatterns) do
v:setBase(s)
s = s + v.size
end
end)
rom:write(header:build())
rom:seek("set", pos - 0x7F80)
song:doAll(function (ch)
rom:write(ch.stream:build())
for _, v in pairs(ch.patterns) do
rom:write(v:build())
end
for _, v in ipairs(ch.anonpatterns) do
rom:write(v:build())
end
end)
end
-- export module
builder.quickcmd = nil
return engine
Comments
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|0 👍
/0 👎