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