Skip to content

Instantly share code, notes, and snippets.

@HertzDevil
Last active September 3, 2016 22:22
Show Gist options
  • Save HertzDevil/8486deb7a8689a86b5c948b0ba371a7b to your computer and use it in GitHub Desktop.
Save HertzDevil/8486deb7a8689a86b5c948b0ba371a7b to your computer and use it in GitHub Desktop.
how to write an MML compiler
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--- The Mega Man 3 - 6 MML engine.
-- @module mm3
-- [Commands Reference](https://gist.github.com/HertzDevil/0f868d77a32f92c2877b7ce304f29c53)
local require = require
local ipairs = ipairs
local pairs = pairs
local tonumber = tonumber
local tostring = tostring
local remove = table.remove
local Default = require "default"
local MML = require "mml"
local Music = require "music"
local Class = require "util.class"
local Trie = require "util.trie"
local ChunkNum = require "music.chunk.num"
local Check = Default.Errors.RuntimeCheck
local ParamAssert = Default.Errors.ParamCheck
local CmdAssert = Default.Errors.CommandCheck
local builder = MML.CmdBuilder()
-- misc
local CHANNELS = 4
local LOOP_DEPTH = 4
local DUR_TO_BYTE = {
[ 1] = 0xE0, [ 2] = 0xC0, [ 4] = 0xA0, [8] = 0x80,
[16] = 0x60, [32] = 0x40, [64] = 0x20
}
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
-- for dotted notes
local ChunkNote = Class({}, require "music.chunk.num")
local ChunkDot = Class({__init = function (self)
self.__super.__init(self, 0x02)
end}, require "music.chunk.num")
local ChunkTie = Class({__init = function (self)
self.__super.__init(self, 0x01)
end}, require "music.chunk.num")
-- our pointer implementation
local Pointer = Class({
__init = function (self, dest, name)
self.__super.__init(self, dest, name, 2)
end,
compile = function (self)
local s = Check(self.dest, "Unknown pointer destination")
return ChunkNum(s:getBase() + s:getLabel(self.name), "2>"):compile()
end,
}, require "music.chunk.pointer")
-- stack for generating loop labels
local LoopID = Class {
__init = function (self, maxlevels)
self.maxlv, self.val, self.x = maxlevels, {}, 0
end,
size = function (self)
return #self.val
end,
push = function (self)
assert(#self.val < self.maxlv)
self.x = self.x + 1
table.insert(self.val, self.x)
return self.x
end,
top = function (self)
assert(#self.val > 0)
return self.val[#self.val]
end,
pop = function (self)
assert(#self.val > 0)
return table.remove(self.val)
end,
}
-- lexer
local lengthLexer; do
local LENGTHS = Trie()
for k in pairs(DUR_TO_BYTE) do
LENGTHS:add(tostring(k))
end
lengthLexer = function (sv)
local k = ParamAssert(LENGTHS:lookup(sv))
sv.b = sv.b + #k
return tonumber(k)
end; end
local accLexer; do
accLexer = function (sv)
local neutral = sv:trim "=" ~= nil
local shift = 0
for ch in sv:trim "[+-]*":gmatch "." do
shift = shift + (ch == "+" and 1 or -1)
end
return {shift = shift, neutral = neutral}
end; end
-- channel state
local Channel = Class({
__init = function (self, ...)
self.__super.__init(self, ...)
self.isTriplet = false
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(1)
self.octcmdval = 0
self.hasloop = false
self.loopid = LoopID(LOOP_DEPTH)
end,
after = function (self)
if self.hasloop then
self:addChunk(0x16, Pointer(self.stream, "LOOP"))
else
self:addChunk(0x17)
end
end,
}, Music.Channel)
-- song state
local Song = Class({
}, Music.Song)
-- MML command definitions
builder:setTable(Default.Macros())
-- direct input
builder:setHandler(Class.call "addChunk"):param "Uint8":make "~"
-- notes, repeat, rest
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, dur)
local n = val + (acc.neutral and 0 or ch.key[name])
+ acc.shift + ch.octave:get() * 12 -- query
CmdAssert(n > 0 and n <= 0x1F, "Note out of range")
if not dur then dur = ch.duration:get() end
dur = CmdAssert(DUR_TO_BYTE[dur], "Invalid note duration")
if not ch.muted then
ch:addChunk(ChunkNote(dur + n))
end
ch.lastnote = n
end):param(accLexer):param(lengthLexer):optional():make(name)
end
builder:setHandler(function (ch, dur)
local n = CmdAssert(ch.lastnote, "No previous note")
if not dur then dur = ch.duration:get() end
dur = CmdAssert(DUR_TO_BYTE[dur], "Invalid note duration")
if not ch.muted then
ch:addChunk(ChunkNote(dur + n))
end
end):param(lengthLexer):optional():make "x"
builder:setHandler(function (ch, dur)
if not dur then dur = ch.duration:get() end
dur = CmdAssert(DUR_TO_BYTE[dur], "Invalid note duration")
if not ch.muted then
ch:addChunk(ChunkNote(dur))
end
end):param(lengthLexer):optional():make "r"
-- note modifiers
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(DUR_TO_BYTE[x], "Invalid default note duration")
ch.duration:set(x)
end):param "Uint8":make "l"
builder:setHandler(function (ch)
local last = {ch:unget()}
CmdAssert(#last == 1 and last[1].__class == ChunkNote,
"Dot applied to non-note command")
ch:addChunk(ChunkDot(), last[1])
end):make "."
builder:setHandler(function (ch)
local last = {ch:unget()}
local dot = nil
if #last >= 2 then
dot = remove(last, 1)
CmdAssert(dot.__class == ChunkDot, "Tie applied to non-note command")
end
CmdAssert(#last == 1 and last[1].__class == ChunkNote,
"Tie applied to non-note command")
local extratie = ch:popChunk(1)
if not extratie or extratie.__class ~= ChunkTie then
ch:addChunk(ChunkTie())
end
if dot then ch:addChunk(0x02) end
ch:addChunk(last[1], ChunkTie())
end):make "&"
-- octave-related
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 < 2, "Octave out of range")
ch.octave:set(n + 1)
end):make ">"
builder:setHandler(function (ch, x)
CmdAssert(x < 8, "Octave out of range")
ch:addChunk(0x09, x)
ch.octcmdval = x
ch.octave:set(1)
end):param "Uint8":make "O"
builder:setHandler(function (ch)
ch.octcmdval = ch.octcmdval - 1
CmdAssert(ch.octcmdval >= 0, "Octave out of range")
ch:addChunk(0x09, ch.octcmdval)
ch.octave:set(1)
end):make "O<"
builder:setHandler(function (ch)
ch.octcmdval = ch.octcmdval + 1
CmdAssert(ch.octcmdval < 8, "Octave out of range")
ch:addChunk(0x09, ch.octcmdval)
ch.octave:set(1)
end):make "O>"
-- engine commands
builder:setHandler(function (ch)
CmdAssert(not ch.isTriplet, "Cannot start triplet block here")
ch:addChunk(0x00)
ch.isTriplet = true
end):make "{"
builder:setHandler(function (ch)
CmdAssert(ch.isTriplet, "Cannot end triplet block here")
ch:addChunk(0x00)
ch.isTriplet = false
end):make "}"
builder:quickcmd("`" , 0x03)
builder:setHandler(function (ch, x)
ch:addChunk(0x05, ChunkNum(x, "2>"))
end):param "Uint16":make "T"
builder:quickcmd("Q" , 0x06, "Uint8")
builder:setHandler(function (ch, x)
CmdAssert(x <= 15, "Invalid channel volume")
ch:addChunk(0x07, x)
end):param "Uint8":make "V"
builder:quickcmd("@" , 0x08, "Uint8")
builder:quickcmd("_M", 0x0A, "Int8")
builder:quickcmd("_" , 0x0B, "Int8")
builder:setHandler(function (ch, x)
ch:addChunk(0x0C, (-x) % 0x100)
end):param "Int8":make "D"
builder:quickcmd("P" , 0x0D, "Uint8")
builder:setHandler(function (ch, x)
CmdAssert(x <= 3, "Invalid duty setting")
ch:addChunk(0x18, x * 0x40)
end):param "Uint8":make "W"
-- looping
builder:setHandler(function (ch)
CmdAssert(ch:getStreamLevel() < LOOP_DEPTH,
"TODO: Allow more than 4 levels of nested loops")
ch.stream:addLabel(ch.loopid:push())
local s = ch.stream
local pos = s.size
ch:pushStream().getBase = function (self)
return s:getBase() + pos
end
end):make "["
builder:setHandler(function (ch, x)
CmdAssert(x > 0 and x <= 256, "Invalid loop count")
local s = ch:popStream()
s:push(0x0E + ch:getStreamLevel())
s:push(x - 1)
s:push(Pointer(ch.stream, ch.loopid:pop()))
s:addLabel "BREAK"
ch.stream:join(s)
end):param "Uint":make "]"
builder:setHandler(function (ch)
CmdAssert(not ch.stream.hasbreak, "Multiple break points in a loop")
ch.stream.hasbreak = true
local ptr = Pointer(ch.stream, "BREAK")
ch:addChunk(0x11 + ch:getStreamLevel(), 0, ptr)
end):make ":"
builder:setHandler(function (ch)
CmdAssert(ch.looppoint == nil, "Duplicate channel loop point")
ch.stream:addLabel "LOOP"
ch.hasloop = true
end):make "/"
local mtable = builder:getTable()
-- Preprocessor directives
builder:setTable(Default.Directives())
local dtable = builder:getTable()
-- music inserter
local Inserter = function (rom, song, track)
local header = Music.Stream()
header:addLabel "TARGET"
header:push(0x00)
song:doAll(function (ch)
header:push(Pointer(ch.stream, "START"))
end)
local pos = 0x8C9D
header:setBase(0x8C9D)
local previous = header
song:doAll(function (ch)
ch.stream:setBase(previous:getBase() + previous.size)
previous = ch.stream
end)
rom:seek("set", pos - 0x7F80)
rom:write(header:build())
song:doAll(function (ch)
rom:write(ch.stream:build())
end)
local tableptr = Music.Stream()
tableptr:push(Pointer(header, "TARGET"))
rom:seek("set", 0xAC1 + 2 * track)
rom:write(tableptr:build())
end
-- export module
builder.quickcmd = nil
return {
name = "Mega Man 3 - 6",
song = Song,
channel = Channel,
chcount = CHANNELS,
parser = MML.Parser(mtable, dtable),
inserter = Inserter
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment