Last active
September 3, 2016 22:22
-
-
Save HertzDevil/8486deb7a8689a86b5c948b0ba371a7b to your computer and use it in GitHub Desktop.
how to write an MML compiler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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